一、核心概念
热点数据更新是指在高并发场景下,大量请求集中更新同一行或少数几行数据,导致严重的行锁竞争和性能瓶颈。典型场景包括:
- 商品库存扣减(秒杀场景)
- 账户余额更新
- 文章点赞数/阅读数统计
- 排行榜分数更新
二、问题原因与原理
1. 行锁竞争瓶颈
-- 热点行更新导致锁等待
UPDATE goods SET stock = stock - 1 WHERE id = 1001;
核心问题:
- InnoDB行锁:每次UPDATE会对目标行加排他锁(X锁)
- 高并发场景下,后续请求必须串行等待前一个事务提交
- 锁等待时间累积,TPS急剧下降,甚至触发锁等待超时
2. 锁等待链路
请求1: BEGIN -> UPDATE -> [持有行锁] -> COMMIT
请求2: [等待行锁...]
请求3: [等待行锁...]
请求N: [等待行锁...]
##三、高效更新方案
方案1:Redis缓存 + 异步刷盘(推荐)
核心思路:将热点更新操作迁移到Redis,定期批量刷回MySQL
@Service
public class HotDataUpdateService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
// 1. 扣减库存(Redis层面)
public boolean deductStock(Long goodsId, int count) {
String key = "stock:" + goodsId;
Long remain = redisTemplate.opsForValue().decrement(key, count);
return remain != null && remain >= 0;
}
// 2. 定时任务批量刷盘
@Scheduled(fixedRate = 5000)
public void syncStockToDb() {
Set<String> keys = redisTemplate.keys("stock:*");
for (String key : keys) {
Long redisStock = Long.parseLong(redisTemplate.opsForValue().get(key));
Long goodsId = Long.parseLong(key.split(":")[1]);
// 批量更新MySQL
goodsMapper.updateStock(goodsId, redisStock);
}
}
}
优势:
- Redis单线程模型天然避免竞争
- 吞吐量提升100倍以上
- 异步刷盘降低数据库压力
注意:需处理缓存与DB的最终一致性
方案2:分桶设计(Sharding)
核心思路:将单行热点数据拆分为多行,分散锁竞争
-- 原始设计(热点行)
CREATE TABLE goods (
id BIGINT PRIMARY KEY,
stock INT
);
-- 分桶设计(10个桶)
CREATE TABLE goods_stock (
goods_id BIGINT,
bucket_id INT, -- 桶编号 0-9
stock INT,
PRIMARY KEY (goods_id, bucket_id)
);
更新逻辑:
public boolean deductStock(Long goodsId, int count) {
// 随机选择一个桶
int bucketId = ThreadLocalRandom.current().nextInt(10);
int updated = jdbcTemplate.update(
"UPDATE goods_stock SET stock = stock - ? " +
"WHERE goods_id = ? AND bucket_id = ? AND stock >= ?",
count, goodsId, bucketId, count
);
return updated > 0;
}
// 查询总库存
public int getTotalStock(Long goodsId) {
return jdbcTemplate.queryForObject(
"SELECT SUM(stock) FROM goods_stock WHERE goods_id = ?",
Integer.class, goodsId
);
}
优势:
- 10个桶可将锁竞争降低10倍
- 纯DB方案,无需引入缓存
- 适合库存等可拆分的场景
劣势:
- 需重构表结构
- 查询总数需要SUM聚合
方案3:乐观锁 + 重试
核心思路:使用版本号避免行锁,冲突时客户端重试
-- 添加版本号字段
ALTER TABLE goods ADD COLUMN version INT DEFAULT 0;
-- 乐观锁更新
UPDATE goods
SET stock = stock - 1, version = version + 1
WHERE id = 1001 AND version = #{oldVersion} AND stock >= 1;
@Service
public class OptimisticLockService {
private static final int MAX_RETRY = 3;
public boolean deductStock(Long goodsId, int count) {
for (int i = 0; i < MAX_RETRY; i++) {
// 1. 查询当前版本
Goods goods = goodsMapper.selectById(goodsId);
if (goods.getStock() < count) {
return false;
}
// 2. CAS更新
int updated = goodsMapper.updateWithVersion(
goodsId, count, goods.getVersion()
);
if (updated > 0) {
return true; // 更新成功
}
// 版本冲突,重试
}
return false; // 重试失败
}
}
优势:
- 无需外部缓存
- 冲突较少时性能较好
劣势:
- 高并发下重试次数多,CPU浪费
- 不适合极端热点场景
方案4:队列削峰
核心思路:请求先入队列,后端单线程消费处理
@Service
public class QueueBasedUpdateService {
private BlockingQueue<UpdateTask> queue = new LinkedBlockingQueue<>(10000);
// 提交更新任务
public CompletableFuture<Boolean> submitUpdate(Long goodsId, int count) {
CompletableFuture<Boolean> future = new CompletableFuture<>();
queue.offer(new UpdateTask(goodsId, count, future));
return future;
}
// 单线程消费
@PostConstruct
public void startConsumer() {
new Thread(() -> {
while (true) {
try {
UpdateTask task = queue.take();
boolean success = doUpdate(task.getGoodsId(), task.getCount());
task.getFuture().complete(success);
} catch (Exception e) {
// 错误处理
}
}
}).start();
}
}
优势:
- 天然串行化,无锁竞争
- 流量削峰保护数据库
劣势:
- 响应时间增加
- 需处理队列积压
四、方案对比与选择
| 方案 | 性能 | 复杂度 | 一致性 | 适用场景 |
|---|---|---|---|---|
| Redis缓存 | ⭐⭐⭐⭐⭐ | 中 | 最终一致 | 秒杀、高并发写 |
| 分桶设计 | ⭐⭐⭐⭐ | 高 | 强一致 | 库存、计数器 |
| 乐观锁 | ⭐⭐⭐ | 低 | 强一致 | 中等并发 |
| 队列削峰 | ⭐⭐⭐ | 中 | 强一致 | 流量不均匀 |
五、答题总结
面试回答框架:
- 问题识别:”热点数据更新的核心问题是行锁竞争导致的串行化”
- 首选方案:”生产环境优先使用Redis缓存+异步刷盘,可提升百倍性能”
- 备选方案:”若需强一致性,可采用分桶设计分散锁竞争”
- 实战经验:”曾在秒杀系统中使用Redis预扣库存+MQ异步扣DB,QPS从800提升到8万”
关键点:
- 理解InnoDB行锁机制
- 掌握缓存、分桶、乐观锁等多种方案
- 能根据业务特点(一致性要求、并发量)选择合适方案