优惠券秒杀
全局ID生成器
- 对于商城项目,如果每个生成的订单的id规律性太明显(比如自增),假设第一天在某个时刻下单的订单id为10,第二天同一时刻下单的订单为100,则暴露了过去一天的销售量,因此id不应该遵守规律性
- 同时,当数据量大时,如订单数量随时间达到亿级别时,单张表无法存储这么大量的数据,需要分表,但如果分的每个表的id都是自增的,且各自计算自增,则不满足订单id的唯一性,无法达到业务需求
全局ID格式如下:
其中时间戳解释为:先初始定义一个起始时间(自己设定),然后下单时间与该起始时间做差后的二进制序列就是时间戳,这个时间戳由于有31位,因此可以支持长达69年的时间
序列号解释为:如果在同一时间时刻下单,则时间戳相同,那么可以用序列号自增等策略来唯一标识每一个订单,由于有32位,因此支持很多个不同的ID
具体实现
opsForValue.increment(Key)是对键Key的值进行自增长,在key值中拼接了一个date,这个date是当前日期,方便做统计订单量,同时也可以保证每天的键是不同的(也就是每天生成一个新的缓存,注意这里如果键不存在,会自动创建,因此不用担心空指针异常),又因为每天的订单量实在不可能超过2^32,因此也可以保证不会出现数据错误
最后,进行拼接,而这里可以对时间戳进行左移32位,腾出32位来给序列号(通过或运算来填充)
调用 如下是在并发情形下测试生成id,使用线程池,在测试类中首先设置任务(task生成100个id),然后用线程池,300个线程同时执行该任务(task),则总共生成300*100个id,同时使用latch进行计时,每个任务完成时得到该任务的耗时,同时也输出了300个线程完成的总耗时
添加优惠券
优惠券类:提供优惠券信息,包括库存、生效时间、失效时间三个核心字段
controller(优惠券接口)
service实现类
秒杀优惠券
订单接口信息
实现流程
具体实现
service实现类
库存超卖分析
乐观锁实现原理
乐观锁解决超卖问题
如下是谨慎的做法,只要库存和之前查到的不一致,就秒杀失败,因此失败率很高
如下,判断大于0即可,降低了谨慎度,实际上判断大于0即允许并发,但不允许并发超过实际情况(也就是并发到库存为负数的情况)实际上大部分时间并不能保证只能有一个线程进行秒杀,其他线程如果要秒杀成功,必须还有库存
一人一单
一人一单加锁版
一人一单的问题在于查库存(采用CAS解决了与扣减库存的并发问题,所以主要是后面两个操作的并发问题)、查是否已秒杀过、扣减库存这三个操作不是原子性的,容易因为并发导致问题
因此需要将查询、扣减库存、新增数据提取为一个方法
给用户加锁而不是给方法加锁
优点:一人一单针对的是同一个用户,所以锁范围限制在用户id,如果加在方法上,则对不同的用户都来调用该方法,都会被加锁,导致同一时间只能有一个用户创建订单,性能极低
缺点:给用户加锁,在查询是否已经抢过、扣减库存、新增数据后其实就已经释放锁了,但此时事务提交还需要时间,如果在这段时间内其他用户获得锁了,并去查询,发现还是老数据,同样会出现问题
这里给用户加锁,每个用户调用方法时,都会new一个用户id的字符串(toString底层采用new的方式),所以需要用intern()将字符串转成规范表示,即每次如果要new一个用户id的字符串,会从字符常量池里查找有没有相同值的字符串,如果有就用这个,没有则new
更进一步的说法:给用户加锁,由于用户id是Long类型,synchronized不能给Long类型加锁,只能给引用类型的对象加锁,因此先转为String类型,而又因为每次调用该方法时,String类型都会重新new一个字符串出来,导致性能很低,所以使用intern去字符串常量池里找相同值的字符串,因此不需要每次调用都new一次
为了解决给用户加锁的缺点(查到老数据),同时避免给方法加锁,所以我们考虑在调用方法之前加锁
但调用方法之前加锁又产生一个问题:事务失效
这里实际上调用的是实现类的this.方法,而我们知道事务要生效,实际上是spring拿到了实现类的代理对象(一个对象),而这里的this不是一个对象,而只是类的this方法,所以事务失效
具体解释为:
- Spring AOP(Aspect-Oriented Programming,面向切面编程)机制:
-
- 在 Spring 中,事务是通过 AOP(基于代理的机制)实现的。Spring AOP 创建一个代理对象来处理事务逻辑。当你调用一个被
@Transactional注解的方法时,实际上调用的是这个代理对象,而代理对象负责开启事务、提交事务或回滚事务。 - 当你从外部调用一个有
@Transactional注解的方法时,Spring AOP 能够拦截这个方法的调用,并处理事务。
- 在 Spring 中,事务是通过 AOP(基于代理的机制)实现的。Spring AOP 创建一个代理对象来处理事务逻辑。当你调用一个被
this调用绕过了代理对象:
-
- 当你通过
this调用类的内部方法时,this指的是当前类的实例本身,并没有经过 Spring AOP 创建的代理对象。 - 因此,
this调用跳过了 Spring 代理对象的拦截器链,Spring 没有机会对被@Transactional注解的方法进行事务管理(如开启、提交或回滚事务)。 - 换句话说,Spring 只会为通过代理对象调用的方法处理事务,但不会为直接通过
this调用的内部方法处理事务。
- 当你通过
事务失效的解决方法:
引入依赖
启动类加注解
使用proxy作为代理对象
一人一单在集群模式下存在的问题
多机部署
也就是各机器各自有一个锁监视器,各自监视自己机器上的线程,不同机器的线程仍有可能并发(每个jvm的锁是独立的)
这个问题我们会在下一章使用分布式锁解决