Redis解决实现秒杀+解决超卖问题

义父们点点赞,球球了,更新不易,qaq

目录

一Redis解决秒杀问题

        1.1解决秒杀思路:直接上图

        1.2乐观锁来解决超卖问题

二、一人一单问题

三、分布式锁

        3.1setnx分布式锁

        3.2锁误删问题

        3.3改进分布式锁,解决锁误删

              3.4redis命令原子性问题

       3.5lua脚本解决命令原子性问题

        3.6 基于setnx实现分布式锁的问题,引出Redission

  四、Redission

        4.1Redission快速入门

4.2Redission的可重入锁原理

4.3Redission锁重试原理

4.4Redission超时释放机制(watchdog 看门狗机制)

4.5Redission主从一直性(连锁 multi lock)

4.6小结


一Redis解决秒杀问题

        1.1解决秒杀思路:直接上图

图一(实现秒杀基本思路)

        注意点:需要为优惠券Id设置唯一性id,且没有明显规律,原因:如果直接使用数据库自增id,可能会造成数据泄露,通过观察可以得到一些店铺的营业数据(比如日常喝奶茶的订单号)

        所以需要为优惠券订单设置唯一id:

        常见方法:1UUID 2雪花算法  3自定义时间戳+序列号

使用时间戳的好处:方便统计每天,每月的订单量,并且有规律,其次业务区分度明显,不同的业务名使用不同的前缀

@Component
@RequiredArgsConstructor
public class RedisIdWorker {
    @Resource
    private   StringRedisTemplate stringRedisTemplate;
    //设置开始时间
    private static final long BEGIN_TIME = 1735689600L;
    //序列号的位数
    private static final int COUNT_BITS = 32;

    public long nextId(String keyPrefix){
        //生成时间戳
        LocalDateTime now = LocalDateTime.now();
        long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
        long timestamp = nowSecond - BEGIN_TIME;
        //生成序列号
        //获取当前日期,精确到天
        String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        //自增长
        long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
        //拼接
        return timestamp << COUNT_BITS |count;
    }


}

        按照图一的逻辑,当并发量较高时,我们很容易看出在扣减库存时会发送超卖风险,导致优惠券超卖。

        解决方式有两种:1sychronized锁,但是此锁影响性能

                                     2 redis乐观锁

这里先介绍两种锁类型:悲观锁:认为线程安全问题一定会发生,所以操作前先获取锁

                                       乐观锁:认为线程安全问题不一定会发生,只有在更新数据时去判断有没有其他线程做了修改

        1.2乐观锁来解决超卖问题

        这里我们使用乐观锁来解决超卖问题:
        CAS法(compare and set ,前面讲过ConcurrentHashMap,底层实现的就是CAS锁)

        我们在减少秒杀券库存时去判断库存是否>0,如果 <0 ,则停止操作,这里展示关键代码

//是,扣减库存,扣减库存前比较库存是否发生改变
        //CAS法compare and set
        boolean sucess = seckillVoucherService.update()
                .setSql("stock = stock - 1")
                .eq("voucher_id", voucherId).gt("stock", 0)
                .update();

即在进行更新操作时,进行条件判断: 当前库存是否 > 0,即是否还有余量

        此时超卖问题即可解决,非常的神奇啊家人们,CAS非常好用的思想,非常nice!!!

        但是此时虽然解决了超卖问题,我们测试时不难发现一人可一直抢购订单,我们并未对每个人抢购优惠券数量做出限制,由此引发下一个问题:一人一单

  

二、一人一单问题

        那解决一人一单问题和前面的思想一致,我们在修改库存前  判断该用户是否下过订单(根据用户的优惠券id和订单id),并且为该用户id加上(Synchronized)锁,保证每个用户只能获取一单并且不影响其他用户抢购。

图二(实现一人一单)

     核心代码:

Long userId = UserHolder.getUser().getId();
synchronized (userId.toString().intern()) {
    IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
    return proxy.createVoucherOrder(voucherId);
}

        此时我们发现在单机情况下能够解决一人一单问题,但是在集群模式下依旧有问题?为什么呢,因为JVM是通过锁监视器来进行监控的,不同的jvm并不能监控同一个线程,这个时候就需要分布式锁了

