springboot+mybatis+redis 二級(jí)緩存問(wèn)題實(shí)例詳解
前言
什么是mybatis二級(jí)緩存?
二級(jí)緩存是多個(gè)sqlsession共享的,其作用域是mapper的同一個(gè)namespace。
即,在不同的sqlsession中,相同的namespace下,相同的sql語(yǔ)句,并且sql模板中參數(shù)也相同的,會(huì)命中緩存。
第一次執(zhí)行完畢會(huì)將數(shù)據(jù)庫(kù)中查詢的數(shù)據(jù)寫(xiě)到緩存,第二次會(huì)從緩存中獲取數(shù)據(jù)將不再?gòu)臄?shù)據(jù)庫(kù)查詢,從而提高查詢效率。
Mybatis默認(rèn)沒(méi)有開(kāi)啟二級(jí)緩存,需要在全局配置(mybatis-config.xml)中開(kāi)啟二級(jí)緩存。
本文講述的是使用Redis作為緩存,與springboot、mybatis進(jìn)行集成的方法。
1、pom依賴
使用springboot redis集成包,方便redis的訪問(wèn)。redis客戶端選用Jedis。
另外讀寫(xiě)kv緩存會(huì)進(jìn)行序列化,所以引入了一個(gè)序列化包。
<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>
依賴搞定之后,下一步先調(diào)通Redis客戶端。
2、Redis訪問(wèn)使用的Bean
增加Configuration,配置jedisConnectionFactory bean,留待后面使用。
一般來(lái)講,也會(huì)生成了redisTemplate bean,但是在接下來(lái)的場(chǎng)景沒(méi)有使用到。
@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相關(guān)配置,有更簡(jiǎn)單的配置讀取方式(@ConfigurationProperties(prefix=...)),可以嘗試使用。
Redis相關(guān)配置如下
#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相關(guān)的一般都和性能有關(guān),需要根據(jù)并發(fā)量權(quán)衡句柄、內(nèi)存等資源進(jìn)行設(shè)置。
Redis客戶端設(shè)置好了,我們開(kāi)始配置Redis作為Mybatis的緩存。
3、Mybatis Cache
這一步是最為關(guān)鍵的一步。實(shí)現(xiàn)方式是實(shí)現(xiàn)Mybatis的一個(gè)接口org.apache.ibatis.cache.Cache。
這個(gè)接口設(shè)計(jì)了寫(xiě)緩存,讀緩存,銷毀緩存的方式,和訪問(wèn)控制讀寫(xiě)鎖。
我們實(shí)現(xiàn)實(shí)現(xiàn)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; } }
注意:
可以看到,這個(gè)類并不是由Spring虛擬機(jī)管理的類,但是,其中有一個(gè)靜態(tài)屬性jedisConnectionFactory需要注入一個(gè)Spring bean,也就是在RedisConfig中生成的bean。
在一個(gè)普通類中使用Spring虛擬機(jī)管理的Bean,一般使用Springboot自省的SpringContextAware。
這里使用了另一種方式,靜態(tài)注入的方式。這個(gè)方式是通過(guò)RedisCacheTransfer來(lái)實(shí)現(xiàn)的。
4、靜態(tài)注入
RedisCacheTransfer.java
@Component public class RedisCacheTransfer { @Autowired public void setJedisConnectionFactory(JedisConnectionFactory jedisConnectionFactory) { MybatisRedisCache.setJedisConnectionFactory(jedisConnectionFactory); } }
可以看到RedisCacheTransfer是一個(gè)springboot bean,在容器創(chuàng)建之初進(jìn)行初始化的時(shí)候,會(huì)注入jedisConnectionFactory bean給setJedisConnectionFactory方法的傳參。
而setJedisConnectionFactory通過(guò)調(diào)用靜態(tài)方法設(shè)置了類MybatisRedisCache的靜態(tài)屬性jedisConnectionFactory。
這樣就把spring容器管理的jedisConnectionFactory注入到了靜態(tài)域。
到這里,代碼基本已經(jīng)搞定,下面是一些配置。主要有(1)全局開(kāi)關(guān);(2)namespace作用域開(kāi)關(guān);(3)Model實(shí)例序列化。
5、Mybatis二級(jí)緩存的全局開(kāi)關(guān)
前面提到過(guò),默認(rèn)二級(jí)緩存沒(méi)有打開(kāi),需要設(shè)置為true。這是全局二級(jí)緩存的開(kāi)關(guān)。
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> <!-- 全局參數(shù) --> <settings> <!-- 使全局的映射器啟用或禁用緩存。 --> <setting name="cacheEnabled" value="true"/> </settings> </configuration>
全局配置的加載在dataSource中可以是這樣的。
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mybatis-mapper/*.xml"));
指定了mapper.xml的存放路徑,在mybatis-mapper路徑下,所有后綴是.xml的都會(huì)讀入。
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
前面提到過(guò),二級(jí)緩存的作用域是mapper的namespace,所以這個(gè)配置需要到mapper中去寫(xiě)。
<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標(biāo)簽就是加載緩存的配置,緩存使用的正式我們剛才實(shí)現(xiàn)的MybatisRedisCache。
<cache type="com.kangaroo.studio.moonlight.dao.cache.MybatisRedisCache"/>
這里只實(shí)現(xiàn)了一個(gè)查詢queryGeoFence,你可以在select標(biāo)簽中,開(kāi)啟或者關(guān)閉這個(gè)sql的緩存。使用屬性值useCache=true/false。
7、Mapper和Model
讀寫(xiě)緩存Model需要序列化:只需要類聲明的時(shí)候?qū)崿F(xiàn)Seriaziable接口就好了。
public class GeoFence implements Serializable { // setter和getter省略 } public class GeoFenceParam implements Serializable { // setter和getter省略 }
mapper就還是以前的寫(xiě)法,使用mapper.xml的方式這里只需要定義出抽象函數(shù)即可。
@Mapper public interface MoonlightMapper { List<GeoFence> queryGeoFence(GeoFenceQueryParam geoFenceQueryParam); }
到這里,所有的代碼和配置都完成了,下面測(cè)試一下。
8、測(cè)試一下
Controller中實(shí)現(xiàn)一個(gè)這樣的接口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發(fā)送請(qǐng)求,注意
1)-H - Content-type:application/json方式
2)-d - 后面是json格式的參數(shù)包體
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
請(qǐng)求了三次,日志打印如下,
可以看到,只有第一次執(zhí)行了sql模板查詢,后面都是命中了緩存。
在我們的測(cè)試環(huán)境中由于數(shù)據(jù)量比較小,緩存對(duì)查詢速度的優(yōu)化并不明顯。這里就不過(guò)多說(shuō)明了。
相關(guān)文章
Intellij IDEA菜單欄不見(jiàn)了(Main Menu as Separat
有人問(wèn)博主,關(guān)于Intellij IDEA菜單欄找不到了,被不小心的操作給隱藏了,怎么辦?下面給大家分享解決方案,感興趣的朋友跟隨小編一起看看吧2024-06-06JAVA Spring中讓人頭痛的JAVA大事務(wù)問(wèn)題要如何解決你知道嗎
這篇文章主要介紹了Java Spring事務(wù)使用及驗(yàn)證過(guò)程詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2021-09-09IDEA設(shè)置JVM運(yùn)行參數(shù)的方法步驟
這篇文章主要介紹了IDEA設(shè)置JVM運(yùn)行參數(shù)的方法步驟,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08Java利用Strategy模式實(shí)現(xiàn)堆排序
策略設(shè)計(jì)模式(Strategy):可以整體的替換一個(gè)算法的實(shí)現(xiàn)部分,能夠整體的替換算法,能讓我們輕松地用不同方法解決同一個(gè)問(wèn)題。本文將利用Strategy模式實(shí)現(xiàn)堆排序,感興趣的可以學(xué)習(xí)一下2022-09-09詳解PowerDesigner之CDM、PDM、SQL之間轉(zhuǎn)換
這篇文章主要介紹了詳解PowerDesigner之CDM、PDM、SQL之間轉(zhuǎn)換的相關(guān)資料,希望通過(guò)本文能幫助到大家,需要的朋友可以參考下2017-10-10詳解SpringBoot 使用Spring Initializr 快速構(gòu)建工程(官方推薦)
本篇文章主要介紹了SpringBoot 使用Spring Initializr 快速構(gòu)建工程(官方推薦),非常具有實(shí)用價(jià)值,需要的朋友可以參考下2017-10-10Java詳細(xì)講解Math和Random類中有哪些常用方法
Math類位于java.lang包中,包含很多用于科學(xué)計(jì)算的類方法,這些方法可以直接通過(guò)類名調(diào)用。Random類獲取隨機(jī)數(shù),位于java.util包中,本篇帶你了解它們的常用方法2022-05-05