数据库 面试(面试突击:如果让你设计一个秒杀系统,数据库层面如何防爆?)

数据库 面试(面试突击:如果让你设计一个秒杀系统,数据库层面如何防爆?)
面试突击:如果让你设计一个秒杀系统,数据库层面如何防爆?

高并发场景下,数据库不再是瓶颈,而是需要被“保护”的对象

秒杀系统,几乎是所有互联网大厂面试中的必考题。当面试官抛出“如何设计秒杀系统”时,很多人滔滔不绝地讲Redis预扣库存、MQ异步削峰、CDN静态化……

但当面试官追问一句:“如果让你在数据库层面防爆,你会怎么做?”很多人就卡壳了。

今天,我们就直击痛点,聊聊秒杀系统在数据库层面的防爆实战。


一、为什么数据库会“爆”?

秒杀系统的流量特点是:瞬时流量极高、热点数据集中、读写冲突剧烈

假设一个商品库存100件,10万人同时抢。如果没有防护,数据库会面临:

  • 连接数被打满:大量线程阻塞在数据库连接池上
  • 行锁竞争激烈:对同一行库存记录的更新操作排队执行
  • 死锁风险:多表操作或复杂事务容易触发死锁
  • 主从延迟:高并发写入导致从库同步滞后

一句话:数据库不是用来扛高并发的,是用来保证数据一致性的。


二、数据库防爆的核心策略

1. 连接池隔离:别让秒杀拖垮整个系统

问题:秒杀流量把数据库连接池占满,导致正常业务无法访问数据库。

解决方案:为秒杀业务单独配置数据库连接池。

# HikariCP 配置示例# 正常业务连接池normal-pool:  maximum-pool-size: 50  connection-timeout: 30000# 秒杀业务专用连接池(更小的连接数)seckill-pool:  maximum-pool-size: 10  connection-timeout: 5000  # 超时更快,快速失败  connection-init-sql: "SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED"

关键点

  • 专用连接池连接数控制在 5-15 之间,防止打满整个数据库连接资源
  • 超时时间设短,快速失败比长时间等待更优雅

2. 库存扣减:用对SQL,避免超卖

问题:高并发下,多个请求同时读到剩余库存>0,然后同时扣减,导致超卖。

错误写法

-- 先查询,后更新(有并发风险)SELECT stock FROM product WHERE id = 1;if (stock > 0) {    UPDATE product SET stock = stock - 1 WHERE id = 1;}

正确写法:用带条件的原子更新

-- 一条SQL搞定,数据库保证原子性UPDATE product SET stock = stock - 1 WHERE id = 1 AND stock > 0;-- 返回受影响行数,为1表示成功,为0表示库存不足

进阶方案:乐观锁+版本号(适用于非热点场景,秒杀慎用)

UPDATE product SET stock = stock - 1, version = version + 1WHERE id = 1 AND version = #{version} AND stock > 0;

秒杀场景建议:直接用悲观锁(行锁),因为冲突概率极高,乐观锁重试反而浪费资源。


3. 事务控制:缩短锁持有时间

问题:事务中包含远程调用、复杂业务逻辑,导致行锁长时间不释放。

反例

@Transactionalpublic void seckill(Long userId, Long productId) {    // 扣减库存(持有行锁)    productMapper.decreaseStock(productId);        // 创建订单    orderMapper.insert(order);        // 发送MQ通知(远程调用,可能耗时)    mqClient.send(message);}

正确做法:事务越小越好,扣库存与创建订单可以解耦

public void seckill(Long userId, Long productId) {    // 1. 扣减库存(独立事务,立即释放锁)    int rows = productMapper.decreaseStock(productId);    if (rows == 0) {        throw new StockException("库存不足");    }        // 2. 异步创建订单(MQ解耦)    orderMQ.send(new OrderMessage(userId, productId));}

关键原则

  • 事务中只做必要的数据操作
  • 网络调用、缓存操作、外部请求移出事务
  • 设置事务超时时间 @Transactional(timeout = 3)

4. 表结构优化:从源头降低冲突

问题:所有秒杀商品共用一张库存表,热点数据扎堆。

解决方案:分表+行设计优化

分表策略

-- 按商品ID哈希分表,如拆成16张表product_stock_0product_stock_1...product_stock_15

库存行设计优化

CREATE TABLE `product_stock` (  `id` bigint NOT NULL,  `stock` int NOT NULL COMMENT '总库存',  `sold` int NOT NULL DEFAULT 0 COMMENT '已售数量',  `version` int NOT NULL DEFAULT 0,  PRIMARY KEY (`id`)) ENGINE=InnoDB;

进阶技巧:将库存拆成多个槽位

-- 将100件库存拆成10行,每行10件-- 扣减时随机选一行操作,分散行锁压力UPDATE product_stock_slot SET stock = stock - 1 WHERE product_id = 1 AND slot_id = #{randomSlot} AND stock > 0;

5. 限流熔断:在数据库前兜底

问题:再优化的SQL也扛不住无限流量。

解决方案:在数据库访问层做限流

// 使用RateLimiter或Semaphoreprivate final Semaphore semaphore = new Semaphore(10); // 数据库层并发上限public void seckill(Long productId) {    if (!semaphore.tryAcquire(1, TimeUnit.MILLISECONDS)) {        throw new BusyException("系统繁忙,请稍后再试");    }    try {        // 执行数据库操作        productMapper.decreaseStock(productId);    } finally {        semaphore.release();    }}

数据库层面的熔断配置

数据库 面试(面试突击:如果让你设计一个秒杀系统,数据库层面如何防爆?)

-- MySQL:设置最大连接数SET GLOBAL max_connections = 500;-- 设置线程池大小(MariaDB/Percona)SET GLOBAL thread_pool_size = 16;SET GLOBAL thread_pool_max_threads = 100;

6. 最终一致性:异步削峰

问题:所有请求都实时落库,数据库扛不住。

解决方案:Redis预扣库存 + 异步消费落库

流程:用户请求 → Redis预扣库存(Lua脚本保证原子性)→ 返回“排队中”→ 发送MQ消息 → 消费者批量落库(每批100条)→ 更新最终库存

Redis Lua脚本示例

-- 库存扣减Lua脚本local key = KEYS[1]local stock = tonumber(redis.call('get', key))if stock and stock > 0 then    redis.call('decr', key)    return 1endreturn 0

三、面试加分项:说出你的防爆三板斧

当面试官问到数据库防爆时,可以这样总结:

第一板斧:限流隔离
专用连接池 + 数据库层限流,防止流量打满连接。

第二板斧:行锁优化
单行原子更新 + 短事务 + 多槽位拆分,最小化锁竞争。

第三板斧:异步削峰
Redis预扣库存 + MQ异步落库,用最终一致性换取系统稳定性。


四、总结

数据库在秒杀系统中的角色,不是“扛流量”的战士,而是“保一致”的底线。

记住三个核心原则:

  1. 能不进库就不进库:用Redis、CDN、静态化挡掉99%的流量
  2. 进库就要快:短事务、原子操作、减少锁持有时间
  3. 扛不住就限流:在数据库前面加一层限流熔断,优雅降级

掌握了这些,再遇到“数据库防爆”的问题,你就能从容应对了。

文章版权声明:除非注明,否则均为边学边练网络文章,版权归原作者所有