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

