MyBatis的缓存过期机制, flushInterval参数

在实际测试中, 发现Redis中的缓存数据TTL为-1, 在Hash中的key也无过期时间信息, 怀疑RedisCache的实现是否能正常处理缓存过期, 因此一路追查到了MyBatis的代码.

MyBatis在每个Mapper中, 可以设置参数 flushInterval 用来控制缓存的过期时间, 这个参数, 在 MapperBuilderAssistant 中, 被设置为Cache的clearInternal

  public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
Properties props) {
Cache cache = new CacheBuilder(currentNamespace)
.implementation(valueOrDefault(typeClass, PerpetualCache.class))
.addDecorator(valueOrDefault(evictionClass, LruCache.class))
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build();
configuration.addCache(cache);
currentCache = cache;
return cache;
}

而后在CacheBuilder中, 会根据这个参数, 判断是否生成代理类ScheduledCache

  private Cache setStandardDecorators(Cache cache) {
try {
MetaObject metaCache = SystemMetaObject.forObject(cache);
if (size != null && metaCache.hasSetter("size")) {
metaCache.setValue("size", size);
}
if (clearInterval != null) {
cache = new ScheduledCache(cache);
((ScheduledCache) cache).setClearInterval(clearInterval);
}
if (readWrite) {
cache = new SerializedCache(cache);
}
cache = new LoggingCache(cache);
cache = new SynchronizedCache(cache);
if (blocking) {
cache = new BlockingCache(cache);
}
return cache;
} catch (Exception e) {
throw new CacheException("Error building standard cache decorators. Cause: " + e, e);
}
}

ScheduledCache内部存储了一个变量lastClear, 用来记录最后一次清空缓存的时间, 在get, put, remove等各个操作前, 会判断是否需要清空, 注意是整个namespace的缓存清空.

  private boolean clearWhenStale() {
if (System.currentTimeMillis() - lastClear > clearInterval) {
clear();
return true;
}
return false;
} @Override
public void putObject(Object key, Object object) {
clearWhenStale();
delegate.putObject(key, object);
} @Override
public Object getObject(Object key) {
return clearWhenStale() ? null : delegate.getObject(key);
}

由此可以看出, MyBatis的缓存过期管理机制还是比较粗糙的, 并且依赖本地实例中记录的时间, 同样的LRU机制也是依赖本地.

在分布系统中使用MyBatis如果使用Redis作为缓存, 需要注意这个问题,

1. 默认情况下, Redis中的缓存时间为-1永不过期, 根据各个实例中的计时进行过期清除, 在节点数超过2的情况下, 建议关闭mapper中的flushInterval

2. 如果使用Github中beta3版本的代码, 那么可以在mapper中增加一个timeout来设置Redis key的过期时间, 这个可以在flushInterval关闭的情况下, 通过redis自身进行缓存的过期清理, 但是这个过期时间对应的是一个namespace, 意味着每隔一段时间, 这整个namespace中的缓存全部失效, 哪怕这个查询结果一秒前刚刚被缓存, 这一秒就被清空了.

3. 缓存的主动失效由insert, update, delete发起, 这个在分布式环境下的触发依然是有效的. 但是对于通过多表join得到的结果, 如果未共享namespace, 容易出现缓存未更新而拿到旧数据的情况. 建议在sql编写中通过1+N形式完成复杂查询, 尽量不用使用join

针对Redis缓存的优化方案

1. 关闭 MyBatis 的 flushInterval , 避免各个节点互相干扰的问题, 将缓存的过期控制交给Redis管理

2. 对每一个缓存结果, 在序列化和反序列化时增加一个时间戳, 在读取缓存的时候判断是否过期, 如果过期就返回空(等同于缓存未命中或缓存失效), 将过期时间粒度细化到单个结果.

3. 实现了2之后可以关闭key的timeout, 仅由增删改来触发整个key的清理.

MyBatis RedisCache

