Mybatis自定义分布式二级缓存实现与遇到的一些问题解决方案!
先说两句:
我们都知道Mybatis缓存分两类: 一级缓存(同一个Session会话内) & 二级缓存(基于HashMap实现的以 namespace为范围的缓存)
今天呢, 我们不谈一级缓存, 我们来谈一谈 二级缓存, 通过查看Mybatis源码发现, 他的二级缓存实现真的十分简单, 默认的实现类是 org.apache.ibatis.cache.impl.PerpetualCache 这里贴一下他的源码吧:
/**
* Copyright 2009-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.ibatis.cache.impl; import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock; import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.CacheException; /**
* @author Clinton Begin
*/
public class PerpetualCache implements Cache { private String id; private Map<Object, Object> cache = new HashMap<Object, Object>(); public PerpetualCache(String id) {
this.id = id;
} @Override
public String getId() {
return id;
} @Override
public int getSize() {
return cache.size();
} @Override
public void putObject(Object key, Object value) {
cache.put(key, value);
} @Override
public Object getObject(Object key) {
return cache.get(key);
} @Override
public Object removeObject(Object key) {
return cache.remove(key);
} @Override
public void clear() {
cache.clear();
} @Override
public ReadWriteLock getReadWriteLock() {
return null;
} @Override
public boolean equals(Object o) {
if (getId() == null) {
throw new CacheException("Cache instances require an ID.");
}
if (this == o) {
return true;
}
if (!(o instanceof Cache)) {
return false;
} Cache otherCache = (Cache) o;
return getId().equals(otherCache.getId());
} @Override
public int hashCode() {
if (getId() == null) {
throw new CacheException("Cache instances require an ID.");
}
return getId().hashCode();
} }
PerpetualCache.java
那么既然他都已经有一个实现了, 我们为什么还要自定义实现呢?
原因很简单, 默认实现是(HashMap)本地缓存, 不支持分布式缓存, 而我们现在大多数项目都是以集群的方式部署, 这种情况下, 使用本地缓存会出现很严重的脏读问题, 特定情况下更新可直接导致数据不一致的问题.
如果让缓存实现支持分布式呢? 方案有很多, 基本围绕着NoSQL数据库实现, 最常用的就是Redis了, 接下来我们就来用Redis实现自定义缓存类
说干就干吧, 开始我们的自定义缓存实现之路
实现过程:
首先: Mybatis自定义实现缓存类的配置方式十分简单, 我们只需要开启二级缓存, 并在 mapper.xml 中修改如下配置:

其中 type 属性值就是我们自定义的缓存实现辣!
接下来我们来看一看实现类内部是怎么写的:
package com.cardgame.demo.game.component.db; import com.cardgame.demo.component.redis.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.cache.Cache; import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock; import static com.cardgame.demo.game.component.core.config.Constants.REDIS_KEY_GAME; /**
* @author yjy
* 2018-08-06 15:46
*/
@Slf4j
public class MybatisRedisCache implements Cache { private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); private String id; public MybatisRedisCache(final String id) {
if (id == null) {
throw new IllegalArgumentException("id can not be null");
}
log.debug("MyBatisRedisCache:id=" + id);
this.id = id;
} @Override
public String getId() {
return this.id;
} @Override
public int getSize() {
int size = (int) RedisUtils.getInstance().getHashSizeObj(getKey());
log.debug("MybatisCache getSize > {}", size);
return size;
} @Override
public void putObject(Object key, Object value) {
RedisUtils.getInstance().setHashObj(getKey(), key, value);
log.debug("MybatisCache put > key: {}, val: {}", key, value);
}
@Override
public Object getObject(Object key) {
Object val = RedisUtils.getInstance().getHashObj(getKey(), key);
log.debug("MybatisCache get > key: {}, val: {}", key, val);
return val;
} @Override
public Object removeObject(Object key) {
// 移除指定缓存
Object obj = RedisUtils.getInstance().getHashObj(getKey(), key);
RedisUtils.getInstance().delHashObj(getKey(), key);
log.debug("MybatisCache remove > key: {}, val: {}", key, obj);
return obj;
} @Override
public void clear() {
// 清除所有缓存
RedisUtils.getInstance().delObj(getKey());
log.debug("MybatisCache clear > hashKey : {}", getKey());
} @Override
public ReadWriteLock getReadWriteLock() {
return readWriteLock;
} protected String getKey() {
return REDIS_KEY_GAME + "mybatis_cache_" + id;
} }
MybatisRedisCache
自定义缓存实现的要求很低, 只需要 实现 org.apache.ibatis.cache.Cache 就可以了. 这里我们用到了 RedisUtils 工具类, 还请同学们 对应到自己的项目中的工具类.
至此: 我们的Mybatis二级缓存就支持分布式啦!
问题来临:
今天我偶然想起, 是否需要给二级缓存 设置一个过期时间?
我们来想一下不设置过期时间会有什么问题:
当有一天我们不得不手动修改数据库的数据时(别问为啥要动数据库, 因为我在本地测试需要修改), 如果相应的 namespace 没有 插入和更新操作, 那么他的缓存将一直有有效, 然后查出来的数据一直是修改前的数据, 而且我们使用的Redis做的缓存, 即使重启了系统缓存依然还是在, 只能从redis中找到指定缓存并清除, 实在是头疼
有没有办法解决呢?
你可能已经想到了, cache标签不是支持 flushInterval 属性的吗? 设置一个 flushInterval = "10000", 这样不就行了吗?
我开始也是这么认为的, 直到有一天我发现这个设置完全没有起作用, 缓存一直是有效的, 那问题就来了, 为什么呢?
百思不得其解的我决定去瞄一瞄Mybatis的源码, 最终让我发现了其中的奥秘, 我们来看一下下面的代码:

