在分布式環(huán)境下正確使用MyBatis二級(jí)緩存的最佳實(shí)踐
前言
在分布式環(huán)境下使用 MyBatis 二級(jí)緩存,核心挑戰(zhàn)是解決多節(jié)點(diǎn)緩存一致性問(wèn)題。單機(jī)環(huán)境中,二級(jí)緩存是內(nèi)存級(jí)別的本地緩存,而分布式環(huán)境下多節(jié)點(diǎn)獨(dú)立部署,本地緩存無(wú)法跨節(jié)點(diǎn)共享,易導(dǎo)致 “緩存孤島” 和數(shù)據(jù)不一致。本文從底層原理出發(fā),提供一套完整的分布式二級(jí)緩存解決方案,包含實(shí)戰(zhàn)配置與最佳實(shí)踐。
一、分布式環(huán)境下二級(jí)緩存的核心問(wèn)題
在分布式架構(gòu)(如微服務(wù)集群)中,默認(rèn)的 MyBatis 二級(jí)緩存(本地內(nèi)存緩存)會(huì)暴露三個(gè)致命問(wèn)題:
- 緩存孤島:每個(gè)節(jié)點(diǎn)維護(hù)獨(dú)立緩存,同一查詢?cè)诓煌?jié)點(diǎn)可能命中不同緩存數(shù)據(jù)(如節(jié)點(diǎn) A 更新數(shù)據(jù)后,節(jié)點(diǎn) B 的緩存仍是舊值)。
- 數(shù)據(jù)不一致:跨節(jié)點(diǎn)更新數(shù)據(jù)時(shí),無(wú)法通知其他節(jié)點(diǎn)同步清空緩存,導(dǎo)致部分節(jié)點(diǎn)返回臟數(shù)據(jù)。
- 序列化風(fēng)險(xiǎn):本地緩存可直接存儲(chǔ) Java 對(duì)象引用,而分布式緩存需網(wǎng)絡(luò)傳輸,若對(duì)象未序列化會(huì)導(dǎo)致緩存失敗。
二、解決方案:基于集中式緩存的二級(jí)緩存改造
分布式環(huán)境下的核心解決方案是:用集中式緩存(如 Redis、Memcached)替代本地內(nèi)存緩存,讓所有節(jié)點(diǎn)共享同一緩存源,實(shí)現(xiàn)緩存數(shù)據(jù)全局一致。
2.1 技術(shù)選型:MyBatis + Redis(最常用組合)
Redis 作為高性能的分布式緩存中間件,支持?jǐn)?shù)據(jù)持久化、過(guò)期策略和集群模式,是 MyBatis 二級(jí)緩存的理想選擇。實(shí)現(xiàn)思路是:
- 讓 MyBatis 的二級(jí)緩存數(shù)據(jù)存儲(chǔ)到 Redis,而非本地內(nèi)存。
- 所有節(jié)點(diǎn)通過(guò) Redis 訪問(wèn)緩存,確保緩存數(shù)據(jù)全局唯一。
三、實(shí)戰(zhàn):MyBatis 集成 Redis 實(shí)現(xiàn)分布式二級(jí)緩存
3.1 環(huán)境準(zhǔn)備
- JDK 17
- MyBatis 3.5.10+
- Redis 6.2+
- Spring Boot 2.7.x(簡(jiǎn)化配置)
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下創(chuàng)建redis.properties,配置 Redis 連接信息:
# Redis服務(wù)器地址 redis.host=192.168.1.100 # Redis端口 redis.port=6379 # 連接超時(shí)時(shí)間(毫秒) redis.timeout=2000 # Redis密碼(無(wú)密碼則留空) redis.password=your_redis_password # 數(shù)據(jù)庫(kù)索引(默認(rèn)0) redis.database=1 # 緩存默認(rèn)過(guò)期時(shí)間(毫秒,30分鐘) redis.default.expiration=1800000
3.4 改造實(shí)體類:實(shí)現(xiàn)序列化
分布式緩存中,對(duì)象需在網(wǎng)絡(luò)中傳輸,必須實(shí)現(xiàn)Serializable接口,否則會(huì)導(dǎo)致緩存失敗。
User.java
package com.example.entity;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 用戶實(shí)體類(必須實(shí)現(xiàn)Serializable)
*/
@Data
public class User implements Serializable {
// 序列化版本號(hào)(避免反序列化沖突)
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,替代默認(rèn)的本地緩存。
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作為二級(jí)緩存 -->
<cache
type="org.mybatis.caches.redis.RedisCache" <!-- 指定Redis緩存實(shí)現(xiàn)類 -->
eviction="LRU" <!-- 緩存淘汰策略:最近最少使用 -->
flushInterval="300000" <!-- 自動(dòng)刷新間隔(5分鐘) -->
size="1000" <!-- 最大緩存對(duì)象數(shù)量 -->
readOnly="false"/> <!-- 非只讀(需序列化) -->
<!-- 查詢語(yǔ)句:默認(rèn)使用二級(jí)緩存 -->
<select id="selectById" resultType="com.example.entity.User">
SELECT id, username, email, create_time AS createTime
FROM t_user
WHERE id = #{id}
</select>
<!-- 更新語(yǔ)句:默認(rèn)觸發(fā)緩存清空(flushCache=true) -->
<update id="update">
UPDATE t_user
SET username = #{username}, email = #{email}
WHERE id = #{id}
</update>
</mapper>
關(guān)鍵配置說(shuō)明:
type="org.mybatis.caches.redis.RedisCache":指定 MyBatis 使用 Redis 存儲(chǔ)緩存數(shù)據(jù)。eviction="LRU":當(dāng)緩存滿時(shí),移除最久未使用的對(duì)象,避免內(nèi)存溢出。flushInterval="300000":5 分鐘自動(dòng)刷新一次緩存,作為數(shù)據(jù)一致性的兜底策略。
3.6 全局啟用二級(jí)緩存
在 MyBatis 配置文件(或 Spring Boot 配置)中確保二級(jí)緩存全局開(kāi)啟(默認(rèn)開(kāi)啟,建議顯式配置)。
application.yml
mybatis:
configuration:
cache-enabled: true # 全局啟用二級(jí)緩存(默認(rèn)true)
mapper-locations: classpath:mapper/*.xml # 指定Mapper.xml路徑
3.7 驗(yàn)證分布式緩存效果
部署兩個(gè)服務(wù)節(jié)點(diǎn)(Node1 和 Node2),通過(guò)測(cè)試驗(yàn)證緩存一致性:
測(cè)試代碼(Service 層)
@Slf4j
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
/**
* 查詢用戶:優(yōu)先從Redis緩存獲取
*/
public User getUserById(Long id) {
if (Objects.isNull(id)) {
log.warn("用戶ID為空");
return null;
}
User user = userMapper.selectById(id);
log.info("查詢用戶結(jié)果:{}", user);
return user;
}
/**
* 更新用戶:觸發(fā)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("更新用戶影響行數(shù):{}", rows);
// 事務(wù)提交后,MyBatis會(huì)自動(dòng)清空Redis中該Mapper的緩存
}
}
測(cè)試步驟與預(yù)期結(jié)果:
- Node1 首次查詢用戶 ID=1:未命中緩存,查詢數(shù)據(jù)庫(kù),結(jié)果存入 Redis。
- Node2 查詢用戶 ID=1:命中 Redis 緩存,直接返回結(jié)果(無(wú)需查庫(kù))。
- Node1 更新用戶 ID=1:事務(wù)提交后,Redis 中該用戶的緩存被清空。
- Node2 再次查詢用戶 ID=1:未命中緩存,查詢數(shù)據(jù)庫(kù)獲取最新數(shù)據(jù),并存入 Redis。
通過(guò) Redis 客戶端(如redis-cli)可觀察到緩存鍵值的創(chuàng)建與刪除,證明所有節(jié)點(diǎn)共享同一緩存。
四、分布式緩存的高級(jí)優(yōu)化策略
4.1 緩存鍵設(shè)計(jì):避免命名沖突
MyBatis 默認(rèn)的緩存鍵由namespace + SQL語(yǔ)句 + 參數(shù)組成,在分布式環(huán)境下需確保唯一性??赏ㄟ^(guò)自定義RedisCache實(shí)現(xiàn)類優(yōu)化鍵名:
CustomRedisCache.java
package com.example.cache;
import org.mybatis.caches.redis.RedisCache;
import java.util.UUID;
/**
* 自定義Redis緩存,添加應(yīng)用前綴避免鍵沖突
*/
public class CustomRedisCache extends RedisCache {
// 應(yīng)用唯一標(biāo)識(shí)(避免多應(yīng)用共用Redis時(shí)鍵沖突)
private static final String APP_PREFIX = "myapp:";
public CustomRedisCache(String id) {
super(id);
}
/**
* 重寫(xiě)緩存鍵,添加應(yīng)用前綴
*/
@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 緩存失效策略:主動(dòng) + 被動(dòng)結(jié)合
分布式環(huán)境下,單一的自動(dòng)失效可能存在延遲,需結(jié)合主動(dòng)失效策略:
- 被動(dòng)失效:依賴
flushInterval自動(dòng)刷新(如 30 分鐘),適合非核心數(shù)據(jù)。 - 主動(dòng)失效:更新數(shù)據(jù)后,通過(guò)代碼手動(dòng)刪除緩存(極端場(chǎng)景):
/**
* 手動(dòng)刪除指定用戶的緩存
*/
public void deleteUserCache(Long userId) {
// 獲取UserMapper的緩存對(duì)象
Cache cache = sqlSessionFactory.getConfiguration().getCache("com.example.mapper.UserMapper");
if (Objects.nonNull(cache)) {
// 構(gòu)造緩存鍵(需與MyBatis生成規(guī)則一致)
// 鍵格式:namespace + "::" + SQLID + "::" + 參數(shù)
String cacheKey = "com.example.mapper.UserMapper::selectById::" + userId;
cache.removeObject(cacheKey);
log.info("手動(dòng)刪除用戶緩存,key: {}", cacheKey);
}
}
4.3 處理緩存與數(shù)據(jù)庫(kù)一致性:延遲雙刪
在高并發(fā)場(chǎng)景,更新數(shù)據(jù)庫(kù)后立即刪除緩存可能仍有風(fēng)險(xiǎn)(刪除緩存前已有請(qǐng)求讀取舊緩存)??刹捎?“延遲雙刪” 策略:
@Transactional
public void updateUserWithDelayDelete(User user) {
// 1. 更新數(shù)據(jù)庫(kù)
userMapper.update(user);
// 2. 第一次刪除緩存(事務(wù)提交后執(zhí)行)
transactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
// 事務(wù)提交后刪除緩存
deleteUserCache(user.getId());
// 3. 延遲1秒后第二次刪除(避免更新前的請(qǐng)求仍讀取舊緩存)
CompletableFuture.runAsync(() -> {
try {
Thread.sleep(1000);
deleteUserCache(user.getId());
} catch (InterruptedException e) {
log.error("延遲刪除緩存失敗", e);
}
});
}
});
}
4.4 緩存序列化優(yōu)化:使用 JSON 替代 Java 序列化
默認(rèn)情況下,MyBatis-Redis 使用 Java 序列化存儲(chǔ)對(duì)象,存在性能差、可讀性低的問(wèn)題??勺远x序列化方式(如 JSON):
JsonRedisCache.java(簡(jiǎn)化版)
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(實(shí)際需通過(guò)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)) {
// 根據(jù)實(shí)際類型反序列化(簡(jiǎn)化示例)
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() {
// 清空當(dāng)前namespace的所有緩存(需批量刪除匹配鍵)
}
@Override
public int getSize() { return 0; }
@Override
public ReadWriteLock getReadWriteLock() { return readWriteLock; }
}
使用 JSON 序列化后,Redis 中緩存的數(shù)據(jù)可讀性更強(qiáng),且序列化效率更高。
五、分布式二級(jí)緩存的適用場(chǎng)景與禁忌
5.1 適用場(chǎng)景
- 查詢頻繁、更新極少的數(shù)據(jù):如字典表、地區(qū)表、系統(tǒng)配置表(更新頻率低,緩存命中率高)。
- 非核心業(yè)務(wù)數(shù)據(jù):如商品詳情、歷史訂單(允許短時(shí)間不一致,優(yōu)先保證性能)。
- 數(shù)據(jù)一致性要求不高的場(chǎng)景:如用戶瀏覽記錄、熱門(mén)商品排行(可接受分鐘級(jí)延遲)。
5.2 禁忌場(chǎng)景
- 實(shí)時(shí)性要求極高的數(shù)據(jù):如庫(kù)存數(shù)量、賬戶余額(緩存延遲可能導(dǎo)致超賣、余額顯示錯(cuò)誤)。
- 高頻更新數(shù)據(jù):如秒殺商品狀態(tài)、實(shí)時(shí)在線人數(shù)(緩存命中率低,反而增加 Redis 負(fù)擔(dān))。
- 超大對(duì)象:如包含大量字段的報(bào)表數(shù)據(jù)(序列化 / 傳輸成本高,不如直接查庫(kù))。
六、監(jiān)控與調(diào)優(yōu)
- 緩存命中率監(jiān)控:通過(guò) Redis 的
INFO stats命令查看keyspace_hits(命中數(shù))和keyspace_misses(未命中數(shù)),命中率低于 70% 需優(yōu)化緩存策略。 - 過(guò)期鍵清理:避免緩存鍵永久有效,結(jié)合業(yè)務(wù)設(shè)置合理過(guò)期時(shí)間(如 30 分鐘~24 小時(shí))。
- Redis 集群:高并發(fā)場(chǎng)景下,使用 Redis Cluster 保證緩存服務(wù)的高可用。
七、總結(jié)
分布式環(huán)境下正確使用 MyBatis 二級(jí)緩存的核心是用集中式緩存(如 Redis)替代本地緩存,關(guān)鍵步驟包括:
- 集成 MyBatis-Redis 適配器,讓緩存數(shù)據(jù)存儲(chǔ)到 Redis。
- 實(shí)體類實(shí)現(xiàn)序列化,確??绻?jié)點(diǎn)傳輸正常。
- 配置合理的緩存淘汰策略與過(guò)期時(shí)間,平衡性能與一致性。
- 結(jié)合業(yè)務(wù)場(chǎng)景選擇緩存對(duì)象,實(shí)時(shí)性數(shù)據(jù)禁用緩存。
通過(guò)這套方案,既能保留二級(jí)緩存的性能優(yōu)勢(shì),又能解決分布式環(huán)境的數(shù)據(jù)一致性問(wèn)題,實(shí)現(xiàn) “高性能 + 高可靠” 的平衡。
到此這篇關(guān)于在分布式環(huán)境下正確使用MyBatis二級(jí)緩存的文章就介紹到這了,更多相關(guān)分布式環(huán)境使用MyBatis二級(jí)緩存內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Jackson優(yōu)雅序列化Java枚舉類過(guò)程解析
這篇文章主要介紹了Jackson優(yōu)雅序列化Java枚舉類過(guò)程解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-09-09
SpringBoot實(shí)現(xiàn)單點(diǎn)登錄的實(shí)現(xiàn)詳解
在現(xiàn)代的Web應(yīng)用程序中,單點(diǎn)登錄(Single?Sign-On)已經(jīng)變得越來(lái)越流行,在本文中,我們將使用Spring?Boot構(gòu)建一個(gè)基本的單點(diǎn)登錄系統(tǒng),需要的可以參考一下2023-05-05
Java Web項(xiàng)目中編寫(xiě)定時(shí)任務(wù)的實(shí)現(xiàn)
本篇文章主要介紹了Java Web項(xiàng)目中編寫(xiě)定時(shí)任務(wù)的實(shí)現(xiàn),具有一定的參考價(jià)值,有興趣的可以了解一下。2017-01-01
Java調(diào)用DeepSeek?API的8個(gè)高頻坑與解決方法
現(xiàn)在大模型開(kāi)發(fā)特別火,DeepSeek?因?yàn)橹形睦斫夂?、反?yīng)快、還便宜,不少?Java?開(kāi)發(fā)者都用它,本文整理了最常踩的?8?個(gè)坑,希望對(duì)大家有所幫助2025-12-12
解決@Validated對(duì)List對(duì)象驗(yàn)證失效問(wèn)題
文章指出@Validated注解無(wú)法驗(yàn)證List數(shù)組參數(shù),建議手動(dòng)逐項(xiàng)驗(yàn)證或封裝List為對(duì)象進(jìn)行驗(yàn)證,前者代碼冗余但直接,后者需調(diào)整JSON結(jié)構(gòu)但更簡(jiǎn)潔,根據(jù)需求選擇方案,權(quán)衡靈活性與參數(shù)改造成本2025-09-09
SpringBoot使用@Scheduled定時(shí)器的示例詳解
SpringBoot的@Scheduled注解用于配置定時(shí)任務(wù),需啟用@EnableScheduling,支持fixedRate、fixedDelay、cron等參數(shù),控制執(zhí)行頻率、延遲及周期,支持線程池和異常處理配置,默認(rèn)單線程串行執(zhí)行,本文介紹SpringBoot使用@Scheduled定時(shí)器的操作示例,感興趣的朋友一起看看吧2025-07-07
Java快速排序的實(shí)現(xiàn)詳細(xì)代碼及通俗解釋
這篇文章主要介紹了Java快速排序?qū)崿F(xiàn)的相關(guān)資料,快速排序是一種高效的排序算法,通過(guò)選擇一個(gè)基準(zhǔn)值將數(shù)組分成兩部分,左邊的元素比基準(zhǔn)值小,右邊的元素比基準(zhǔn)值大,然后遞歸地對(duì)這兩部分進(jìn)行排序,需要的朋友可以參考下2025-02-02