http://mybatis.org/redis-cache/

https://github.com/mybatis/redis-cache

这是MyBatis官方的二级缓存的Redis实现, 不支持Redis Cluster, 因为其依赖于Jedis和固定的redis.properties, 和Spring Boot集成较为麻烦, 在Spring Boot 2.1.x中使用还会报RedisConfig初始化错误.

https://github.com/MiltonLai/redis-cache

魔改后的版本, 支持Cluster, 并且支持单个查询结果的过期控制, 未经高强度验证, 请谨慎使用.

使其正常运行

首先不要用pom的jar包引入, 直接到github项目地址上下载源代码, 需要的只是 src/main/java/org/mybatis/caches/redis/ 目录下的文件, 将其放到自己的项目里.

其次, 现在的源码中, 对redis.properties要求其中各项配置名称要以redis.为前缀, 和jar包引用时的要求不一样.

这样基本就能启动运行了

在beta3之后增加了timeout参数, 可以通过redis自身的ttl设置缓存失效时间, 在mapper中的配置方式为

XML

<cache type="org.mybatis.caches.redis.RedisCache">
<property name="timeout" value="3" />
</cache>

Annotation

@CacheNamespace(properties = { @Property(name = "timeout", value = "3") })

如果需要支持Redis Cluster, 可以使用这个版本

Spring Boot中的配置

在Spring Boot中, 也可以通过redis.properties配置.

使用RedisTemplate

如果希望使用SpringBoot的RedisTemplate, 可以加上一个静态引用, 例如

/**
* Cons:
* 1. Memory issues: if you redeploy the WAR without restarting the VM, you end up with 2 application contexts in the
* same VM: the one attached to the static field of ApplicationContextHolder and the new one that is stored in the
* ServletContext. This is just the same issue as the commons-logging memory issue.
* 2. Tests: if you use spring tests, you will have multiple application contexts in the same VM when running a suite,
* but only the one loaded from the first test is stored in the static field.
* 3. Application context hierarchy: It is quite common to have a "services application context" and a "web application
* context" (and a DispatcherServlet application context), each one being a child of the previous one. Only the root
* (services) application context will be stored in the static variable, and thus you have a lot of beans that are not
* accessible.
*
* Though, it's safe to use this in a java -jar application.
*/
@Component
public class ApplicationContextHolder implements ApplicationContextAware { private static ApplicationContext context; /**
* Returns the Spring managed bean instance of the given class type if it exists.
* Returns null otherwise.
*/
public static <T> T getBean(Class<T> beanClass) {
return context.getBean(beanClass);
} @SuppressWarnings("unchecked")
public static <T> T getBean(String name) {
return (T) context.getBean(name);
} @Override
public void setApplicationContext(ApplicationContext context) throws BeansException {
// store ApplicationContext reference to access required beans later on
synchronized (this) {
if (ApplicationContextHolder.context == null) {
ApplicationContextHolder.context = context;
}
}
}
}

这时候需要自己重写RedisCache.java, 在方法中引用RedisTemplate. 因为在mapper初始化的时候给redisTemplate赋值有可能会失败, 所以使用getRedisTemplate()方法, 在调用时再赋值.

代码参考 https://programmer.help/blogs/spring-boot-mybatis-redis-secondary-cache.html, 这个实现是使用整个db存kv实现的, 跟mybatis redis的实现(用hash)不一样, 这个的好处是自带过期时间, 但是在namespace缓存清空时, 会影响所有的namespace. 如果要在正式环境使用, 需要改一下.

private RedisTemplate redisTemplate;

private RedisTemplate getRedisTemplate() {
if (redisTemplate == null) {
redisTemplate = ApplicationContextHolder.getBean("redisTemplate");
}
return redisTemplate;
}
...
public void putObject(Object key, Object value) {
ValueOperations opsForValue = getRedisTemplate().opsForValue();
opsForValue.set(key, value, timeout, TimeUnit.SECONDS);
}
...