这是Mybatis初始化二级缓存中的一段代码, 我们可以看到, flushInterval 属性对于自定义实现类是不起作用的, 而 Mybatis 实现的缓存过期时间的原理则是利用 设计模式(装饰器模式) 在默认的 缓存实现类上封装了一层 ScheduleCache, 也正是此类实现了缓存的有效期设置.
完了完了, 那不能设置咋办啊, 这不是坑爹吗?
冷静下来, 先别忙. 既然 不能通过设置解决, 那我们就自己想办法解决吧
既然你Mybatis不帮我封装 ScheduleCache, 那我就自己封装一个 ScheduleRedisCache, 原有的MybatisRedisCache不变. 我们看一下 ScheduleRedisCache的代码:
package com.cardgame.demo.game.component.db; import com.cardgame.demo.component.redis.RedisUtils;
import org.apache.ibatis.cache.Cache; import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock; import static com.cardgame.demo.game.component.core.config.Constants.REDIS_KEY_GAME; /**
*
* 二级缓存 过期时间 实现
*
* @author yjy
* 2018-08-13 13:21
*/
public class ScheduleRedisCache implements Cache { private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); private String id;
private long flushInterval = 10000; // 缓存刷新时间, 单位毫秒
private Cache delegate; // 委派缓存类 public ScheduleRedisCache(String id) {
if (id == null) {
throw new IllegalArgumentException("id can not be null");
}
this.id = id;
this.delegate = new MybatisRedisCache(id);
} @Override
public String getId() {
return id;
} @Override
public void putObject(Object key, Object value) {
// 记录过期时间
long timeout = System.currentTimeMillis() + flushInterval;
RedisUtils.getInstance().setHashObj(getKey(), key, timeout);
delegate.putObject(key, value);
} @Override
public Object getObject(Object key) {
Object timeout = RedisUtils.getInstance().getHashObj(getKey(), key);
// if 未过期
if (timeout != null && (long) timeout > System.currentTimeMillis()) {
// 更新过期时间
RedisUtils.getInstance().setHashObj(getKey(), key, System.currentTimeMillis() + flushInterval);
return delegate.getObject(key);
}
return null;
} @Override
public Object removeObject(Object key) {
RedisUtils.getInstance().delHashObj(getKey(), key);
return delegate.removeObject(key);
} @Override
public void clear() {
RedisUtils.getInstance().delObj(getKey());
delegate.clear();
} @Override
public int getSize() {
return (int) RedisUtils.getInstance().getHashSizeObj(getKey());
} @Override
public ReadWriteLock getReadWriteLock() {
return readWriteLock;
} protected String getKey() {
return REDIS_KEY_GAME + "schedule_cache_" + id;
} }
ScheduleRedisCache.java
代码简单易懂, 说白了就是增加一个 缓存管理, 专门 缓存二级缓存的 过期时间. 在适当的时候 更新过期时间 / 清除过期时间
需要注意的是, 既然我们封装了 MybatisRedisCache, 那么 mapper.xml 中就需要 改一改了, 如下:

