一、核心概念

热点数据更新是指在高并发场景下,大量请求集中更新同一行或少数几行数据,导致严重的行锁竞争性能瓶颈。典型场景包括:

  • 商品库存扣减(秒杀场景)
  • 账户余额更新
  • 文章点赞数/阅读数统计
  • 排行榜分数更新

二、问题原因与原理

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缓存 ⭐⭐⭐⭐⭐ 最终一致 秒杀、高并发写
分桶设计 ⭐⭐⭐⭐ 强一致 库存、计数器
乐观锁 ⭐⭐⭐ 强一致 中等并发
队列削峰 ⭐⭐⭐ 强一致 流量不均匀

五、答题总结

面试回答框架

  1. 问题识别:”热点数据更新的核心问题是行锁竞争导致的串行化”
  2. 首选方案:”生产环境优先使用Redis缓存+异步刷盘,可提升百倍性能”
  3. 备选方案:”若需强一致性,可采用分桶设计分散锁竞争”
  4. 实战经验:”曾在秒杀系统中使用Redis预扣库存+MQ异步扣DB,QPS从800提升到8万”

关键点

  • 理解InnoDB行锁机制
  • 掌握缓存、分桶、乐观锁等多种方案
  • 能根据业务特点(一致性要求、并发量)选择合适方案