将过期控制应用到单个查询结果

在序列化/反序列化中增加时间戳

public byte[] serialize(long timestamp, Object object) {
try (
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(object);
baos.write(longToBytes(timestamp));
return baos.toByteArray();
} catch (Exception e) {
throw new CacheException(e);
}
} public long getTimestamp(byte[] bytes) {
if (bytes == null || bytes.length < 8) {
return -1;
}
byte[] copy = new byte[8];
System.arraycopy(bytes, bytes.length - 8, copy, 0, 8);
return bytesToLong(copy);
} public Object unserialize(byte[] bytes) {
if (bytes == null || bytes.length < 8) {
return null;
}
try (ByteArrayInputStream bais = new ByteArrayInputStream(Arrays.copyOf(bytes, bytes.length - 8));
ObjectInputStream ois = new ObjectInputStream(bais)) {
return ois.readObject();
} catch (Exception e) {
throw new CacheException(e);
}
}

在缓存读写时增加过期判断

@Override
public void putObject(final Object key, final Object value) {
final byte[] idBytes = id.getBytes();
long ts = 0;
if (timeout != null) {
ts = System.currentTimeMillis() + timeout * 1000;
}
final byte[] objBytes = redisConfig.getSerializer().serialize(ts, value);
client.hset(idBytes, key.toString().getBytes(), objBytes);
} @Override
public Object getObject(final Object key) {
byte[] objBytes = client.hget(id.getBytes(), key.toString().getBytes());
if (objBytes == null || objBytes.length < 8) return null;
long ts = redisConfig.getSerializer().getTimestamp(objBytes);
if (ts > 0 && ts < System.currentTimeMillis()) {
client.hdel(id, key.toString());
return null;
} else {
return redisConfig.getSerializer().unserialize(objBytes);
}
}

Mybatis的缓存过期机制和RedisCache的更多相关文章

  1. mysql开启缓存、设置缓存大小、缓存过期机制

    目录 一.开启缓存 1.修改配置文件my.ini 2.命令方式 二.查看是否生效 1.query_cache_type 使用查询缓存的方式 2.have_query_cache 设置查询缓存是否可用 ...

  2. mysql系列三、mysql开启缓存、设置缓存大小、缓存过期机制

    一.开启缓存 mysql 开启查询缓存可以有两种方法来开启一种是使用set命令来进行开启,另一种是直接修改my.ini文件来直接设置都是非常的简单的哦. 开启缓存,设置缓存大小,具体实施如下: 1.修 ...

  3. mybatis(四)缓存机制

    转载:https://www.cnblogs.com/wuzhenzhao/p/11103043.html 缓存是一般的ORM 框架都会提供的功能,目的就是提升查询的效率和减少数据库的压力.跟Hibe ...

  4. Mybatis缓存处理机制

    一.MyBatis缓存介绍 正如大多数持久层框架一样,MyBatis 同样提供了一级缓存和二级缓存的支持 一级缓存: 基于PerpetualCache 的 HashMap本地缓存,其存储作用域为 Se ...

  5. Redis 利用锁机制来防止缓存过期产生的惊群现象-转载自 http://my.oschina.net/u/1156660/blog/360552

    首先,所谓的缓存过期引起的“惊群”现象是指,在大并发情况下,我们通常会用缓存来给数据库分压,但是会有这么一种情况发生,那就是在一定时间 内生成大量的缓存,然后当缓存到期之后又有大量的缓存失效,导致后端 ...

  6. 深入浅出mybatis之缓存机制

    目录 前言 准备工作 MyBatis默认缓存设置 缓存实现原理分析 参数localCacheScope控制的缓存策略 参数cacheEnabled控制的缓存策略 总结 前言 提到缓存,我们都会不约而同 ...

  7. mybatis的缓存机制及用例介绍

    在实际的项目开发中,通常对数据库的查询性能要求很高,而mybatis提供了查询缓存来缓存数据,从而达到提高查询性能的要求. mybatis的查询缓存分为一级缓存和二级缓存,一级缓存是SqlSessio ...

  8. MyBatis框架——缓存机制

    使⽤缓存机制的作⽤也是减少 Java 应⽤程序与数据库的交互次数,从⽽提升程序的运⾏效率. ⽐如第 ⼀次查询出某个对象之后,MyBatis 会⾃动将其存⼊缓存,当下⼀次查询同⼀个对象时,就可以直接从 ...

  9. mybatis的缓存机制(一级缓存二级缓存和刷新缓存)和mybatis整合ehcache

    1.1  什么是查询缓存 mybatis提供查询缓存,用于减轻数据压力,提高数据库性能. mybaits提供一级缓存,和二级缓存. 一级缓存是SqlSession级别的缓存.在操作数据库时需要构造 s ...

  10. MyBatis一级缓存(转载)

    <深入理解mybatis原理> MyBatis的一级缓存实现详解 及使用注意事项 http://demo.netfoucs.com/luanlouis/article/details/41 ...

