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模板查詢,後面都是命中了快取。
在我們的測試環境中由於資料量比較小,快取對查詢速度的優化並不明顯。這裡就不過多說明了。
最後上一篇打臉文。給你參考http://blog.csdn.net/isea533/article/details/44566257
完畢。