springboot+mybatis+redis 二级缓存问题实例详解
前言
什么是mybatis二级缓存?
二级缓存是多个sqlsession共享的,其作用域是mapper的同一个namespace。
即,在不同的sqlsession中,相同的namespace下,相同的sql语句,并且sql模板中参数也相同的,会命中缓存。
第一次执行完毕会将数据库中查询的数据写到缓存,第二次会从缓存中获取数据将不再从数据库查询,从而提高查询效率。
mybatis默认没有开启二级缓存,需要在全局配置(mybatis-config.xml)中开启二级缓存。
本文讲述的是使用redis作为缓存,与springboot、mybatis进行集成的方法。
1、pom依赖
使用springboot redis集成包,方便redis的访问。redis客户端选用jedis。
另外读写kv缓存会进行序列化,所以引入了一个序列化包。
<dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-redis</artifactid> </dependency> <dependency> <groupid>redis.clients</groupid> <artifactid>jedis</artifactid> <version>2.8.0</version> </dependency> <dependency> <groupid>com.alibaba</groupid> <artifactid>fastjson</artifactid> <version>1.2.19</version> </dependency>
依赖搞定之后,下一步先调通redis客户端。
2、redis访问使用的bean
增加configuration,配置jedisconnectionfactory bean,留待后面使用。
一般来讲,也会生成了redistemplate bean,但是在接下来的场景没有使用到。
@configuration public class redisconfig { @value("${spring.redis.host}") private string host; // 篇幅受限,省略了 @bean public jedispoolconfig getredisconfig(){ jedispoolconfig config = new jedispoolconfig(); config.setmaxidle(maxidle); config.setmaxtotal(maxtotal); config.setmaxwaitmillis(maxwaitmillis); config.setminidle(minidle); return config; } @bean(name = "jedisconnectionfactory") public jedisconnectionfactory getconnectionfactory(){ jedisconnectionfactory factory = new jedisconnectionfactory(); jedispoolconfig config = getredisconfig(); factory.setpoolconfig(config); factory.sethostname(host); factory.setport(port); factory.setdatabase(database); factory.setpassword(password); factory.settimeout(timeout); return factory; } @bean(name = "redistemplate") public redistemplate<?, ?> getredistemplate(){ redistemplate<?,?> template = new stringredistemplate(getconnectionfactory()); return template; } }
这里使用@value读入了redis相关配置,有更简单的配置读取方式(@configurationproperties(prefix=...)),可以尝试使用。
redis相关配置如下
#redis spring.redis.host=10.93.84.53 spring.redis.port=6379 spring.redis.password=bigdata123 spring.redis.database=15 spring.redis.timeout=0 spring.redis.pool.maxtotal=8 spring.redis.pool.maxwaitmillis=1000 spring.redis.pool.maxidle=8 spring.redis.pool.minidle=0
redis客户端的配置含义,这里不再讲解了。pool相关的一般都和性能有关,需要根据并发量权衡句柄、内存等资源进行设置。
redis客户端设置好了,我们开始配置redis作为mybatis的缓存。
3、mybatis cache
这一步是最为关键的一步。实现方式是实现mybatis的一个接口org.apache.ibatis.cache.cache。
这个接口设计了写缓存,读缓存,销毁缓存的方式,和访问控制读写锁。
我们实现实现cache接口的类是mybatisrediscache。
mybatisrediscache.java
public class mybatisrediscache implements cache { private static jedisconnectionfactory jedisconnectionfactory; private final string id; private final readwritelock readwritelock = new reentrantreadwritelock(); public mybatisrediscache(final string id) { if (id == null) { throw new illegalargumentexception("cache instances require an id"); } this.id = id; } @override public void clear() { redisconnection connection = null; try { connection = jedisconnectionfactory.getconnection(); connection.flushdb(); connection.flushall(); } catch (jedisconnectionexception e) { e.printstacktrace(); } finally { if (connection != null) { connection.close(); } } } @override public string getid() { return this.id; } @override public object getobject(object key) { object result = null; redisconnection connection = null; try { connection = jedisconnectionfactory.getconnection(); redisserializer<object> serializer = new jdkserializationredisserializer(); result = serializer.deserialize(connection.get(serializer.serialize(key))); } catch (jedisconnectionexception e) { e.printstacktrace(); } finally { if (connection != null) { connection.close(); } } return result; } @override public readwritelock getreadwritelock() { return this.readwritelock; } @override public int getsize() { int result = 0; redisconnection connection = null; try { connection = jedisconnectionfactory.getconnection(); result = integer.valueof(connection.dbsize().tostring()); } catch (jedisconnectionexception e) { e.printstacktrace(); } finally { if (connection != null) { connection.close(); } } return result; } @override public void putobject(object key, object value) { redisconnection connection = null; try { connection = jedisconnectionfactory.getconnection(); redisserializer<object> serializer = new jdkserializationredisserializer(); connection.set(serializer.serialize(key), serializer.serialize(value)); } catch (jedisconnectionexception e) { e.printstacktrace(); } finally { if (connection != null) { connection.close(); } } } @override public object removeobject(object key) { redisconnection connection = null; object result = null; try { connection = jedisconnectionfactory.getconnection(); redisserializer<object> serializer = new jdkserializationredisserializer(); result = connection.expire(serializer.serialize(key), 0); } catch (jedisconnectionexception e) { e.printstacktrace(); } finally { if (connection != null) { connection.close(); } } return result; } public static void setjedisconnectionfactory(jedisconnectionfactory jedisconnectionfactory) { mybatisrediscache.jedisconnectionfactory = jedisconnectionfactory; } }
注意:
可以看到,这个类并不是由spring虚拟机管理的类,但是,其中有一个静态属性jedisconnectionfactory需要注入一个spring bean,也就是在redisconfig中生成的bean。
在一个普通类中使用spring虚拟机管理的bean,一般使用springboot自省的springcontextaware。
这里使用了另一种方式,静态注入的方式。这个方式是通过rediscachetransfer来实现的。
4、静态注入
rediscachetransfer.java
@component public class rediscachetransfer { @autowired public void setjedisconnectionfactory(jedisconnectionfactory jedisconnectionfactory) { mybatisrediscache.setjedisconnectionfactory(jedisconnectionfactory); } }
可以看到rediscachetransfer是一个springboot bean,在容器创建之初进行初始化的时候,会注入jedisconnectionfactory bean给setjedisconnectionfactory方法的传参。
而setjedisconnectionfactory通过调用静态方法设置了类mybatisrediscache的静态属性jedisconnectionfactory。
这样就把spring容器管理的jedisconnectionfactory注入到了静态域。
到这里,代码基本已经搞定,下面是一些配置。主要有(1)全局开关;(2)namespace作用域开关;(3)model实例序列化。
5、mybatis二级缓存的全局开关
前面提到过,默认二级缓存没有打开,需要设置为true。这是全局二级缓存的开关。
mybatis的全局配置。
<?xml version="1.0" encoding="utf-8"?> <!doctype configuration public "-//mybatis.org//dtd config 3.0//en" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <!-- 全局参数 --> <settings> <!-- 使全局的映射器启用或禁用缓存。 --> <setting name="cacheenabled" value="true"/> </settings> </configuration>
全局配置的加载在datasource中可以是这样的。
bean.setmapperlocations(new pathmatchingresourcepatternresolver().getresources("classpath:mybatis-mapper/*.xml"));
指定了mapper.xml的存放路径,在mybatis-mapper路径下,所有后缀是.xml的都会读入。
bean.setconfiglocation(new classpathresource("mybatis-config.xml"));
指定了mybatis-config.xml的存放路径,直接放在resource目录下即可。
@bean(name = "moonlightsqlsessionfactory") @primary public sqlsessionfactory moonlightsqlsessionfactory(@qualifier("moonlightdata") datasource datasource) throws exception { sqlsessionfactorybean bean = new sqlsessionfactorybean(); bean.setdatasource(datasource); bean.setmapperlocations(new pathmatchingresourcepatternresolver().getresources("classpath:mybatis-mapper/*.xml")); bean.setconfiglocation(new classpathresource("mybatis-config.xml")); return bean.getobject(); }
6、配置mapper作用域namespace
前面提到过,二级缓存的作用域是mapper的namespace,所以这个配置需要到mapper中去写。
<mapper namespace="com.kangaroo.studio.moonlight.dao.mapper.moonlightmapper"> <cache type="com.kangaroo.studio.moonlight.dao.cache.mybatisrediscache"/> <resultmap id="geofencelist" type="com.kangaroo.studio.moonlight.dao.model.geofence"> <constructor> <idarg column="id" javatype="java.lang.integer" jdbctype="integer" /> <arg column="name" javatype="java.lang.string" jdbctype="varchar" /> <arg column="type" javatype="java.lang.integer" jdbctype="integer" /> <arg column="group" javatype="java.lang.string" jdbctype="varchar" /> <arg column="geo" javatype="java.lang.string" jdbctype="varchar" /> <arg column="createtime" javatype="java.lang.string" jdbctype="varchar" /> <arg column="updatetime" javatype="java.lang.string" jdbctype="varchar" /> </constructor> </resultmap> <select id="querygeofence" parametertype="com.kangaroo.studio.moonlight.dao.model.geofencequeryparam" resultmap="geofencelist"> select <include refid="base_column"/> from geofence where 1=1 <if test="type != null"> and type = #{type} </if> <if test="name != null"> and name like concat('%', #{name},'%') </if> <if test="group != null"> and `group` like concat('%', #{group},'%') </if> <if test="starttime != null"> and createtime >= #{starttime} </if> <if test="endtime != null"> and createtime <= #{endtime} </if> </select> </mapper>
注意:
namespace下的cache标签就是加载缓存的配置,缓存使用的正式我们刚才实现的mybatisrediscache。
<cache type="com.kangaroo.studio.moonlight.dao.cache.mybatisrediscache"/>
这里只实现了一个查询querygeofence,你可以在select标签中,开启或者关闭这个sql的缓存。使用属性值usecache=true/false。
7、mapper和model
读写缓存model需要序列化:只需要类声明的时候实现seriaziable接口就好了。
public class geofence implements serializable { // setter和getter省略 } public class geofenceparam implements serializable { // setter和getter省略 }
mapper就还是以前的写法,使用mapper.xml的方式这里只需要定义出抽象函数即可。
@mapper public interface moonlightmapper { list<geofence> querygeofence(geofencequeryparam geofencequeryparam); }
到这里,所有的代码和配置都完成了,下面测试一下。
8、测试一下
controller中实现一个这样的接口post。
@requestmapping(value = "/fence/query", method = requestmethod.post) @responsebody public responseentity<response> queryfence(@requestbody geofencequeryparam geofencequeryparam) { try { integer pagenum = geofencequeryparam.getpagenum()!=null?geofencequeryparam.getpagenum():1; integer pagesize = geofencequeryparam.getpagesize()!=null?geofencequeryparam.getpagesize():10; pagehelper.startpage(pagenum, pagesize); list<geofence> list = moonlightmapper.querygeofence(geofencequeryparam); return new responseentity<>( new response(resultcode.success, "查询geofence成功", list), httpstatus.ok); } catch (exception e) { logger.error("查询geofence失败", e); return new responseentity<>( new response(resultcode.exception, "查询geofence失败", null), httpstatus.internal_server_error); }
使用curl发送请求,注意
1)-h - content-type:application/json方式
2)-d - 后面是json格式的参数包体
curl -h "content-type:application/json" -xpost http://。。。/moonlight/fence/query -d '{ "name" : "test", "group": "test", "type": 1, "starttime":"2017-12-06 00:00:00", "endtime":"2017-12-06 16:00:00", "pagenum": 1, "pagesize": 8
请求了三次,日志打印如下,
可以看到,只有第一次执行了sql模板查询,后面都是命中了缓存。
在我们的测试环境中由于数据量比较小,缓存对查询速度的优化并不明显。这里就不过多说明了。
上一篇: Photosho打造绚丽的潮流光斑