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优惠券和秒杀优惠券
优惠券数据
4.哪些可以改进(个人想法)
4.1 目前项目使用的是单点redis,为了提高redis并发能力,后面考虑引入redis的集群策略
4.2目前消息队列使用的是redis的Stream类型,如果要提高项目的健壮性,后续可以改用专业的MQ
5.服务集群
用idea自带的功能把项目启动多份模拟集群,用nginx负责反向代理和负载均衡。
nginx会反向代理到你启动的多个节点上。
6.线程池相关的
JAVA 多线程-newSingleThreadExecutor() - 掘金 (juejin.cn)
用的是单个线程的线程池
可以这么说:配合分布式锁保证并发操作不会出错,不用销毁线程。
也可以说是定长,然后设置了xxx个线程
private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();
//刚开始类加载就调用
@PostConstruct
private void init() {
SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
}
//线程功能(去消息队列取消息下订单)
private class VoucherOrderHandler implements Runnable {
//消息队列的名字“orders”
String queueName = "stream.orders";
//取消息下订单
@Override
public void run() {
while (true) {
try {
//1.获取redis消息队列中的订单信息 XREADGROUP group g1 c1 count 1 block 2000 stream.orders >
List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
Consumer.from("g1", "c1"),
StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
StreamOffset.create(queueName, ReadOffset.lastConsumed())
);
//2.判断是否获取成功 没有新消息
if (list == null || list.isEmpty()) {
//获取失败,再来一次
continue;
}
//3.解析消息的订单信息 获取hash,把map里的几个键值对分别对上bean的各个属性和值
MapRecord<String, Object, Object> record = list.get(0);
Map<Object, Object> values = record.getValue();
VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(values, new VoucherOrder(), true);
//3.创建订单存到数据库
handleVoucherOrder(voucherOrder);
//4.ACK确认 SACK stream.orders g1 id
stringRedisTemplate.opsForStream().acknowledge(queueName, "g1", record.getId());
} catch (Exception e) {
log.error("处理订单异常");
handlePendingList();
}
}
}