每个店铺都可以发布优惠券:
当用户抢购时,就会生成订单并保存到tb_voucher_order这张表中,而订单表如果使用数据库自增ID就存在一些问题:
id的规律性太明显受单表数据量的限制所以tb_voucher_order表的主键不能用自增ID:
全局ID生成器,是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足下列特性:
为了增加ID的安全性,我们可以不直接使用Redis自增的数值,而是拼接一些其它信息:
D的组成部分:
符号位:1bit,永远为0,表示正数时间戳:31bit,以秒为单位,可以使用69年序列号:32bit,秒内的计数器,支持每秒产生2^32个不同ID编写全局ID生成器代码:
测试全局ID生成器:
测试结果:
2. 全局唯一ID生成策略 UUID(不是递增的)Redis自增雪花算法(snowflake)数据库自增(单独建一张表存自增id,分配到分库分表后的表中) 3. Redis自增ID策略 以日期作为前缀的key,方便统计订单量自增ID的结构:时间戳 + 计数器 二、实现优惠券秒杀下单 1. 添加优惠券每个店铺都可以发布优惠券,分为平价券和特价券。平价券可以任意购买,而特价券需要秒杀抢购:
优惠券表信息:
tb_voucher:优惠券的基本信息,优惠金额、使用规则等(tb_voucher表的type字段区分是普通券还是秒杀券)tb_seckill_voucher:优惠券的库存、开始抢购时间,结束抢购时间(秒杀券才需要填写这些信息),同时秒杀券拥有普通券的基本信息(秒杀券表tb_seckill_voucher的主键id绑定的是普通券表tb_voucher的id) 2. 编写添加秒杀券的接口主要代码:
测试添加:
测试结果:
三、实现秒杀下单下单时需要判断两点:
秒杀是否开始或结束,如果尚未开始或已经结束则无法下单库存是否充足,不足则无法下单主要代码:
简单测试秒杀成功:
扣减库存成功:
四、超卖问题当有大量请求同时访问时,就会出现超卖问题
超卖问题是典型的多线程安全问题,针对这一问题的常见解决方案就是加锁:
1. 加锁方式 - 乐观锁乐观锁的关键是判断之前查询得到的数据是否有被修改过,常见的方式有两种:
(1)版本号法
(2)CAS法
用库存代替了版本号,可以少加一个字段扣库存时,与查询时的库存比较,没被修改则可以扣减库存 2. 乐观锁解决超卖问题乐观锁方式,通过CAS判断前后库存是否一致,解决超卖问题:
又出现新的问题:
假设100个线程同时请求,但通过CAS判断后,只有一个线程能扣减库存成功,其余99个线程全部失败此时,库存剩余99,但是实际业务可以满足其余99个线程扣减库存虽然能解决超卖问题,但是设计不合理所以为了解决失败率高的问题,需要进一步改进:
通过CAS 不再 判断前后库存是否一致,而是判断库存是否大于0
3. 小结超卖这样的线程安全问题,解决方案有哪些?(1)悲观锁:添加同步锁,让线程串行执行
优点:简单粗暴缺点:性能一般(2)乐观锁:不加锁,在更新时判断是否有其它线程在修改
优点:性能相对悲观锁好(但是仍然需要同时查数据库,影响性能)缺点:存在成功率低的问题(可以采用分段锁方式提高成功率) 五、一人一单问题需求:修改秒杀业务,要求同一个优惠券,一个用户只能下一单
在扣减库存之前,加上一人一单的逻辑:
此处仍会出现并发问题,当同一用户模拟大量请求同时查询是否下过单时,如果正好都查询出count为0,就会跳过判断继续执行扣减库存的逻辑,此时就会出现一人下多单的问题
解决方法:
由于是判断查询的数据是否存在,而不是像之前判断查询的数据是否修改过所以这里只能加悲观锁 1. 加锁分析 首先将一人一单之后的逻辑全部加锁,所以将一人一单之后的逻辑抽取出一个方法进行加锁,public Result createVoucherOrder(Long voucherId)如果直接在方法上加锁,则锁的是this对象,锁的对象粒度过大,就算是不同的人执行都会阻塞住,影响性能,public synchronized Result createVoucherOrder(Long voucherId)所以将锁的对象改为userId,但是不能直接使用synchronized (userId),因为每次执行Long userId = UserHolder.getUser().getId();虽然值一样,但是对象不同,因此需要这样加锁 synchronized (userId.toString().intern()),intern()表示每次从字符串常量池中获取,这样值相同时,对象也相同为了防止事务还没提交就释放锁的问题,则不能将锁加在createVoucherOrder方法内部,例如:而是需要等事务提交完再释放锁,例如:
2. 事务分析由于只有一人一单之后的逻辑涉及到修改数据库,所以只需对该方法加事务@Transactionalpublic Result createVoucherOrder(Long voucherId)
由于只对createVoucherOrder方法加了事务,而该方法是在seckillVoucher方法中被调用,seckillVoucher方法又没有加事务,为了防止事务失效,则不能直接在seckillVoucher方法调用createVoucherOrder方法,例如:
而是需要通过代理对象调用createVoucherOrder方法,因为@Transactional事务注解的原理是通过获取代理对象执行目标对象的方法,进行AOP操作,所以需要这样:
并且还要引入依赖:
还要开启注解暴露出代理对象:
完整VoucherOrderServiceImpl代码:
六、集群模式下并发安全问题通过加锁可以解决在单机情况下的一人一单安全问题,但是在集群模式下就不行了。
我们将服务启动两份,端口分别为8081和8082:
然后修改nginx的conf目录下的nginx.conf文件,配置反向代理和负载均衡:
修改完后,重新加载nginx配置文件:
现在,用户请求会在这两个节点上负载均衡,再次测试下是否存在线程安全问题:
访问8081端口的线程进入了synchronized中
访问8082端口的线程也进入了synchronized中
最终同一个用户下了2单扣了2个库存,所以在集群模式下,出现了一人多单的问题:
分析:
锁的原理是每个JVM中都有一个Monitor作为锁对象,所以当对象相同时,获取的就是同一把锁但是不同的JVM中的Monitor不同,所以获取的不是同一把锁因此集群模式下,加synchronized锁也会出现并发安全问题,需要加分布式锁到此这篇关于Redis优惠券秒杀企业实战的文章就介绍到这了,更多相关Redis 优惠券秒杀 内容请搜索七叶笔记以前的文章或继续浏览下面的相关文章希望大家以后多多支持七叶笔记!