1.整体业务流程
1.1 redis判断流程 (单线程)
1.首先获取订单id和用户id,调用lua脚本进行redis操作,lua内包括 对购买资格/库存充足的判断 、 扣库存下单、发送订单消息到Stream。
2.Stream组成消息队列,有异常自动放到pending-list
1.2线程流程 (多个线程轮流)
1.线程读取消息队列(read消息队列),如果能不能读到消息,就继续读;如果读到了订单消息,就解析消息内容,调用下单函数3写入数据库,然后回复确认ack;如果出现了异常,就去处理penging-list 步骤2。
2.处理penging-list,解析消息,下单,处理;如有异常,重复循环继续处理;直到处理完跳出。
3. 加Redisson分布式锁,调用创建订单数据库操作。
2.解决了哪些问题?
2.1.超卖问题-->乐观锁
通过乐观锁,用库存当做版本号,只要库存大于0,就可以允许下单(一人一单后面有措施解决)
在lua脚本内代码
2.2一人一单->维护set,存储用户id+优惠券id,判重
sismember去判断用户id和优惠券id的记录是否同时存在在集合内
集合结构保证即便用户数量很多,也能最大程度减少重复数量,同时存在判断效率也高
2.3避免lua脚本频繁读取
定义成静态资源,随时取用,不用反复读lua脚本
2.3为什么分布式锁Redisson
分布式锁能够保证在集群模式下,不同的服务器jvm上保持锁的唯一性
后续Redisson使用了hash结构,因为能够保证可重入性
Redisson内部自动解决了2.4和2.5的问题。
2.4(自实现的才有这问题)分布式锁的宕机死锁-->设置过期时间+保证原子性lua脚本
过期时间保证服务宕机之后锁会过期释放
lua脚本保证不会锁还没加过期时间就宕机
Redisson内部自动实现了过期时间和加锁的原子性操作(lua),所以不会分开执行,不会死锁。
2.5(自实现的才有这问题)分布式锁的多线程误删问题-->判断线程标识+保证原子性lua脚本
判断锁的唯一线程标识,不是自己的不删
lua保证原子性,避免id判断和删锁之间的间隙
Redisson有看门狗机制,自动给锁续期,所以不存在锁过期导致别的线程获取锁然后误删的问题。只有服务宕机之后,看门狗机制也停止,才会锁过期,但这不是误删问题(服务阻塞但没有宕机)。
2.6 Redisson可重入、可重试、自动续期
可重入:锁底层是hash结构,hash的名字是,hash的key(field)是线程名字,value是重入次数。
多获取一次锁次数加一,结束一次次数减一,只有次数为0线程才会释放锁。
可重试:发布订阅。订阅锁的消息,一旦其他线程释放了锁,就会发布一个消息通知别人来抢。有一个剩余重试时间waitTime,所有抢锁时间加起来如果超过这个阈值,就会放弃重试,如果还有剩余重试时间,就继续等发布然后抢,等发布的时间就是当前剩余重试时间。如果等不到,就不等了。
自动续期:看门狗,一个函数重置重试时间(默认30s),每次都从30s开始。然后递归,实现无线续期。
3.一些数据结构
3.1 redis里
3.1.1 用于一人一单的set
使用redis的set结构,包含订单id和用户id
redis.call('sismember', orderKey, userId) == 1)
3.1.2 消息队列 Stream
--3.5 发送消息到redis stream队列,xadd stream.orders * k1 v1 k2 v2...... redis.call('xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId)
3.1.3库存Stock 用String计数
redis.call('get', stockKey)) <= 0)
3.2 mysql里
3.2.1 完整订单
持久化进来的订单
3.2.2优惠券和秒杀优惠券
优惠券数据