1.超卖问题
本文章以黑马点评中抢购优惠券这个案例来讲。算是对该项目的秒杀模块的总结,以黑马点评项目为背景,但不限于该项目。
一般情况下,抢购逻辑:
在高并发的情况下,多个线程可能会同时拿到数据库中的值,每个线程减一,但同时写入数据库的话,抢了三件,但实际是数据库中数量减一,这时不可取的。
所以,我们要对数据操作进行加锁:
悲观锁:
一个线程拿到锁,其他线程进行等待,这样可以解决超卖问题,但多个线程阻塞在那里,这显然是不划算的,性能很低
乐观锁:
每次进行数据操作,都带上乐观锁版本号,如果这次修改版本号与一开始获取的版本号不一致,就执行失败。这样不会阻塞,性能更好。
2.一人一单
当有些优惠券只限于一人只能抢一单时,该如何实现?
实现方案:
先查询订单表——如果订单表中有记录,则无法不能继续下单
缺点:
由于是多线程,高并发的情况下,查询操作,和之后下单操作中,有可能在判断订单表时,有其他 线程已经插入了订单,而这时你恰好判断完毕,这时依旧可以抢到优惠卷,从而无法达到一人一单的效果。
解决方案:
对查询订单表,插入订单表行为进行加锁,同时对该业务进行事务封装,保证原子性,注意spring中事务的失效。
Long userId = UserHolder.getUser().getId();
synchronized (userId.toString().intern()) {
//由于这里是当前方法,并没有事务注解,拿到的是当前对象,而事务是要拿到代理对象,通过代理对象实现事务。所以这样做会导致事务失效
return this.createVoucherOrder(voucherId);
// 拿到当前类的代理对象
IVoucherOrderService o = (IVoucherOrderService)AopContext.currentProxy();
return o.createVoucherOrder(voucherId);
}
3.分布式锁
JVM中是通过锁监视器来实现锁机制,但多进程下,每个服务的锁监视器是不一样的,所以在分布式的情况下,我们要想办法让每个进程的锁监视器是一样的,这样就能实现分布式锁
实现方案:
通过redis这个中间件。setnx来实现开闭锁。通过设置过期时间,来避免死锁问题。
分布式锁中的各种极端情况:
1.当业务执行时间超过key过期时间,导致锁过期,其他线程进来由于锁过期,它能继续拿到锁,然而当拿到锁后该线程执行业务时,A线程业务执行好了,删除了锁,但由于A一开始拿到的锁已经过期了,所以现在执行的删除锁操作,删的是其他线程的锁。
解决思路:
在删除锁时进行判断,判断是否是当前线程的锁:
2.当判断完是否是该线程的锁时,恰好锁过期,并且另一个线程介入,此时再执行删除操作,依旧会误删
解决方案:将判断和删除原子化。通过lua脚本实现