在分布式环境下使用 MyBatis 二级缓存,核心挑战是解决多节点缓存一致性问题。单机环境中,二级缓存是内存级别的本地缓存,而分布式环境下多节点独立部署,本地缓存无法跨节点共享,易导致 "缓存孤岛" 和数据不一致。本文从底层原理出发,提供一套完整的分布式二级缓存解决方案,包含实战配置与最佳实践。
一、分布式环境下二级缓存的核心问题
在分布式架构(如微服务集群)中,默认的 MyBatis 二级缓存(本地内存缓存)会暴露三个致命问题:
- 缓存孤岛:每个节点维护独立缓存,同一查询在不同节点可能命中不同缓存数据(如节点 A 更新数据后,节点 B 的缓存仍是旧值)。
- 数据不一致:跨节点更新数据时,无法通知其他节点同步清空缓存,导致部分节点返回脏数据。
- 序列化风险:本地缓存可直接存储 Java 对象引用,而分布式缓存需网络传输,若对象未序列化会导致缓存失败。
二、解决方案:基于集中式缓存的二级缓存改造
分布式环境下的核心解决方案是:用集中式缓存(如 Redis、Memcached)替代本地内存缓存,让所有节点共享同一缓存源,实现缓存数据全局一致。
2.1 技术选型:MyBatis + Redis(最常用组合)
Redis 作为高性能的分布式缓存中间件,支持数据持久化、过期策略和集群模式,是 MyBatis 二级缓存的理想选择。实现思路是:
- 让 MyBatis 的二级缓存数据存储到 Redis,而非本地内存。
- 所有节点通过 Redis 访问缓存,确保缓存数据全局唯一。
三、实战:MyBatis 集成 Redis 实现分布式二级缓存
3.1 环境准备
- JDK 17
- MyBatis 3.5.10+
- Redis 6.2+
- Spring Boot 2.7.x(简化配置)
3.2 依赖配置(Maven)
<!-- MyBatis核心依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.0</version>
</dependency>
<!-- Redis缓存依赖(MyBatis官方适配) -->
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>1.0.0-beta2</version>
</dependency>
<!-- Redis客户端 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
3.3 配置 Redis 连接
在src/main/resources
下创建redis.properties
,配置 Redis 连接信息:
# Redis服务器地址
redis.host=192.168.1.100
# Redis端口
redis.port=6379
# 连接超时时间(毫秒)
redis.timeout=2000
# Redis密码(无密码则留空)
redis.password=your_redis_password
# 数据库索引(默认0)
redis.database=1
# 缓存默认过期时间(毫秒,30分钟)
redis.default.expiration=1800000
3.4 改造实体类:实现序列化
分布式缓存中,对象需在网络中传输,必须实现Serializable
接口,否则会导致缓存失败。
User.java
package com.example.entity;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 用户实体类(必须实现Serializable)
*/
@Data
public class User implements Serializable {
// 序列化版本号(避免反序列化冲突)
private static final long serialVersionUID = 1L;
private Long id;
private String username;
private String email;
private LocalDateTime createTime;
}
3.5 配置 Mapper 使用 Redis 缓存
在 Mapper.xml 中指定缓存类型为 Redis,替代默认的本地缓存。
UserMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.UserMapper">
<!-- 配置Redis作为二级缓存 -->
<cache
type="org.mybatis.caches.redis.RedisCache" <!-- 指定Redis缓存实现类 -->
eviction="LRU" <!-- 缓存淘汰策略:最近最少使用 -->
flushInterval="300000" <!-- 自动刷新间隔(5分钟) -->
size="1000" <!-- 最大缓存对象数量 -->
readOnly="false"/> <!-- 非只读(需序列化) -->
<!-- 查询语句:默认使用二级缓存 -->
<select id="selectById" resultType="com.example.entity.User">
SELECT id, username, email, create_time AS createTime
FROM t_user
WHERE id = #{id}
</select>
<!-- 更新语句:默认触发缓存清空(flushCache=true) -->
<update id="update">
UPDATE t_user
SET username = #{username}, email = #{email}
WHERE id = #{id}
</update>
</mapper>
关键配置说明:
type="org.mybatis.caches.redis.RedisCache"
:指定 MyBatis 使用 Redis 存储缓存数据。eviction="LRU"
:当缓存满时,移除最久未使用的对象,避免内存溢出。flushInterval="300000"
:5 分钟自动刷新一次缓存,作为数据一致性的兜底策略。
3.6 全局启用二级缓存
在 MyBatis 配置文件(或 Spring Boot 配置)中确保二级缓存全局开启(默认开启,建议显式配置)。
application.yml
mybatis:
configuration:
cache-enabled: true # 全局启用二级缓存(默认true)
mapper-locations: classpath:mapper/*.xml # 指定Mapper.xml路径
3.7 验证分布式缓存效果
部署两个服务节点(Node1 和 Node2),通过测试验证缓存一致性:
测试代码(Service 层)
@Slf4j
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
/**
* 查询用户:优先从Redis缓存获取
*/
public User getUserById(Long id) {
if (Objects.isNull(id)) {
log.warn("用户ID为空");
return null;
}
User user = userMapper.selectById(id);
log.info("查询用户结果:{}", user);
return user;
}
/**
* 更新用户:触发Redis缓存清空
*/
@Transactional
public void updateUser(User user) {
if (Objects.isNull(user) || Objects.isNull(user.getId())) {
log.warn("用户信息不完整");
return;
}
int rows = userMapper.update(user);
log.info("更新用户影响行数:{}", rows);
// 事务提交后,MyBatis会自动清空Redis中该Mapper的缓存
}
}
测试步骤与预期结果:
- Node1 首次查询用户 ID=1:未命中缓存,查询数据库,结果存入 Redis。
- Node2 查询用户 ID=1:命中 Redis 缓存,直接返回结果(无需查库)。
- Node1 更新用户 ID=1:事务提交后,Redis 中该用户的缓存被清空。
- Node2 再次查询用户 ID=1:未命中缓存,查询数据库获取最新数据,并存入 Redis。
通过 Redis 客户端(如redis-cli
)可观察到缓存键值的创建与删除,证明所有节点共享同一缓存。
四、分布式缓存的高级优化策略
4.1 缓存键设计:避免命名冲突
MyBatis 默认的缓存键由namespace + SQL语句 + 参数
组成,在分布式环境下需确保唯一性。可通过自定义RedisCache
实现类优化键名:
CustomRedisCache.java
package com.example.cache;
import org.mybatis.caches.redis.RedisCache;
import java.util.UUID;
/**
* 自定义Redis缓存,添加应用前缀避免键冲突
*/
public class CustomRedisCache extends RedisCache {
// 应用唯一标识(避免多应用共用Redis时键冲突)
private static final String APP_PREFIX = "myapp:";
public CustomRedisCache(String id) {
super(id);
}
/**
* 重写缓存键,添加应用前缀
*/
@Override
public Object getObject(Object key) {
String cacheKey = APP_PREFIX + key.toString();
return super.getObject(cacheKey);
}
@Override
public void putObject(Object key, Object value) {
String cacheKey = APP_PREFIX + key.toString();
super.putObject(cacheKey, value);
}
@Override
public Object removeObject(Object key) {
String cacheKey = APP_PREFIX + key.toString();
return super.removeObject(cacheKey);
}
}
在 Mapper.xml 中使用自定义缓存:
<cache type="com.example.cache.CustomRedisCache"/>
4.2 缓存失效策略:主动 + 被动结合
分布式环境下,单一的自动失效可能存在延迟,需结合主动失效策略:
-
被动失效 :依赖
flushInterval
自动刷新(如 30 分钟),适合非核心数据。 -
主动失效:更新数据后,通过代码手动删除缓存(极端场景):
/**
- 手动删除指定用户的缓存
*/
public void deleteUserCache(Long userId) {
// 获取UserMapper的缓存对象
Cache cache = sqlSessionFactory.getConfiguration().getCache("com.example.mapper.UserMapper");
if (Objects.nonNull(cache)) {
// 构造缓存键(需与MyBatis生成规则一致)
// 键格式:namespace + "::" + SQLID + "::" + 参数
String cacheKey = "com.example.mapper.UserMapper::selectById::" + userId;
cache.removeObject(cacheKey);
log.info("手动删除用户缓存,key: {}", cacheKey);
}
}
- 手动删除指定用户的缓存
4.3 处理缓存与数据库一致性:延迟双删
在高并发场景,更新数据库后立即删除缓存可能仍有风险(删除缓存前已有请求读取旧缓存)。可采用 "延迟双删" 策略:
@Transactional
public void updateUserWithDelayDelete(User user) {
// 1. 更新数据库
userMapper.update(user);
// 2. 第一次删除缓存(事务提交后执行)
transactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
// 事务提交后删除缓存
deleteUserCache(user.getId());
// 3. 延迟1秒后第二次删除(避免更新前的请求仍读取旧缓存)
CompletableFuture.runAsync(() -> {
try {
Thread.sleep(1000);
deleteUserCache(user.getId());
} catch (InterruptedException e) {
log.error("延迟删除缓存失败", e);
}
});
}
});
}
4.4 缓存序列化优化:使用 JSON 替代 Java 序列化
默认情况下,MyBatis-Redis 使用 Java 序列化存储对象,存在性能差、可读性低的问题。可自定义序列化方式(如 JSON):
JsonRedisCache.java(简化版)
package com.example.cache;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.ibatis.cache.Cache;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class JsonRedisCache implements Cache {
private final String id;
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private final RedisTemplate<String, Object> redisTemplate;
private final ObjectMapper objectMapper = new ObjectMapper();
public JsonRedisCache(String id) {
this.id = id;
// 注入RedisTemplate(实际需通过Spring上下文获取)
this.redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
}
@Override
public String getId() { return id; }
@Override
public void putObject(Object key, Object value) {
try {
String jsonValue = objectMapper.writeValueAsString(value);
redisTemplate.opsForValue().set(key.toString(), jsonValue, 30, TimeUnit.MINUTES);
} catch (Exception e) {
log.error("缓存序列化失败", e);
}
}
@Override
public Object getObject(Object key) {
try {
String jsonValue = (String) redisTemplate.opsForValue().get(key.toString());
if (StringUtils.hasText(jsonValue)) {
// 根据实际类型反序列化(简化示例)
return objectMapper.readValue(jsonValue, User.class);
}
} catch (Exception e) {
log.error("缓存反序列化失败", e);
}
return null;
}
@Override
public Object removeObject(Object key) {
redisTemplate.delete(key.toString());
return null;
}
@Override
public void clear() {
// 清空当前namespace的所有缓存(需批量删除匹配键)
}
@Override
public int getSize() { return 0; }
@Override
public ReadWriteLock getReadWriteLock() { return readWriteLock; }
}
使用 JSON 序列化后,Redis 中缓存的数据可读性更强,且序列化效率更高。
五、分布式二级缓存的适用场景与禁忌
5.1 适用场景
- 查询频繁、更新极少的数据:如字典表、地区表、系统配置表(更新频率低,缓存命中率高)。
- 非核心业务数据:如商品详情、历史订单(允许短时间不一致,优先保证性能)。
- 数据一致性要求不高的场景:如用户浏览记录、热门商品排行(可接受分钟级延迟)。
5.2 禁忌场景
- 实时性要求极高的数据:如库存数量、账户余额(缓存延迟可能导致超卖、余额显示错误)。
- 高频更新数据:如秒杀商品状态、实时在线人数(缓存命中率低,反而增加 Redis 负担)。
- 超大对象:如包含大量字段的报表数据(序列化 / 传输成本高,不如直接查库)。
六、监控与调优
- 缓存命中率监控 :通过 Redis 的
INFO stats
命令查看keyspace_hits
(命中数)和keyspace_misses
(未命中数),命中率低于 70% 需优化缓存策略。 - 过期键清理:避免缓存键永久有效,结合业务设置合理过期时间(如 30 分钟~24 小时)。
- Redis 集群:高并发场景下,使用 Redis Cluster 保证缓存服务的高可用。
七、总结
分布式环境下正确使用 MyBatis 二级缓存的核心是用集中式缓存(如 Redis)替代本地缓存,关键步骤包括:
- 集成 MyBatis-Redis 适配器,让缓存数据存储到 Redis。
- 实体类实现序列化,确保跨节点传输正常。
- 配置合理的缓存淘汰策略与过期时间,平衡性能与一致性。
- 结合业务场景选择缓存对象,实时性数据禁用缓存。
通过这套方案,既能保留二级缓存的性能优势,又能解决分布式环境的数据一致性问题,实现 "高性能 + 高可靠" 的平衡。