随机推荐

  1. 百度网盘(百度云)SVIP超级会员共享账号每日更新(2023.12.15)

    一.百度网盘SVIP超级会员共享账号 可能很多人不懂这个共享账号是什么意思,小编在这里给大家做一下解答. 我们多知道百度网盘很大的用处就是类似U盘,不同的人把文件上传到百度网盘,别人可以直接下载,避免 ...

  2. [转帖]三篇文章了解 TiDB 技术内幕 - 谈调度

    返回全部 申砾产品技术解读2017-06-06 为什么要进行调度 先回忆一下 三篇文章了解 TiDB 技术内幕 - 说存储提到的一些信息,TiKV 集群是 TiDB 数据库的分布式 KV 存储引擎,数 ...

  3. [转帖]elasticsearch-create-enrollment-tokenedit

    https://www.elastic.co/guide/en/elasticsearch/reference/current/create-enrollment-token.html The ela ...

  4. [转帖]018 磁盘 IO 性能监控 / 压测工具 (sar、iotop、fio、iostat)

    https://my.oschina.net/u/3113381/blog/5465063   1 sar 命令查看当前磁盘 IO 读写 sar(System Activity Reporter 系统 ...

  5. [转帖]写给想了解"集成电路"的朋友

    https://zhuanlan.zhihu.com/p/602627000 寒假和朋友小聚,每当就专业问题展开谈话,很容易形成"一边热火朝天,一边大脑宕机"的局面.俗话说隔行如隔 ...

  6. mysql系列14---mysql数据库还原与备份

    一.Liunx服务器下数据库定时备份 1.编写mysql在docker容器中备份的shell脚本: #!/bin/bash#  2020-11-15#docker启动的mysql备份mysql_use ...

  7. OpenIM Open Source Instant Messaging Project Docker Compose Deployment Guide

    The deployment of OpenIM involves multiple components and supports various methods including source ...

  8. java8新特性知识整理

    目录 前言 Lambda 表达式 方法引用 函数式接口 Stream 流 构造流的几种方式 常用 api Collectors.toMap (List 转 Map) peek 和 map 区别 gro ...

  9. C++ CryptoPP使用AES加解密

    Crypto++ (CryptoPP) 是一个用于密码学和加密的 C++ 库.它是一个开源项目,提供了大量的密码学算法和功能,包括对称加密.非对称加密.哈希函数.消息认证码 (MAC).数字签名等.C ...

  10. 从嘉手札<2023-11-13>

    1. 很多时候 成功并不等同于成长 成功是很多因素复合形成的一种结果 而并不等同于一个人阅历的丰富.认知的提高 2. 我一直认为 世界不属于投机者 也不属于堕落者 信念感在这个大数据泛滥.碎片化汹涌的 ...