这样, 我们就实现了 二级缓存的超时自动过期功能了!!!
结论: 谢谢大家!666
Mybatis自定义分布式二级缓存实现与遇到的一些问题解决方案!的更多相关文章
- Mybatis使用Redis二级缓存
在Mybatis中允许开发者自定义自己的缓存,本文将使用Redis作为Mybatis的二级缓存.在Mybatis中定义二级缓存,需要如下配置: 1. MyBatis支持二级缓存的总开关:全局配置变量参 ...
- 【MyBatis学习13】MyBatis中的二级缓存
1. 二级缓存的原理 前面介绍了,mybatis中的二级缓存是mapper级别的缓存,值得注意的是,不同的mapper都有一个二级缓存,也就是说,不同的mapper之间的二级缓存是互不影响的.为了更加 ...
- 分布式二级缓存组件实战(Redis+Caffeine实现)
前言 在生产中已有实践,本组件仅做个人学习交流分享使用.github:https://github.com/axinSoochow/redis-caffeine-cache-starter 个人水平有 ...
- Mybatis一级、二级缓存
Mybatis一级.二级缓存 一级缓存 首先做一个测试,创建一个mapper配置文件和mapper接口,我这里用了最简单的查询来演示. <mapper namespace="c ...
- Spring + MySQL + Mybatis + Redis【二级缓存】
一.Redis环境 Redis 官网 :http://redis.io/ windows下载:https://github.com/dmajkic/redis/downloads 1.文件解压缩 2. ...
- mybatis整合redis二级缓存
mybatis默认开启了二级缓存功能,在mybatis主配置文件中,将cacheEnabled设置成false,则会关闭二级缓存功能 <settings> <!--二级缓存默认开启, ...
- MyBatis 一、二级缓存和自定义缓存
1.一级缓存 MyBatis 默认开启了一级缓存,一级缓存是在SqlSession 层面进行缓存的.即,同一个SqlSession ,多次调用同一个Mapper和同一个方法的同一个参数,只会进行一 ...
- (转)MyBatis 一、二级缓存和自定义缓存
1.一级缓存 MyBatis 默认开启了一级缓存,一级缓存是在SqlSession 层面进行缓存的.即,同一个SqlSession ,多次调用同一个Mapper和同一个方法的同一个参数, 只会进行一次 ...
- MyBatis笔记——EhCache二级缓存
介绍 ehcache是一个分布式缓存框架. 我们系统为了提高系统并发,性能.一般对系统进行分布式部署(集群部署方式) 不使用分布缓存,缓存的数据在各各服务单独存储,不方便系统开发.所以要使用分布式缓 ...
随机推荐
- Go语言中的make和new
相同点: make和new都是用来创建分配类型内存的. 不同点: 先看下面的代码 package main import "fmt" func main(){ var i *int ...
- KDJ计算公式
计算方法编辑KDJ的计算比较复杂,首先要计算周期(n日.n周等)的RSV值,即未成熟随机指标值,然后再计算K值.D值.J值等.以n日KDJ数值的计算为例,其计算公式为n日RSV=(Cn-Ln)/(Hn ...
- nginx 配置详解(转)
nginx概述 nginx是一款自由的.开源的.高性能的HTTP服务器和反向代理服务器:同时也是一个IMAP.POP3.SMTP代理服务器:nginx可以作为一个HTTP服务器进行网站的发布处理,另外 ...
- logstash配置多入多出并互相隔离
需求:需要利用同一logstash进程采集不同日志,输出到es的不同index,各输入输出隔离: 主要需要解决如下两个问题: 1.如何加载多个配置文件? 普通启动方式:nohup bin/logsta ...
- idea中如何添加RunDashboard
在微服务开发中,往往要同时启动多个服务,这时候使用Run控制台难免会出错,并且不方便管理,这里推荐一个功能Run Dashboard idea中打开Run Dashboard的方法如下 view &g ...
- ArcGis Python脚本——将细碎小面合并到相邻的面积最大的面
参数: polygon_fc 面要素类 area_limit 给定面积值,小于它的面会被合并 给定两个参数即可,这回没有注释. #polygon_fc 面要素类 #area_limit 给定面积值,小 ...
- Python——使用高德API获取指定城指定类别POI并实现XLSX文件合并
# 以下内容为原创,转载请注明出处 1 import xlrd # 读xlsx import xlsxwriter # 写xlsx import urllib.request # url请求,Pyth ...
- ASP.NET-SHA256加密方法
/// <summary> /// SHA256加密 /// </summary> /// <param name="strData">< ...
- 毕业设计——Django表单重复提交问题
1. 先记录一些正常情况:在填写表单页面(如注册.登录页面)时刷新页面时数据清空是正常的,此时就是重新发送了一个页面请求. 2. 问题一:用户登录后,刷新登陆后的页面(如首页)时会出现表单重复提交问题 ...
- luogu P5302 [GXOI/GZOI2019]特技飞行
传送门 强行二合一可还行 首先\(c\)的贡献是不会变的,先考虑求出多少交点被矩形覆盖,交点的话可以按左端点纵坐标从下到上顺序枚举一条线段,然后维护右端点纵坐标的set,把之前处理过线段的右端点放进s ...