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. java - 对象装载数据返回

    1. 创建 Phone 类 package class_object; public class Phone { String brand; String color; double price; v ...

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

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

  3. JVM内存用量的再学习

    JVM内存用量的再学习 背景 最近解决一个SQLServer的问题耗时很久. 最终找到了一个看似合理的问题解释. 但是感觉不能只是总结于数据库方面 因为为了解决这个问题增加了很多监控措施. 所以想就这 ...

  4. [转帖]058、集群优化之PD

    PD调度基本概念 调度流程 调度中还有这还缺来了merge,例如合并空region. store: 基本信息,容量,剩余空间,读写流量等 region: 范围,副本分布,副本状态,数据量,读写流量等 ...

  5. [转帖]Kafka可靠性之HW与Leader Epoch

    <深入理解Kafka:核心设计与实现原理>是基于2.0.0版本的书 在这本书中,终于看懂了笔者之前提过的几个问题 准备知识 1.leader里存着4个数据:leader_LEO.leade ...

  6. nr_requests 以及 queue_depth的学习与了解

    nr_requests 以及 queue_depth的学习与了解 背景 冯诺依曼的计算机体系结果里面 运算器,存储器是核心. 但是将核心的产生的结果推送出去的其实是IO IO虽然不是像运算器和存储器那 ...

  7. [转帖]linux性能优化-CPU利用率

    参数说明 /proc/stat提供系统的CPU和任务统计信息. user(us): 用户态CPU时间,不包括下面的nice时间,但包括了guest时间. nice(ni): 代表低优先级用户态CPU时 ...

  8. [转帖]从v8到v9,Arm服务器发展之路

    https://zhuanlan.zhihu.com/p/615344155   01 ARM:3A大作 将 CPU 的设计与制造相分离的代工模式,给 AMD 提供了高度的灵活性.第二.三代 EPYC ...

  9. [转帖]OutOfMemoryError内存溢出相关的JVM参数

    原文在这里: OutOfMemoryError内存溢出相关的JVM参数 JVM提供了很多处理内存溢出的相关参数,本文主要来讲解下这些参数,当你遇到内存溢出的时候可能会对你非常有帮助,这些参数主要有: ...

  10. node中的fs模块和http模块的学习

    读取文件 fs 模块 第1个参数就是要读取的文件路径 第2个参数是一个回调函数(error,data)=>{} error 如果读取失败,error 就是错误对象 如果读取成功,error 就是 ...