Preface
What is mybatis Level 2 Cache?
The secondary cache is shared by multiple SQLSessions, and its scope is the same namespace of the mapper.
That is, in different SQLsessions, under the same namespace, the same SQL statement, and the parameters in the SQL template are also the same, and the cache will be hit.
After the first execution, the data queried in the database will be written to the cache, and the data will be retrieved from the cache will no longer be queried from the database, thereby improving query efficiency.
Mybatis does not enable the secondary cache by default, and it is necessary to enable the secondary cache in the global configuration (mybatis-config.xml).
This article describes the method of using Redis as a cache to integrate with springboot and mybatis.
1. Pom dependency
Use the springboot redis integration package to facilitate access to redis. Jedis is used on the redis client.
In addition, read and write kv caches will be serialized, so a serialization package is introduced.
<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>
After the dependency is completed, the next step is to adjust the Redis client.
2. Beans used by Redis access
Add Configuration, configure the jedisConnectionFactory bean, and leave it to later use.
Generally speaking, a redisTemplate bean will also be generated, but it is not used in the next scenario.
@Configurationpublic class RedisConfig { @Value("${spring.redis.host}") private String host; // Space is limited, @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; }}Here, @Value is used to read the redis related configuration, and there is a simpler configuration reading method (@ConfigurationProperties(prefix=...)), which you can try.
Redis related configurations are as follows
#redisspring.redis.host=10.93.84.53spring.redis.port=6379spring.redis.password=bigdata123spring.redis.database=15spring.redis.timeout=0spring.redis.pool.maxTotal=8spring.redis.pool.maxWaitMillis=1000spring.redis.pool.maxIdle=8spring.redis.pool.minIdle=0
The configuration meaning of Redis client will not be explained here. Pool-related are generally related to performance, and they need to be set based on the concurrency amount of handles, memory and other resources.
The Redis client has been set up, and we start configuring Redis as the cache for Mybatis.
3. Mybatis Cache
This step is the most critical step. The implementation method is to implement an interface org.apache.ibatis.cache.Cache of Mybatis.
This interface designs write cache, read cache, destroy cache, and access control read and write locks.
The class we implement to implement the Cache interface is 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; }}Notice:
As you can see, this class is not a class managed by the Spring virtual machine, but there is a static property jedisConnectionFactory that requires injecting a Spring bean, that is, the bean generated in RedisConfig.
In a normal class, Spring ContextAware is generally used to use Springboot introspective SpringContextAware.
Another method is used here, static injection. This method is implemented through RedisCacheTransfer.
4. Static injection
RedisCacheTransfer.java
@Componentpublic class RedisCacheTransfer { @Autowired public void setJedisConnectionFactory(JedisConnectionFactory jedisConnectionFactory) { MybatisRedisCache.setJedisConnectionFactory(jedisConnectionFactory); }}You can see that RedisCacheTransfer is a springboot bean. When initializing the container at the beginning of creation, the jedisConnectionFactory bean will be injected into the parameter passing of the setJedisConnectionFactory method.
The setJedisConnectionFactory sets the static property of the class MybatisRedisCache by calling a static method.
This injects the jedisConnectionFactory managed by the spring container into the static domain.
At this point, the code has basically been completed, and the following are some configurations. The main ones are (1) global switch; (2) namespace scope switch; (3) Model instance serialization.
5. The global switch of Mybatis Level 2 cache
As mentioned earlier, the default Level 2 cache is not turned on and needs to be set to true. This is the switch for the global Level 2 cache.
Mybatis global configuration.
<?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> <!-- Global Parameters--> <settings> <!-- Make the global mapper enable or disable cache. --> <setting name="cacheEnabled" value="true"/> </settings></configuration>
The loading of global configurations in dataSource can be like this.
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mybatis-mapper/*.xml"));
Specifies the storage path of mapper.xml. Under the mybatis-mapper path, all suffixes with .xml will be read in.
bean.setConfigLocation(new ClassPathResource("mybatis-config.xml"));
The storage path of mybatis-config.xml is specified and placed directly in the Resource directory.
@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. Configure mapper scope namespace
As mentioned earlier, the scope of the secondary cache is the mapper namespace, so this configuration needs to be written in the 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>Notice:
The cache tag under namespace is the configuration of loading cache, and the cache usage is officially implemented by MybatisRedisCache we just implemented.
<cache type="com.kangaroo.studio.moonlight.dao.cache.MybatisRedisCache"/>
Only a query queryGeoFence is implemented here. You can turn on or off the cache of this SQL in the select tag. Use the property value useCache=true/false.
7. Mapper and Model
Read and write cache Model needs to be serialized: it only needs to implement the Seriaziable interface when the class is declared.
public class GeoFence implements Serializable { // setter and getter omitted} public class GeoFenceParam implements Serializable { // setter and getter omitted}Mapper is still the same as before. When using mapper.xml, you only need to define abstract functions.
@Mapperpublic interface MoonlightMapper { List<GeoFence> queryGeoFence(GeoFenceQueryParam geoFenceQueryParam);}At this point, all the code and configuration are completed, let's test it below.
8. Test it
A such interface POST is implemented in the Controller.
@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, "Query geoFence Success", list), HttpStatus.OK); } catch (Exception e) { logger.error("Query geoFence Failed", e); return new ResponseEntity<>( new Response(ResultCode.EXCEPTION, "Query geoFence failed", null), HttpStatus.INTERNAL_SERVER_ERROR); }Use curl to send requests, note
1) -H - Content-type:application/json method
2) -d - The following is the parameter package in json format
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": 8Requested three times, the log is printed as follows
As you can see, only the first time the SQL template query was executed, and the cache was hit.
In our test environment, due to the relatively small amount of data, the cache optimization of query speed is not obvious. I won't explain much here.