秒杀系统实现
0. 近期思路:
-
异步调用升级为 CompletableFuture
-
秒杀完成后回调执行发短信,写日志等
1. 概述
系统架构如下图所示:
系统一共包括 7 个模块:
- limit-app 启动模块:包括启动类和页面静态资源。
- limit-common 公共模块:包括通用模型,如限流工具、线程池、缓存淘汰策略、Util 等的实现。
- limit-config 配置模块:目前没有使用。
- limit-redis 缓存模块:包括缓存的配置、前缀生成、分布式锁等的实现或封装。
- limit-rocketmq 消息队列模块:包括生产者消费者的抽象。
- limit-seckill 秒杀模块:业务模块,包括秒杀功能的实现。
- limit-user 用户模块:业务模块,包括用户信息管理的实现。
2. 公共模块
- 重定义线程池和阻塞队列,在 JDK 原有线程池的基础上,实现参数可调整,实现监控线程池运行状态,包括任务统计,执行时间,最长执行时间等。
- 项目中目前用到线程池的功能点主要包括缓存预热和线程派发。
- 实现缓存淘汰策略 LFU、LRU、LRU2。
- 项目中目前用到缓存淘汰策略的功能点主要是本地缓存中的商品库存预减操作。
- 借助缓存实现分布式令牌桶、漏桶和滑动窗口。
- 以上三种限流工具的的实现请参考 “基于 Redis 的分布式令牌桶、漏桶、滑动窗口实现”。
- 项目中目前用到令牌桶的功能点主要包括数据库读写操作的流量控制。具体操作为在请求读取数据库之前需要从读令牌桶中获取令牌,获取成功才可读取,写操作同理。
- 借助缓存实现布隆过滤器。项目中目前用到布隆过滤器的功能点主要包括用户黑白名单过滤。
3. 缓存模块
- 对 redisson 获取的分布式锁进行封装,实现自定义锁粒度。
- 项目中目前用到分布式锁的功能点主要包括防止缓存击穿、限流工具的实现、控制访问数据库流量等。
- 以模块、功能点为基础定义缓存的前缀规则。
4. 消息队列模块
- 定义生产者和消费者抽象,定义生产者和消费者工厂。
5. 秒杀模块
5.1 总体流程
核心的秒杀功能大致流程如下:
详细的步骤如下:
5.2 线程派发
线程派发模型的流程如下:
- 把从消息队列接收到的秒杀请求派发到自定义线程池中,便于监控和调整线程池参数。发送秒杀请求后,在固定时间间隔不断轮询,尝试从 DefaultFuture 池中获取结果。
5.3 缓存
如上文中的秒杀流程图所示使用两级缓存,执行秒杀请求时,先在本地缓存中尝试预减库存,如果成功,下一步在分布式缓存中尝试预减库存,如果成功,则开始正式的秒杀流程。
- 缓存预热:在秒杀开始之前,提前将秒杀商品库存加载到缓存中。
- 项目中使用定时线程池每 20 分钟读取一次数据库,获取即将参加秒杀的商品库存并存入缓存中。
- 缓存击穿:在缓存失效(或缓存过期)的情况下,大并发请求缓存中没有但数据库中有的数据。大量请求到达数据库,引起数据库压力瞬间增大。使用互斥锁限制访问数据库请求。缓存失效的时候,线程先去获取锁,获取到锁的线程请求数据库数据,没有得到锁的继续不断重试并检查缓存中是否有了该数据。读取数据库的线程将数据加入到缓存后释放锁。
- 项目中使用互斥锁来避免缓存击穿的功能点主要包括:“预减库存”之前判断库存是否已加载到缓存中,如果未加载,线程请求互斥锁。请求成功的线程则从数据库中加载商品库存,其他请求在此过程中自旋等待。互斥锁粒度设置为 100,对同一商品只会有一个线程访问数据库获得库存。
- 订单缓存:将订单写入数据库之后,将订单存入缓存中,以便用户查询订单状态和订单信息。
- 使用 redis 的 hash 数据结构保存用户的秒杀订单,用户的 ID 作为键,单个商品的 id 作为 hash 结构的 key,订单详情作为 hash 结构的 value。
5.4 消息队列
- 实现抽象生产者和消费者,定义异步发送的回调方法。
- 项目中使用消息队列的功能点包括限流、异步发送秒杀请求、返回秒杀结果。
5.5 其它
- 在流量较大时避免超卖的措施有以下选择,此模块同时使用前三种方法:
- 在减库存的 sql 语句中限制库存大于 0 时才执行减库存操作。
- 数据库秒杀订单表的(用户,商品)联合字段加唯一索引。
- 执行 “减库存 下订单 写入秒杀订单” 事务前设置互斥锁,控制写入数据库流量,只有获取到互斥锁的线程才能开启事务。此方法属于悲观锁操作。
- 为秒杀操作添加版本号。在减库存之前,先检查版本号是否被改变,如果未被改变才能执行。此方法属于乐观锁操作。
- 用户点击购买按钮之前需要输入图片验证码,此功能的目的是分散用户请求。将验证码结果存于缓存中,验证通过开始秒杀流程,验证失败需要再次验证。
- 为每一次秒杀生成随机的秒杀地址,将地址存入缓存中,在执行秒杀之前,验证地址是否正确,地址如果正确则继续执行后面的流程。
6. 用户模块
- 使用 Redis 中的 hash 数据结构缓存用户的基本信息和会话信息。从最后一次登录时间开始算起,过期时间限定为五分钟,会话过期后用户需要重新登陆。
- 缓存一致:修改用户信息时,由于单个用户不存在高并发的情况,所以选择直接修改(不需要先淘汰再写入),同样由于不存在高并发的情况,为了用户可以即时读取最新数据,选择先修改缓存再修改数据。
- 对于缓存的修改可能导致和数据库不一致,所以大多数情况下都是选择缓存淘汰和先修改数据库。此处的特殊情况单独处理。
- 启用黑名单机制实现访问控制:用户请求秒杀地址前过滤访问请求,将用户访问次数存入缓存中。设置单个用户 5 秒内请求达到 5 次,将该用户加入黑名单。黑名单由布隆过滤器实现。在固定时间内,直接拒绝黑名单用户的访问请求,并提示用户“已被加入黑名单”。
- 此方案也是用来避免缓存穿透的有效手段,用户通过恶意请求数据库中没有的数据来攻击系统时也将受到限制。