黑马点评09 秒杀功能总结

 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();
                }
            }
        }

  • 19
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值