图四(引出分布式锁)

三、分布式锁

        3.1setnx分布式锁

        我们来看看redis分布式锁的解决思路:利用setnx(只有当该key不存在时才能常见,互斥性)命令,为每个线程生成唯一的一把锁,并且为了防止锁超时未删除,我们设置一个ttl自动过期时间

        这里给出获取锁的代码,个人觉得这块代码比较好值得学习

//使用BooleanUtil判断,
//避免Boolean自动拆箱成boolean时发生空指针异常

        3.2锁误删问题

图五(锁误删)

        不难看出当线程一获取锁业务阻塞后,此时锁超时自动释放锁(锁被误删),而线程一继续执行业务时,会继续释放锁从而删除其他线程的锁,因为我们的key值为 业务名+ “order:userId”,所以会出现并行情况,造成线程安全问题。

        3.3改进分布式锁,解决锁误删

        改进的方式非常简单,在我们释放锁之前进行判断,这是否是我们当前线程锁获取的那把锁,如果不是,则说明锁被超时释放了,无需再次删除

              3.4redis命令原子性问题

此时便不会造成锁被误删问题,因为我们会进行判定,但是此时我们违反了分布式锁的原子性问题,即我们在判断锁是否一致之后,线程发送阻塞(比如服务直接挂了概率非常小),发生新的线程安全问题,如下图所示

图六(分布式锁的原子性问题)

       3.5lua脚本解决命令原子性问题

        所以我们要让判断和释放锁同时成功或失败,使用lua脚本编写,解决redis命令的原子性问题

图七(lua脚本解决redis命令原子性问题)

即如果 锁中的线程标识等于当前线程标识才进行释放锁

图八(调用lua脚本api)

        到此,我们基于redis的set nx命令解决一人一单问题到此结束,实现的过程非常麻烦,而且仍然存在许多问题,直接上工具

        恭喜前面全部白雪!!!白雪!!!白雪!!!!(开个玩笑,其实我们已经深入了解锁的原理)

        3.6 基于setnx实现分布式锁的问题,引出Redission

图九。setnx命令实现分布式锁的问题

  四、Redission

        4.1Redission快速入门

<!--        redission-->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.13.6</version>
        </dependency>


@Configuration
public class RedisConfig {
    @Bean
    public RedissonClient redissonClient(){
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.6.128:6379");
        return Redisson.create(config);
    }
}

4.2Redission的可重入锁原理

        利用hash结构替代string结构(记录获取锁的线程和重入次数),hash key-field-value,多存入一个value字段,用来记录获取锁的次数,所以当某一线程进行业务操作时(不止需要一次锁),可以多次获取锁,只有当value= 0时,锁才会被删除

4.3Redission锁重试原理

        Redission利用订阅+信号量的功能实现等待,唤醒 获取锁失败的重试机制

        即当某线程获取锁失败时,会先判断有没有剩余时间可用,如果有则会进行等待,直到接收到锁释放的信号再进行重试

4.4Redission超时释放机制(watchdog 看门狗机制)

        当我们不主动设置锁的主动失效时间时,会触发Redission的看门狗机制(leaseTime=-1),Redission会利用watchdog,每隔一段时间(releaseTime/3),重置超时时间。renewExpiration()(一直循环,永不过期)直到释放锁,unlockAsync()会执行其中的cancelExpirationRenewal(threadId)

4.5Redission主从一直性(连锁 multi lock)

        什么是主从一致,即redis集群搭建后会选择一台服务器为RedisMaster,只处理写,其他服务器redis slave只处理读,所以主机需要不停地把信息同步给其他服务器。当主机挂掉后,服务会从从机选出一台作为新的主机,此时之前主机中的锁会丢失,造成线程安全问题。

4.6小结

        直接给图,困了要睡觉了家人们,八股文今天看了但是来不及更新了,写博客写太久了,只能先把今天学的给更了。义父们点点赞,球球了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值