MySQL数据库与缓存一致性保障方案
MySQL数据库与缓存一致性保障方案
作者:技术团队
更新时间:2026-01-16
文档类型:技术知识库
目录
一、问题背景与本质
1.1 为什么会出现不一致
在分布式系统中,数据库和缓存是两个独立的系统,会出现以下问题:
| 问题类型 | 说明 | 影响 |
|---|---|---|
| 原子性问题 | 数据库和缓存更新不是原子操作 | 可能只成功一个 |
| 并发问题 | 多个请求同时读写 | 脏数据覆盖新数据 |
| 网络延迟 | 缓存删除/更新有延迟 | 短暂数据不一致 |
| 操作失败 | 数据库成功但缓存失败(或相反) | 长期数据不一致 |
1.2 一致性分类
实际应用:
- 强一致性场景: 金融交易、库存扣减、账户余额
- 最终一致性场景: 用户信息、商品详情、文章内容(大部分业务场景)
- 弱一致性场景: 浏览次数、点赞数、在线人数
1.3 核心原则
⚠️ 重要原则:数据库是唯一的数据源(Single Source of Truth),缓存只是辅助!
优先级:数据库正确性 > 缓存一致性 > 性能
二、缓存更新策略详解
2.1 Cache Aside 模式(旁路缓存)★★★★★
最常用的模式,适合大部分业务场景。
读操作流程
Java实现:
@Service
public class UserService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private UserMapper userMapper;
/**
* 读取用户信息 - Cache Aside 模式
*/
public UserInfo getUserById(Long userId) {
String key = "user:info:" + userId;
// 1. 查缓存
UserInfo userInfo = (UserInfo) redisTemplate.opsForValue().get(key);
if (userInfo != null) {
log.info("缓存命中: {}", key);
return userInfo;
}
log.info("缓存未命中,查询数据库: {}", userId);
// 2. 查数据库
userInfo = userMapper.selectById(userId);
if (userInfo == null) {
return null;
}
// 3. 写入缓存(设置30分钟过期)
redisTemplate.opsForValue().set(key, userInfo, 30, TimeUnit.MINUTES);
return userInfo;
}
}
写操作流程(推荐方案)
Java实现:
/**
* 更新用户信息 - 先更新数据库,再删除缓存
*/
@Transactional(rollbackFor = Exception.class)
public void updateUser(UserInfo userInfo) {
// 1. 更新数据库
int rows = userMapper.updateById(userInfo);
if (rows == 0) {
throw new BusinessException("用户不存在");
}
// 2. 删除缓存
String key = "user:info:" + userInfo.getId();
redisTemplate.delete(key);
log.info("更新用户成功,已删除缓存: {}", key);
}
2.2 为什么删除缓存而不是更新缓存?★★★★★
这是面试高频问题!
| 对比项 | 删除缓存 | 更新缓存 |
|---|---|---|
| 实现复杂度 | ✅ 简单,只需delete | ❌ 复杂,需要重新查询和计算 |
| 性能开销 | ✅ 低,只是删除操作 | ❌ 高,每次都要查库和计算 |
| 无效更新 | ✅ 不存在 | ❌ 可能多次更新但没人读 |
| 数据一致性 | ✅ 下次读取时加载最新数据 | ❌ 更新过程中可能出错 |
场景对比:
// ❌ 更新缓存的问题
public void updateUser(UserInfo userInfo) {
// 1. 更新数据库
userMapper.updateById(userInfo);
// 2. 更新缓存 - 需要重新查询和计算
// 如果用户信息涉及关联查询,成本很高
UserInfoDTO dto = buildComplexUserDTO(userInfo.getId()); // 查询爵位、勋章、等级等
redisTemplate.opsForValue().set(key, dto);
// 问题:如果短时间内更新3次,就要查库3次,但可能没人读取!
}
// ✅ 删除缓存的优势
public void updateUser(UserInfo userInfo) {
// 1. 更新数据库
userMapper.updateById(userInfo);
// 2. 删除缓存
redisTemplate.delete(key);
// 优势:只有被读取时才会重新加载,避免无效计算
}
2.3 其他缓存模式(了解)
Read Through(读穿透)
缓存框架自动处理缓存加载,业务代码只需要读取缓存。
// 类似Spring Cache的 @Cacheable
@Cacheable(value = "users", key = "#userId")
public UserInfo getUserById(Long userId) {
return userMapper.selectById(userId);
}
Write Through(写穿透)
更新数据时,缓存框架自动同步更新缓存和数据库。
@CachePut(value = "users", key = "#userInfo.id")
public UserInfo updateUser(UserInfo userInfo) {
userMapper.updateById(userInfo);
return userInfo;
}
Write Behind(写回)
先更新缓存,异步批量更新数据库(适合写多读少的场景)。
不推荐: 可能丢失数据,一致性难以保证。
三、常见的不一致场景
3.1 场景1:先删除缓存,再更新数据库 ❌
问题: 并发读写导致旧数据重新写入缓存
时间线:
T1: 请求A删除缓存
T2: 请求B查询,缓存未命中,读取旧数据
T3: 请求B将旧数据写入缓存
T4: 请求A更新数据库
→ 结果:缓存旧数据,数据库新数据,不一致!
解决方案: 改为先更新数据库,再删除缓存(见场景2)
3.2 场景2:先更新数据库,再删除缓存 - 删除失败 ❌
问题: 删除缓存时发生异常(网络超时、Redis宕机等)
Java代码:
// 问题代码
public void updateUser(UserInfo userInfo) {
// 1. 更新数据库(成功)
userMapper.updateById(userInfo);
// 2. 删除缓存(失败)
try {
redisTemplate.delete(key);
} catch (Exception e) {
// Redis异常,缓存删除失败
log.error("删除缓存失败", e);
// ❌ 数据已经不一致了!
}
}
解决方案: 见第四章的重试机制、延迟双删、MQ保障等方案
3.3 场景3:先更新数据库,再删除缓存 - 并发问题 ⚠️
问题: 极端情况下的并发读写(发生概率较低)
为什么发生概率低?
这个场景需要满足:
- 缓存刚好失效
- 请求B读取数据库速度慢
- 请求A更新+删除缓存的速度快
- 请求B读数据库比请求A的更新+删除还慢
时间窗口分析:
正常情况:数据库查询(10ms) << 数据库更新(50ms) + 缓存删除(5ms)
异常情况:数据库查询被阻塞(慢查询、锁等待) > 100ms
解决方案: 延迟双删(见第四章)
四、一致性保障方案
4.1 方案1:延迟双删 ★★★★☆
原理: 删除两次缓存,兜底清除可能被并发请求写入的旧数据
Java实现:
@Service
public class UserService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private UserMapper userMapper;
@Autowired
private ThreadPoolTaskExecutor asyncExecutor;
/**
* 延迟双删方案
*/
@Transactional(rollbackFor = Exception.class)
public void updateUser(UserInfo userInfo) {
String key = "user:info:" + userInfo.getId();
// 1. 第一次删除缓存
redisTemplate.delete(key);
log.info("第一次删除缓存: {}", key);
// 2. 更新数据库
userMapper.updateById(userInfo);
log.info("更新数据库成功: {}", userInfo.getId());
// 3. 异步延迟第二次删除缓存
asyncExecutor.execute(() -> {
try {
// 延迟时间根据业务场景调整(一般500ms-1s)
Thread.sleep(500);
redisTemplate.delete(key);
log.info("第二次删除缓存(延迟): {}", key);
} catch (InterruptedException e) {
log.error("延迟双删异常", e);
Thread.currentThread().interrupt();
}
});
}
}
延迟时间如何确定?
延迟时间 > 读取数据库并写入缓存的时间
一般设置:
- 低并发场景:500ms
- 高并发场景:1000ms
- 极端场景:根据监控的P99耗时设置
优点:
- ✅ 简单易实现
- ✅ 能解决大部分并发问题
- ✅ 适合中小型项目
缺点:
- ❌ 延迟时间不好确定
- ❌ 第二次删除仍可能失败
- ❌ 增加了响应时间(异步可缓解)
4.2 方案2:消息队列异步重试 ★★★★★
原理: 通过MQ的重试机制保证缓存最终被删除
Java实现(RocketMQ):
// ========== 生产者 ==========
@Service
public class UserService {
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Autowired
private UserMapper userMapper;
/**
* 更新用户 - MQ异步删除缓存
*/
@Transactional(rollbackFor = Exception.class)
public void updateUser(UserInfo userInfo) {
// 1. 更新数据库
userMapper.updateById(userInfo);
log.info("更新数据库成功: {}", userInfo.getId());
// 2. 发送MQ消息删除缓存
CacheDeleteMessage message = CacheDeleteMessage.builder()
.cacheKey("user:info:" + userInfo.getId())
.userId(userInfo.getId())
.operationType("UPDATE")
.timestamp(System.currentTimeMillis())
.build();
// 同步发送,确保消息发送成功
SendResult sendResult = rocketMQTemplate.syncSend(
"cache-delete-topic",
message,
3000 // 3秒超时
);
if (sendResult.getSendStatus() != SendStatus.SEND_OK) {
throw new BusinessException("发送MQ消息失败");
}
log.info("发送删除缓存消息成功: {}", message);
}
}
// ========== 消息实体 ==========
@Data
@Builder
public class CacheDeleteMessage {
private String cacheKey;
private Long userId;
private String operationType;
private Long timestamp;
}
// ========== 消费者 ==========
@Component
@RocketMQMessageListener(
topic = "cache-delete-topic",
consumerGroup = "cache-delete-consumer-group",
consumeMode = ConsumeMode.CONCURRENTLY,
maxReconsumeTimes = 3 // 最多重试3次
)
public class CacheDeleteConsumer implements RocketMQListener<CacheDeleteMessage> {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public void onMessage(CacheDeleteMessage message) {
log.info("收到删除缓存消息: {}", message);
try {
// 删除缓存
Boolean deleted = redisTemplate.delete(message.getCacheKey());
if (Boolean.TRUE.equals(deleted)) {
log.info("删除缓存成功: {}", message.getCacheKey());
} else {
log.warn("缓存可能已不存在: {}", message.getCacheKey());
}
} catch (Exception e) {
log.error("删除缓存失败,将重试: {}", message, e);
// 抛出异常,触发RocketMQ重试机制
throw new RuntimeException("删除缓存失败", e);
}
}
}
重试策略配置:
@Configuration
public class RocketMQConfig {
@Bean
public DefaultMQPushConsumer cacheDeleteConsumer() {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("cache-delete-consumer-group");
// 重试间隔:1s, 5s, 10s, 30s, 1m, 2m, 3m, 4m, 5m, 6m, 7m, 8m, 9m, 10m, 20m, 30m, 1h, 2h
consumer.setMaxReconsumeTimes(3); // 最多重试3次
return consumer;
}
}
优点:
- ✅ 解耦业务逻辑和缓存删除
- ✅ 支持自动重试机制
- ✅ 高可用(MQ集群保证消息不丢失)
- ✅ 适合大型分布式系统
缺点:
- ❌ 增加系统复杂度(需要MQ)
- ❌ 有一定延迟(消息投递时间)
4.3 方案3:订阅MySQL Binlog(Canal)★★★★★
原理: 监听MySQL的Binlog日志,数据库变更时自动删除缓存
架构优势:
- 业务代码无侵入
- 数据库是唯一数据源
- 支持数据同步、实时计算等扩展场景
Java实现(Canal):
// ========== 1. 引入依赖 ==========
// pom.xml
<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.client</artifactId>
<version>1.1.7</version>
</dependency>
// ========== 2. Canal监听器 ==========
@Component
@Slf4j
public class CanalCacheHandler {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 监听用户表变更
*/
@CanalEventListener(
destination = "example",
schema = "party_user",
table = "user_info",
eventType = {CanalEntry.EventType.UPDATE, CanalEntry.EventType.DELETE}
)
public void handleUserChange(CanalEntry.EventType eventType, CanalEntry.RowData rowData) {
log.info("监听到用户表变更: eventType={}", eventType);
try {
// 解析用户ID
Long userId = null;
for (CanalEntry.Column column : rowData.getAfterColumnsList()) {
if ("id".equals(column.getName())) {
userId = Long.parseLong(column.getValue());
break;
}
}
if (userId == null) {
log.warn("未找到用户ID,跳过缓存删除");
return;
}
// 删除用户相关缓存
String[] cacheKeys = {
"user:info:" + userId,
"user:extend:" + userId,
"user:level:" + userId
};
Long deleted = redisTemplate.delete(Arrays.asList(cacheKeys));
log.info("删除用户缓存成功: userId={}, keys={}, deleted={}", userId, cacheKeys, deleted);
} catch (Exception e) {
log.error("处理Canal变更事件失败", e);
// 不抛出异常,避免影响Canal消费
}
}
/**
* 监听礼物表变更
*/
@CanalEventListener(
destination = "example",
schema = "party_assets",
table = "gift_info",
eventType = CanalEntry.EventType.UPDATE
)
public void handleGiftChange(CanalEntry.EventType eventType, CanalEntry.RowData rowData) {
log.info("监听到礼物表变更");
// 删除礼物列表缓存
redisTemplate.delete("gift:list:all");
log.info("删除礼物列表缓存成功");
}
}
// ========== 3. Canal配置 ==========
@Configuration
public class CanalConfig {
@Bean
public CanalConnector canalConnector() {
// Canal Server地址
return CanalConnectors.newSingleConnector(
new InetSocketAddress("127.0.0.1", 11111),
"example", // destination
"", // username
"" // password
);
}
}
MySQL配置(开启Binlog):
-- 1. 查看Binlog是否开启
SHOW VARIABLES LIKE 'log_bin';
-- 2. my.cnf配置
[mysqld]
log-bin=mysql-bin
binlog-format=ROW
server-id=1
-- 3. 创建Canal用户
CREATE USER 'canal'@'%' IDENTIFIED BY 'canal';
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';
FLUSH PRIVILEGES;
Canal Server配置:
# canal.properties
canal.instance.master.address=127.0.0.1:3306
canal.instance.dbUsername=canal
canal.instance.dbPassword=canal
canal.instance.filter.regex=party_user\\.user_info,party_assets\\.gift_info
优点:
- ✅ 业务代码无侵入(最大优势)
- ✅ 数据库变更100%被捕获
- ✅ 支持多种下游消费(缓存删除、数据同步、实时计算)
- ✅ 适合大厂复杂场景
缺点:
- ❌ 架构复杂度高(需要Canal服务)
- ❌ 运维成本高(需要监控Canal)
- ❌ 有一定延迟(Binlog解析和投递)
4.4 方案4:设置缓存过期时间(兜底方案)★★★★★
原理: 即使缓存不一致,过期后会自动加载最新数据
/**
* 所有缓存都应该设置过期时间
*/
public UserInfo getUserById(Long userId) {
String key = "user:info:" + userId;
UserInfo userInfo = (UserInfo) redisTemplate.opsForValue().get(key);
if (userInfo != null) {
return userInfo;
}
userInfo = userMapper.selectById(userId);
if (userInfo != null) {
// ✅ 设置过期时间(兜底方案)
// 即使删除缓存失败,30分钟后也会自动过期
redisTemplate.opsForValue().set(key, userInfo, 30, TimeUnit.MINUTES);
}
return userInfo;
}
过期时间设置建议:
| 数据类型 | 过期时间 | 说明 |
|---|---|---|
| 用户基础信息 | 30分钟 | 更新频率低 |
| 用户等级/爵位 | 10分钟 | 可能变化 |
| 礼物列表 | 1小时 | 基本不变 |
| 活动配置 | 5分钟 | 运营可能调整 |
| 热点数据 | 永不过期 | 主动删除 + 监控 |
随机过期时间(防止缓存雪崩):
/**
* 设置随机过期时间,避免大量缓存同时失效
*/
public void setCacheWithRandomExpire(String key, Object value, long baseExpireMinutes) {
// 基础时间 + 随机时间(±10%)
long randomMinutes = baseExpireMinutes + ThreadLocalRandom.current().nextLong(
-baseExpireMinutes / 10,
baseExpireMinutes / 10
);
redisTemplate.opsForValue().set(key, value, randomMinutes, TimeUnit.MINUTES);
log.info("设置缓存: key={}, expire={}min", key, randomMinutes);
}
4.5 方案对比与选择
| 方案 | 实现复杂度 | 一致性保障 | 性能影响 | 适用场景 |
|---|---|---|---|---|
| 延迟双删 | ⭐⭐ 简单 | ⭐⭐⭐ 中等 | ⭐⭐⭐⭐ 低 | 中小型项目,并发不高 |
| MQ异步重试 | ⭐⭐⭐ 中等 | ⭐⭐⭐⭐ 高 | ⭐⭐⭐ 中等 | 大型项目,高并发场景 |
| Canal Binlog | ⭐⭐⭐⭐ 复杂 | ⭐⭐⭐⭐⭐ 最高 | ⭐⭐⭐⭐ 低 | 大厂,微服务架构 |
| 缓存过期 | ⭐ 最简单 | ⭐⭐ 弱 | ⭐⭐⭐⭐⭐ 无 | 所有场景(兜底) |
组合方案推荐:
🏆 最佳实践 = MQ异步重试 + 缓存过期 + 监控告警
中小型项目 = 延迟双删 + 缓存过期
大型项目 = MQ异步重试 + 缓存过期 + 监控
超大型项目 = Canal Binlog + 缓存过期 + 监控
五、项目实战方案
5.1 完整的缓存服务封装
/**
* 缓存服务 - 统一封装缓存操作
*/
@Service
@Slf4j
public class CacheService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private RocketMQTemplate rocketMQTemplate;
/**
* 查询缓存 - 支持缓存穿透防护
*/
public <T> T get(String key, Class<T> type, Supplier<T> dbLoader) {
// 1. 查缓存
T value = (T) redisTemplate.opsForValue().get(key);
if (value != null) {
log.debug("缓存命中: {}", key);
return value;
}
// 2. 查数据库
value = dbLoader.get();
if (value == null) {
// 缓存空值,防止缓存穿透
redisTemplate.opsForValue().set(key, NULL_VALUE, 5, TimeUnit.MINUTES);
return null;
}
// 3. 写入缓存
setWithRandomExpire(key, value, 30);
return value;
}
/**
* 更新缓存 - 先删除缓存,发送MQ异步删除
*/
public void delete(String key) {
try {
// 1. 同步删除缓存
redisTemplate.delete(key);
log.info("删除缓存成功: {}", key);
// 2. 发送MQ消息(兜底)
CacheDeleteMessage message = CacheDeleteMessage.builder()
.cacheKey(key)
.timestamp(System.currentTimeMillis())
.build();
rocketMQTemplate.asyncSend("cache-delete-topic", message, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
log.info("发送删除缓存消息成功: key={}", key);
}
@Override
public void onException(Throwable e) {
log.error("发送删除缓存消息失败: key={}", key, e);
// 告警
alertService.send("缓存删除MQ发送失败: " + key);
}
});
} catch (Exception e) {
log.error("删除缓存异常: key={}", key, e);
// 告警
alertService.send("缓存删除失败: " + key);
}
}
/**
* 批量删除缓存
*/
public void deletePattern(String pattern) {
Set<String> keys = redisTemplate.keys(pattern);
if (keys != null && !keys.isEmpty()) {
redisTemplate.delete(keys);
log.info("批量删除缓存: pattern={}, count={}", pattern, keys.size());
}
}
/**
* 设置缓存(随机过期时间)
*/
private void setWithRandomExpire(String key, Object value, long baseExpireMinutes) {
long randomMinutes = baseExpireMinutes + ThreadLocalRandom.current().nextLong(
-baseExpireMinutes / 10,
baseExpireMinutes / 10
);
redisTemplate.opsForValue().set(key, value, randomMinutes, TimeUnit.MINUTES);
}
private static final String NULL_VALUE = "NULL";
}
5.2 用户服务完整示例
@Service
@Slf4j
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private UserInfoApi userInfoApi;
@Autowired
private CacheService cacheService;
/**
* 查询用户信息
*/
public UserInfo getUserById(Long userId) {
String key = "user:info:" + userId;
return cacheService.get(key, UserInfo.class, () -> {
// 查询数据库
return userInfoApi.getUserInfoById(userId);
});
}
/**
* 查询用户扩展信息(包含爵位等)
*/
public UserInfoExtend getUserExtendById(Long userId) {
String key = "user:extend:" + userId;
return cacheService.get(key, UserInfoExtend.class, () -> {
// 查询数据库(包含爵位等额外字段)
return userInfoApi.insideGetExtendById(userId);
});
}
/**
* 批量查询用户(避免循环查询)
*/
public Map<Long, UserInfo> getUserMapByIds(List<Long> userIds) {
if (CollectionUtils.isEmpty(userIds)) {
return Collections.emptyMap();
}
// 1. 先批量查询数据库
List<UserInfo> userList = userInfoApi.getUserInfoListByIds(userIds);
// 2. 转Map
return userList.stream()
.collect(Collectors.toMap(UserInfo::getId, Function.identity()));
}
/**
* 更新用户信息 - 删除缓存
*/
@Transactional(rollbackFor = Exception.class)
public void updateUser(UserInfo userInfo) {
// 1. 更新数据库
int rows = userMapper.updateById(userInfo);
if (rows == 0) {
throw new BusinessException("用户不存在");
}
// 2. 删除相关缓存
cacheService.delete("user:info:" + userInfo.getId());
cacheService.delete("user:extend:" + userInfo.getId());
log.info("更新用户成功: userId={}", userInfo.getId());
}
/**
* 更新用户等级 - 删除多个缓存
*/
@Transactional(rollbackFor = Exception.class)
public void updateUserLevel(Long userId, Integer level) {
// 1. 更新数据库
userMapper.updateLevel(userId, level);
// 2. 删除相关缓存
String[] keys = {
"user:info:" + userId,
"user:extend:" + userId,
"user:level:" + userId
};
for (String key : keys) {
cacheService.delete(key);
}
log.info("更新用户等级成功: userId={}, level={}", userId, level);
}
}
六、缓存常见问题
6.1 缓存穿透
问题: 查询不存在的数据,缓存和数据库都没有,每次都打到数据库
解决方案1:布隆过滤器
@Service
public class UserService {
@Autowired
private RedissonClient redisson;
private RBloomFilter<Long> userIdBloomFilter;
@PostConstruct
public void init() {
// 初始化布隆过滤器
userIdBloomFilter = redisson.getBloomFilter("user:id:bloom");
userIdBloomFilter.tryInit(10000000L, 0.01); // 1000万用户,1%误判率
// 加载已有用户ID
List<Long> userIds = userMapper.selectAllUserIds();
for (Long userId : userIds) {
userIdBloomFilter.add(userId);
}
}
public UserInfo getUserById(Long userId) {
// 1. 布隆过滤器判断
if (!userIdBloomFilter.contains(userId)) {
log.warn("用户ID不存在(布隆过滤器): {}", userId);
return null;
}
// 2. 查缓存和数据库
String key = "user:info:" + userId;
return cacheService.get(key, UserInfo.class, () -> {
return userMapper.selectById(userId);
});
}
/**
* 新增用户时,添加到布隆过滤器
*/
@Transactional(rollbackFor = Exception.class)
public void createUser(UserInfo userInfo) {
userMapper.insert(userInfo);
// 添加到布隆过滤器
userIdBloomFilter.add(userInfo.getId());
}
}
解决方案2:缓存空值
public UserInfo getUserById(Long userId) {
String key = "user:info:" + userId;
// 1. 查缓存
Object cached = redisTemplate.opsForValue().get(key);
if (cached != null) {
// 空值标记
if ("NULL".equals(cached)) {
return null;
}
return (UserInfo) cached;
}
// 2. 查数据库
UserInfo userInfo = userMapper.selectById(userId);
if (userInfo == null) {
// 缓存空值,5分钟过期
redisTemplate.opsForValue().set(key, "NULL", 5, TimeUnit.MINUTES);
return null;
}
// 3. 缓存正常数据
redisTemplate.opsForValue().set(key, userInfo, 30, TimeUnit.MINUTES);
return userInfo;
}
6.2 缓存击穿
问题: 热点数据过期,大量请求同时打到数据库
解决方案1:分布式锁
@Service
public class UserService {
@Autowired
private RedissonClient redisson;
public UserInfo getUserById(Long userId) {
String key = "user:info:" + userId;
// 1. 查缓存
UserInfo userInfo = (UserInfo) redisTemplate.opsForValue().get(key);
if (userInfo != null) {
return userInfo;
}
// 2. 分布式锁(防止缓存击穿)
String lockKey = "lock:user:" + userId;
RLock lock = redisson.getLock(lockKey);
try {
// 尝试获取锁,最多等待3秒,锁自动释放时间10秒
if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {
// 双重检查
userInfo = (UserInfo) redisTemplate.opsForValue().get(key);
if (userInfo != null) {
return userInfo;
}
// 查询数据库
userInfo = userMapper.selectById(userId);
if (userInfo != null) {
// 写入缓存
redisTemplate.opsForValue().set(key, userInfo, 30, TimeUnit.MINUTES);
}
return userInfo;
} else {
// 获取锁失败,稍后重试或返回默认值
log.warn("获取锁失败: {}", lockKey);
Thread.sleep(50);
return getUserById(userId); // 递归重试
}
} catch (InterruptedException e) {
log.error("获取锁异常", e);
Thread.currentThread().interrupt();
return null;
} finally {
// 释放锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
解决方案2:热点数据永不过期
/**
* 热点数据(如热门用户、热门礼物)设置为永不过期
* 通过主动更新的方式刷新缓存
*/
public void refreshHotUserCache(Long userId) {
String key = "user:info:" + userId;
// 查询最新数据
UserInfo userInfo = userMapper.selectById(userId);
// 永不过期
redisTemplate.opsForValue().set(key, userInfo);
log.info("刷新热点用户缓存: userId={}", userId);
}
/**
* 定时任务刷新热点数据
*/
@Scheduled(fixedRate = 300000) // 5分钟
public void refreshHotUsersTask() {
// 获取热点用户列表(如在线用户、高活跃用户)
List<Long> hotUserIds = getHotUserIds();
for (Long userId : hotUserIds) {
refreshHotUserCache(userId);
}
}
6.3 缓存雪崩
问题: 大量缓存同时失效,数据库压力暴增
解决方案1:随机过期时间
/**
* 设置随机过期时间,避免同时失效
*/
public void setWithRandomExpire(String key, Object value, long baseMinutes) {
// 基础时间 ± 10%
long randomMinutes = baseMinutes + ThreadLocalRandom.current().nextLong(
-baseMinutes / 10,
baseMinutes / 10 + 1
);
redisTemplate.opsForValue().set(key, value, randomMinutes, TimeUnit.MINUTES);
}
解决方案2:缓存预热
/**
* 系统启动时预热缓存
*/
@Component
public class CacheWarmer implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
log.info("开始缓存预热...");
// 1. 预热用户缓存
warmUpUsers();
// 2. 预热礼物缓存
warmUpGifts();
// 3. 预热配置缓存
warmUpConfigs();
log.info("缓存预热完成");
}
private void warmUpUsers() {
// 预热活跃用户
List<UserInfo> activeUsers = userMapper.selectActiveUsers(1000);
for (UserInfo user : activeUsers) {
String key = "user:info:" + user.getId();
redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES);
}
}
}
解决方案3:多级缓存
/**
* 多级缓存:本地缓存(Caffeine) + Redis
*/
@Service
public class MultiLevelCacheService {
// 本地缓存
private final Cache<String, Object> localCache = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public <T> T get(String key, Class<T> type, Supplier<T> dbLoader) {
// 1. 查本地缓存
T value = (T) localCache.getIfPresent(key);
if (value != null) {
log.debug("本地缓存命中: {}", key);
return value;
}
// 2. 查Redis
value = (T) redisTemplate.opsForValue().get(key);
if (value != null) {
log.debug("Redis缓存命中: {}", key);
// 写入本地缓存
localCache.put(key, value);
return value;
}
// 3. 查数据库
value = dbLoader.get();
if (value != null) {
// 写入Redis和本地缓存
redisTemplate.opsForValue().set(key, value, 30, TimeUnit.MINUTES);
localCache.put(key, value);
}
return value;
}
}
七、监控与降级
7.1 缓存监控指标
/**
* 缓存监控服务
*/
@Service
@Slf4j
public class CacheMonitorService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private final AtomicLong hitCount = new AtomicLong(0);
private final AtomicLong missCount = new AtomicLong(0);
/**
* 记录缓存命中
*/
public void recordHit(String key) {
hitCount.incrementAndGet();
log.debug("缓存命中: {}", key);
}
/**
* 记录缓存未命中
*/
public void recordMiss(String key) {
missCount.incrementAndGet();
log.debug("缓存未命中: {}", key);
}
/**
* 获取缓存命中率
*/
public double getHitRate() {
long total = hitCount.get() + missCount.get();
if (total == 0) {
return 0.0;
}
return (double) hitCount.get() / total * 100;
}
/**
* 定时上报监控指标
*/
@Scheduled(fixedRate = 60000) // 1分钟
public void reportMetrics() {
double hitRate = getHitRate();
long hit = hitCount.getAndSet(0);
long miss = missCount.getAndSet(0);
log.info("缓存监控: 命中率={:.2f}%, 命中={}, 未命中={}", hitRate, hit, miss);
// 上报到监控系统(Prometheus/Grafana)
// metricsRegistry.gauge("cache.hit.rate", hitRate);
// 命中率过低告警
if (hitRate < 80) {
alertService.send("缓存命中率过低: " + hitRate + "%");
}
}
}
7.2 缓存降级策略
/**
* 缓存降级服务
*/
@Service
@Slf4j
public class CacheService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private CircuitBreakerRegistry circuitBreakerRegistry;
/**
* 带熔断的缓存查询
*/
public <T> T getWithFallback(String key, Class<T> type, Supplier<T> dbLoader) {
CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker("redis");
try {
// 尝试查询缓存(受熔断器保护)
return circuitBreaker.executeSupplier(() -> {
T value = (T) redisTemplate.opsForValue().get(key);
if (value != null) {
return value;
}
// 查数据库
value = dbLoader.get();
if (value != null) {
redisTemplate.opsForValue().set(key, value, 30, TimeUnit.MINUTES);
}
return value;
});
} catch (Exception e) {
log.error("Redis异常,降级查询数据库: key={}", key, e);
// 降级:直接查数据库
return dbLoader.get();
}
}
}
/**
* 熔断器配置
*/
@Configuration
public class CircuitBreakerConfig {
@Bean
public CircuitBreakerRegistry circuitBreakerRegistry() {
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50) // 失败率50%触发熔断
.waitDurationInOpenState(Duration.ofSeconds(30)) // 熔断30秒
.slidingWindowSize(100) // 滑动窗口100个请求
.build();
return CircuitBreakerRegistry.of(config);
}
}
八、最佳实践总结
8.1 缓存设计原则
/**
* 缓存设计最佳实践
*/
public class CacheBestPractices {
// ✅ 1. 缓存Key规范
private static final String KEY_USER_INFO = "user:info:{userId}";
private static final String KEY_USER_EXTEND = "user:extend:{userId}";
private static final String KEY_GIFT_LIST = "gift:list:all";
// ✅ 2. 过期时间设置
private static final long EXPIRE_USER_INFO = 30; // 30分钟
private static final long EXPIRE_HOT_DATA = -1; // 永不过期
// ✅ 3. 空值缓存时间
private static final long EXPIRE_NULL_VALUE = 5; // 5分钟
// ✅ 4. 缓存大小限制
private static final int MAX_CACHE_SIZE = 1024 * 1024; // 1MB
// ✅ 5. 批量操作限制
private static final int MAX_BATCH_SIZE = 500; // 单次最多500条
}
8.2 实战检查清单
# 缓存一致性保障清单
## 设计阶段
- [ ] 是否确定了缓存更新策略(Cache Aside/Read Through/Write Through)
- [ ] 是否选择了合适的一致性保障方案(延迟双删/MQ/Canal)
- [ ] 是否设计了缓存Key规范
- [ ] 是否评估了缓存大小和过期时间
## 开发阶段
- [ ] 是否先更新数据库再删除缓存
- [ ] 是否设置了缓存过期时间(兜底)
- [ ] 是否使用了随机过期时间(防雪崩)
- [ ] 是否处理了缓存删除失败的情况(重试/告警)
- [ ] 是否防护了缓存穿透(布隆过滤器/空值缓存)
- [ ] 是否防护了缓存击穿(分布式锁/热点数据永不过期)
- [ ] 是否批量查询避免循环(getUserMapByIds)
## 测试阶段
- [ ] 是否测试了并发更新场景
- [ ] 是否测试了Redis异常降级场景
- [ ] 是否测试了缓存穿透/击穿/雪崩场景
- [ ] 是否进行了压力测试
## 上线阶段
- [ ] 是否配置了监控告警(命中率/异常率)
- [ ] 是否准备了降级预案
- [ ] 是否进行了缓存预热
- [ ] 是否记录了应急处理文档
8.3 常见错误总结
| 错误 | 问题 | 正确做法 |
|---|---|---|
| 先删缓存再更新DB | 并发读写导致旧数据入缓存 | 先更新DB再删缓存 |
| 不设置过期时间 | 缓存永久不一致 | 必须设置过期时间(兜底) |
| 循环查询用户 | N+1查询,性能差 | 批量查询转Map赋值 |
| 更新缓存而非删除 | 计算成本高,无效更新多 | 删除缓存,让读请求重新加载 |
| 不处理异常 | Redis异常导致服务不可用 | 捕获异常,降级查DB |
| 不做监控 | 问题发现不及时 | 监控命中率、异常率 |
附录
A. 相关依赖
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Redisson(分布式锁) -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.17.7</version>
</dependency>
<!-- RocketMQ -->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<!-- Canal -->
<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.client</artifactId>
<version>1.1.7</version>
</dependency>
<!-- Caffeine(本地缓存) -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.8</version>
</dependency>
<!-- Resilience4j(熔断器) -->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot2</artifactId>
<version>1.7.1</version>
</dependency>
B. Redis配置
spring:
redis:
host: localhost
port: 6379
password:
database: 0
lettuce:
pool:
max-active: 20
max-idle: 10
min-idle: 5
timeout: 3000ms
# Redisson配置
redisson:
single-server-config:
address: redis://localhost:6379
password:
connection-pool-size: 20
connection-minimum-idle-size: 5
C. 参考资料
文档版本: v1.0
最后更新: 2026-01-16
维护团队: PartyPlay技术团队

浙公网安备 33010602011771号