如何实现iBatis分布式缓存?,Redis缓存怎么实现
- 前端开发
- 2026-08-09
- 11
iBatis(MyBatis前身)的分布式缓存,最务实的做法就是把Redis作为二级缓存容器,让所有应用实例共享同一份数据,从根本上解决本地缓存不一致的问题。
为什么iBatis自带的二级缓存撑不住分布式场景?
传统iBatis的二级缓存默认是PerpetualCache,说白了就是每个应用节点自己内存里存一份,单机部署时挺省事,但一旦上了多实例,问题就来了:每个节点的缓存各自为政,A节点更新了数据,B节点还在用旧值,多数团队早期会用“清理全部缓存”这种粗暴方式应对,但缓存命中率直线下降,DB压力反而更大。
业内专家的共识是:分布式环境下,缓存必须从“本地内存”升级为“共享存储”,而Redis凭借高性能、持久化、原子操作等特性,成为iBatis分布式缓存的首选搭档,相比Memcached,Redis支持更丰富的数据结构,而且能通过AOF+RDB组合保证缓存数据可恢复,这对于电商、金融等对一致性敏感的业务尤为重要。
ibatis redis二级缓存配置实战
第一步:引入Redis客户端依赖
项目里需要同时有iBatis(或MyBatis)和Redis客户端,以常见的MyBatis 3.x + Jedis为例,在pom.xml里加:
<dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.x</version> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>4.3.x</version> </dependency>
如果用的是Spring Boot,直接引入spring-boot-starter-data-redis更省事,Template的序列化策略要自己配好,避免默认的JDK序列化导致Key可读性差。
第二步:实现MyBatis的Cache接口
MyBatis的二级缓存机制允许自定义org.apache.ibatis.cache.Cache实现,核心逻辑就是读写Redis,用namespace作为Key前缀,下面是一个精简版实现思路:
- putObject(Object key, Object value):将结果序列化后写入Redis,建议设置TTL,比如30分钟。
- getObject(Object key):从Redis读取并反序列化。
- removeObject(Object key):删除单个键。
- clear():删除该namespace下所有缓存键。
- getSize():返回当前缓存键数量,可借助Redis的SCARD或DBSIZE实现。
关键点在于Key的粒度,MyBatis传给Cache的key是SQL语句+参数拼接后的对象,直接toString可能又长又乱,更稳妥的做法是自己生成哈希Key,比如namespace:sqlId:paramsHash,这样Redis里的键可读、可控、好排查。
第三步:在Mapper中启用自定义缓存
在Mapper XML文件的<mapper>标签下加一行:
<cache type="com.example.MyRedisCache" eviction="FIFO" flushInterval="1800000" readOnly="false"/>
注意flushInterval设为30分钟,就是让Redis里的缓存自动过期,相当于是兜底策略。readOnly建议设为false,这样MyBatis会做对象拷贝,避免多个线程拿到同一份引用互相踩踏。
第四步:序列化方案选型
Redis存储的都是字节数组,所以对象序列化方式决定了缓存性能和兼容性,行业里常用三种:
- JDK原生序列化:简单但体积大,且对象类必须实现Serializable。
- Jackson/Fastjson:JSON格式,可读性好,但反序列化时需要类型信息。
- Protobuf/Kryo:体积小、速度快,但需要额外生成schema或注册类。
我的建议是:内部系统用Jackson就够了,性能敏感场景用Kryo,注意反序列化时要用TypeReference,避免List


缓存一致性:Redis方案如何避免脏数据
更新策略:先写DB,再删缓存
iBatis的update操作会触发clear()方法,如果直接删除整个namespace的缓存,实现简单但浪费严重,更好的做法是精确失效:在update的SQL中,通过flushCache="true"让MyBatis只清除受影响的缓存项,但MyBatis默认做不到精确到行,只能全清。
所以实际项目里,常见的折中方案是:
- 读多写少的配置类数据,缓存TTL设长一点,比如2小时。
- 频繁更新的业务数据,TTL设短一点,比如5分钟,并接受短时不一致。
- 关键交易数据,直接不用二级缓存,或者只在只读查询上使用。
和本地缓存配合:两级缓存模式
有些团队为了性能,在Redis前面再放一层Caffeine本地缓存,这种模式要特别注意广播失效,比如用Redis的Pub/Sub,当某个节点更新数据时,发一条消息让其他节点清掉本地缓存,如果没有这个机制,本地缓存会重新变成“脏数据源头”。
Redis集群的高可用影响
缓存服务挂了,数据库还能扛得住吗?这取决于你的Redis部署模式,单机Redis一旦宕机,所有请求打向DB,很多系统就是这么被压垮的,所以生产环境至少要上主从+哨兵模式,数据量大的用Redis Cluster,据行业共识,Redis Cluster在节点故障时能做到秒级切换,对业务基本无感。
常见的坑与排查方法
缓存穿透、击穿、雪崩
这三个问题在iBatis+Redis场景下同样存在:

- 穿透:查询一个不存在的ID,每次都打到DB,解决办法是缓存空值,TTL设短一些,比如60秒。
- 击穿:某个热点key过期瞬间,大量请求同时打到DB,用互斥锁(如Redis的SETNX)只让一个线程去查库,其余线程等待。
- 雪崩:大量key同时失效,解决办法是给TTL加随机偏移量,比如30分钟±5分钟。
Redis连接池耗尽
如果每个Mapper查询都新建Jedis连接,高并发下连接池会瞬间被打满,务必使用连接池,并设置合理的maxTotal和maxIdle,在Cache实现里要处理连接异常,比如JedisException时直接返回null,让请求落到DB,绝不因为缓存故障导致业务报错。
序列化版本冲突
部署新版本时,如果对象结构变了,老缓存反序列化会报错,解决办法是:
- 在Cache实现里catch掉反序列化异常,直接返回null。
- 发布时主动清一次Redis中相关namespace的键。
ibatis分布式缓存方案对比:Redis vs 其他
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Redis | 数据结构丰富、持久化、高可用方案成熟 | 需要额外维护缓存服务 | 绝大多数分布式系统 |
| Memcached | 纯内存、读性能极高 | 不支持持久化、容量受内存限制 | 纯缓存、可接受数据丢失 |
| Hazelcast/Ignite | 内嵌于应用、免独立部署 | 与语言绑定、集群规模受限 | 小规模集群、不想引入外部依赖 |
| 数据库做缓存 | 不用额外组件 | 性能差、增加DB负担 | 基本不推荐 |
从运维成本看,Redis虽然要单独部署,但云厂商提供的Redis服务价格已经比较透明,如果是自建,Redis做分布式缓存多少钱主要看内存规格和副本数,2核4G起步的实例足够支撑中小业务,用云数据库Redis,按量付费模式对初创团队更友好,这个成本换来的是缓存层面的高可用和弹性扩展,值得的。
Q&A:关于iBatis分布式缓存的常见疑问
iBatis的二级缓存和Redis缓存有什么区别?
iBatis的二级缓存是框架层面的缓存机制,默认实现是JVM本地缓存,作用范围仅限单个应用实例,Redis缓存是独立的分布式存储服务,多个应用实例可以共享访问,将Redis作为iBatis二级缓存的存储介质后,既能享受框架的自动缓存管理能力,又能获得分布式环境下的数据一致性。
使用Redis做二级缓存后,MyBatis一级缓存还有必要开吗?
一级缓存是SqlSession级别的,默认开启,作用范围仅限一次会话内的多次查询,分布式场景下,一级缓存不会造成跨节点数据不一致,可以保留,但注意,如果同一SqlSession长期不关闭,且中间有更新操作,可能查到旧数据,建议在Spring管理下每次请求使用新的SqlSession,避免长会话。
缓存和数据库数据不一致时,怎么快速定位?
先在Redis里查看对应namespace的key是否存在,用redis-cli执行KEYS com.example.mapper.能列出所有相关缓存键,如果key存在且值明显是旧版本,说明clear()没有被触发,检查Mapper XML中<update>标签是否设置了flushCache="true",或者自定义Cache的removeObject方法是否逻辑正确,最后再看TTL,如果TTL很长,手动删除该key后观察是否被重新加载。
最后说一句
iBatis整合Redis做分布式缓存,本质上是把“缓存存储”和“缓存策略”解耦,框架管好命中和失效逻辑,Redis管好数据存储和共享,只要序列化、失效策略、高可用这三点做到位,这套方案能稳定支撑从创业期到上市期的业务增长。