mybatis 自定義redis做二級緩存
前言
如果關注功能實現,可以直接看功能實現部分
何時使用二級緩存
一個宗旨---不常變的穩定而常用的
- 一級是默認開啓的sqlsession級別的。
- 只在單表中使用,且所有的操作都是一個namespace下
- 查詢多 增刪改少的情況下
緩存並不全是優點,缺點很明顯,緩存有時不是最新的數據。
二級緩存參數說明
這是一個跨Sqlsession級雖的緩存,是mapper級別的,也就是可以多個sqlsession訪問同一個mapper時生效
關鍵字 | 解讀 |
---|---|
eviction | 緩存回收策略 |
flushInterval | 刷新時間間隔,單位毫秒 |
size | 引用數目,代表緩存最多可以存多少對象 |
readOnly | 是否只讀默認false 如果True所有的sql返回的是一個對象,性能高併發安全性底,如果false返回的是序列化後的副本,安全高效率底 |
常用的回收策略(與操作系統內存回收策略差不多)
- LRU 默認,最近最少使用
- FIFO 先進先出
- SOFT 軟引用 移除基於垃圾回收器狀態和軟引用規則的對象
WEAK 弱引用 更積極的移除移除基於垃圾回收器狀態和弱引用規則的對象
sql中控制是否使用緩存及刷新緩存
<!-- 開啓二級緩存 -->
<setting name="cacheEnabled" value="true"/>
<select id="" resultMap="" useCache="false">
<update id="" parameterType="" flushCache="false" />
緩存何時會失效(意思就是構成key的因素一樣,但是沒有命中value)
一級失效
- 不在同一個Sqlsession中,例如未開啓事務,mybatis每次查詢都會關閉舊的創建新的。
- 增刪改操作程序會clear緩存
手動執行sqlsession.clearCache()
二級失效
- insert ,update,delete語句的執行
- 超時
- 被回收策略回收
功能實現
添加依賴
<!-- 集成了lettuce的連接方式也可以用jedis方式看自己建議用集成的說明穩定些 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 序列化 -->
<dependency>
<groupId>de.javakaffee</groupId>
<artifactId>kryo-serializers</artifactId>
</dependency>
<!-- 連接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- 自定義獲取Bean -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<!-- 斷言判斷,正式環境中可以使用 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
啓動二級緩存
- 可以配置在調用mapper的項目中,方便以後維護
spring:
redis:
lettuce:
pool:
max-active: 8
max-wait: -1ms
max-idle: 8
min-idle: 0
#集羣的方式
# sentinel:
# master: mymaster
# nodes: 192.168.15.154:6379
database: 0
host: 192.168.15.154
port: 6379
# MyBatis Config properties
mybatis:
type-aliases-package:
mapper-locations: classpath:mapper/*.xml
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
cache-enabled: true
自定義相關代碼
- 確保所有POJO都實現了序列化並聲明瞭序列號
private static final long serialVersionUID =-1L;
- 自定義實現緩存接口
/**
* @author lyy
* @description
* @date 2019/9/2
*/
public class RedisCache implements Cache {
private static final Logger logger = LoggerFactory.getLogger(RedisCache.class);
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
// cache instance id
private final String id;
private RedisTemplate redisTemplate;
// redis過期時間
private static final long EXPIRE_TIME_IN_MINUTES = 30;
public RedisCache(String id) {
if (id == null) {
throw new IllegalArgumentException("Cache instances require an ID");
}
this.id = id;
}
@Override
public String getId() {
return id;
}
/**
* Put query result to redis
*
* @param key
* @param value
*/
@Override
public void putObject(Object key, Object value) {
try {
RedisTemplate redisTemplate = getRedisTemplate();
ValueOperations opsForValue = redisTemplate.opsForValue();
opsForValue.set(key, value, EXPIRE_TIME_IN_MINUTES, TimeUnit.MINUTES);
logger.debug("Put query result to redis");
} catch (Throwable t) {
logger.error("Redis put failed", t);
}
}
/**
* Get cached query result from redis
*
* @param key
* @return
*/
@Override
public Object getObject(Object key) {
try {
RedisTemplate redisTemplate = getRedisTemplate();
ValueOperations opsForValue = redisTemplate.opsForValue();
logger.debug("Get cached query result from redis");
return opsForValue.get(key);
} catch (Throwable t) {
logger.error("Redis get failed, fail over to db", t);
return null;
}
}
/**
* Remove cached query result from redis
*
* @param key
* @return
*/
@Override
@SuppressWarnings("unchecked")
public Object removeObject(Object key) {
try {
RedisTemplate redisTemplate = getRedisTemplate();
redisTemplate.delete(key);
logger.debug("Remove cached query result from redis");
} catch (Throwable t) {
logger.error("Redis remove failed", t);
}
return null;
}
/**
* Clears this cache instance
*/
@Override
public void clear() {
RedisTemplate redisTemplate = getRedisTemplate();
redisTemplate.execute((RedisCallback) connection -> {
connection.flushDb();
return null;
});
logger.debug("Clear all the cached query result from redis");
}
/**
* This method is not used
*
* @return
*/
@Override
public int getSize() {
return 0;
}
@Override
public ReadWriteLock getReadWriteLock() {
return readWriteLock;
}
private RedisTemplate getRedisTemplate() {
if (redisTemplate == null) {
redisTemplate = ApplicationContextHolder.getBean("redisTemplate");
}
return redisTemplate;
}
}
- 定義ApplicationContextHolder來實現Bean的注入和獲取
/**
* @author lyy
* @description
* @date 2019/9/2
*/
@Component
public class ApplicationContextHolder implements ApplicationContextAware, DisposableBean {
private static final Logger logger = LoggerFactory.getLogger(ApplicationContextHolder.class);
private static ApplicationContext applicationContext;
public static ApplicationContext getApplicationContext() {
if (applicationContext == null) {
throw new IllegalStateException(
"'applicationContext' property is null,ApplicationContextHolder not yet init.");
}
return applicationContext;
}
/**
* * 根據bean的id來查找對象
* * @param id
* * @return
*
*/
public static Object getBeanById(String id) {
checkApplicationContext();
return applicationContext.getBean(id);
}
/**
* * 通過名稱獲取bean
* * @param name
* * @return
*
*/
@SuppressWarnings("unchecked")
public static <T> T getBean(String name) {
checkApplicationContext();
Object object = applicationContext.getBean(name);
return (T) object;
}
/**
* * 根據bean的class來查找對象
* * @param c
* * @return
*
*/
@SuppressWarnings("all")
public static <T> T getBeanByClass(Class<T> c) {
checkApplicationContext();
return (T) applicationContext.getBean(c);
}
/**
* * 從靜態變量ApplicationContext中取得Bean, 自動轉型爲所賦值對象的類型.
* 如果有多個Bean符合Class, 取出第一個.
* * @param cluss
* * @return
*
*/
public static <T> T getBean(Class<T> cluss) {
checkApplicationContext();
return (T) applicationContext.getBean(cluss);
}
/**
* * 名稱和所需的類型獲取bean
* * @param name
* * @param cluss
* * @return
*
*/
public static <T> T getBean(String name, Class<T> cluss) {
checkApplicationContext();
return (T) applicationContext.getBean(name, cluss);
}
public static <T> Map<String, T> getBeansOfType(Class<T> type) throws BeansException {
checkApplicationContext();
return applicationContext.getBeansOfType(type);
}
/**
* 檢查ApplicationContext不爲空.
*/
private static void checkApplicationContext() {
if (applicationContext == null) {
throw new IllegalStateException("applicaitonContext未注入,請在applicationContext.xml中定義ApplicationContextHolderGm");
}
}
@Override
public void destroy() throws Exception {
checkApplicationContext();
}
/**
* 清除applicationContext靜態變量
*/
public static void cleanApplicationContext() {
applicationContext = null;
}
@Override
public void setApplicationContext(ApplicationContext context) throws BeansException {
//checkApplicationContext();
applicationContext = context;
logger.info("holded applicationContext,顯示名稱:" + applicationContext.getDisplayName());
}
}
啓用緩存
- 註解的方式,註解在mapper接口上
@CacheNamespace(implementation = RedisCache.class)
- xml的方式
<cache type="***.RedisCache">
<property name="eviction" value="LRU"/>
<property name="flushInterval" value="60000000"/>
<property name="size" value="2048"/>
<property name="readOnly" value="false"/>
</cache>
爲什麼使用redis?除了redis還有其它什麼?
- redis是一個高性能nosql庫,配上集羣穩定高效,因爲默認的二級緩存是在內存中的。這樣可以把緩存獨立出來,也是所有第三方map結構做二級緩存的優點
- 可以更好的適應分佈式
- 還有ehcache,還有理論上的所有nosql庫應該都可以
注意事項
緩存不生效(或者叫緩存穿透)
- 註解的sql使用註解開啓,xml配置的sql要在xml中開啓,混用不生效
- windows版的redis要配置一下允許外網連接,即使把redis.windows.conf中的bind 127.0.0.1註釋掉了也不行
# Examples:
#
# bind 192.168.1.100 10.0.0.1
bind 0.0.0.0
- intellij idea 生成序列號,下面選中後放光標放在類是,不是接口上按alt+enter
File -> Settings -> Inspections -> Serialization issues -> Serialization class without ‘serialVersionUID’