优惠项目相关
一人一单具体如何实现
本项目采用了 Redis+Lua脚本 、数据库唯一索引 实现一人一单,在用户发起秒杀请求时,首先通过Lua脚本在Redis中原子性判断该用户是否已经下过单(利用 SISMEMBER 判断用户ID是否存在于订单集合中),如果存在则直接返回,防止重复下单。在消息队列消费者( receiveSeckillMessage
)中,落库前再次通过数据库查询是否已存在该用户的订单,防止极端情况下的重复下单。数据库层面在订单表(如 tb_voucher_order )设置了 (user_id, voucher_id) 唯一索引,作为最终防线。
如何保证数据一致性
使用 Cache Aside 策略,先更新数据库,再删除缓存
数据库事务 :在MQ消费者中,扣减库存和保存订单操作放在同一个事务中,保证原子性。
如何保证不超卖
项目通过 Redis+Lua脚本原子操作 和 数据库乐观锁 双重机制防止超卖:
- Redis层 :Lua脚本中先判断库存是否大于0,再进行扣减,整个过程是原子操作,防止并发超卖。
- 数据库层 :在MQ消费者中,扣减库存时使用 where voucher_id = ? and stock > 0 ,只有库存大于0时才会扣减成功,防止极端情况下的超卖(见
receiveSeckillMessage
)。
Redis集群模式下能否保证不超卖
理论上存在风险 。Lua脚本在Redis集群模式下只能保证同一个slot内的key原子性,如果库存key和订单key不在同一个slot,Lua脚本无法跨slot原子执行,可能导致并发下超卖。
- 解决方案 :可以通过hash tag(如 seckill:{voucherId}:stock 和 seckill:{voucherId}:order )保证相关key落在同一个slot,从而保证Lua脚本的原子性。
- 更保险的做法 :在集群环境下,建议引入分布式锁,如Redisson或将关键业务逻辑下沉到数据库层(如乐观锁、唯一索引)作为最终保障。
ZSET数据结构 和 时间窗口
主要目的还是为了防止频繁发送验证码,我没有使用短信而是学生邮箱
每次发送验证码成功时都会将当前的时间作为socre存入zset中,ZSET会根据 score (时间戳)自动排序。
然后判断是否达到限制条件,利用ZSET的 ZCOUNT 命令,可以非常高效地统计指定时间窗口(score范围)内的成员数量。
我主要分为三级限流,一分钟内如果发送过一次就会限制一分钟内无法发送,五分钟内发送五次就会限制五分钟内不能发送,还有一个限制20分钟的。
每次发送验证码前都会判断是否满足某一级的限制条件,如果满足就不再发送验证码并且返回错误信息。
多条件验证的Token自动续期
通过拦截器实现的,从redis中查询token对应的用户是否存在,如果存在就把他的token刷新
但是我发现最初设计的token续期是没有条件的,只要重新进入就会刷新,给redis带来了很多不必要的请求,于是就想着给续期机制加一个降级状态,根据状态去选择续期条件,并且为了安全考虑,决定再加一个续期次数限制,
最初只要刷新就可以重新续期,如果还没有到过期时间的3/4就进行了重新续期,就改为如果过期时间到了3/4才允许续期,如果不到一半就续期,就再次改为1/2
如果总续期次数达到10次就要求重新登陆
本地缓存
我使用Caffeine实现本地缓存
- 本地缓存数据存储在应用服务器的内存中
- 无网络开销,访问速度比Redis快10倍以上
- 减少了Redis的访问压力
利用多级缓存架构,即使Redis宕机,还有本地缓存兜底
在这个商铺查询场景中,使用本地缓存是很好的优化手段,因为商铺信息更新频率较低,而查询频率很高。
秒杀系统的并发安全问题如何解决?
使用Redis+Lua脚本保证原子性操作,Lua脚本在Redis中是一个原子操作,要么全部执行,要么全不执行,库存的检查和扣减都是在lua脚本中完成的,又因为redis是单线程处理请求,就避免了并发安全。我还使用令牌桶算法进行限流,控制每秒的请求数量。使用RabbitMQ异步处理订单:将订单创建操作异步化,减轻系统压力
逻辑过期与互斥锁相比的优势:
- 不会阻塞其他线程的读取操作,即使缓存过期也能返回旧数据
- 重建缓存的过程是异步的,不会影响主业务流程
- 性能更好,没有额外的等待开销
- 适合对数据实时性要求不高的场景
LoadingCache+令牌桶
这里使用了Google Guava的LoadingCache来实现用户级别的限流:
-
每个用户都有独立的限流器
-
限流器会在1小时后过期,避免内存占用过大
-
最多缓存1万个用户的限流器
-
每个用户每秒最多允许5个请求 设置了全局限流器,控制整个系统的总体访问量:
-
系统整体每秒最多处理100个请求
-
防止系统整体被压垮