您现在的位置是:首页 >学无止境 >【Redis】电商项目秒杀问题之超卖问题与一人一单问题网站首页学无止境
【Redis】电商项目秒杀问题之超卖问题与一人一单问题
目录
一、超卖问题
1、背景
在如双11等购物需求剧增的背景下,一个物品库存里有100件但是由于并发等问题可能会导致该物品被卖出超过100件。这就是超卖问题,他是由于库存量被高并发请求而产生的线程安全问题。
2、产生原因以及线程安全问题
当库存仅为1的时候,此时由于并发量高多个线程进行库存查询操作,此时这些线程都查询到库存为1,都各自去进行后续下单扣减库存的操作,此时库存被扣减多次成为负数,产生超卖问题,也就是线程安全问题
3、解决
对于上述超卖的线程安全问题我们可以采用两种方式来解决
1.悲观锁
首先悲观锁的概念是他认为一定会产生线程安全问题所以采用直接加锁的方法,我们此处可以对伤处库存查询于库存扣减操作加锁来处理,但是加锁也要注意防止锁释放了其他线程获取到了锁,但是之前的事务还没有提交,这又会产生新的问题,下面会提到。
2.乐观锁
版本号:
乐观锁的方式我们可以通过给库存来加一个版本号,当线程1与线程2同时读取到库存都为1时,此时版本号也为1,线程1进行了扣减库存操作修改数据库时,加版本号判断的条件:数据库里版本号与上面查询库存时的版本号相同则修改成功,此时线程1修改时数据库中版本号为1,上面查询库存时版本号也为1则修改成功,这个时候线程2也进行扣减库存操作,当他去修改数据库时,发现他在查询库存时的版本号为1,但是数据库中由于线程1已经修改版本号为2,此时线程2则不能扣减成功。
CAS:
我们也可以采用CAS的办法,线程1与2查询库存都为1,此时线程1进行后续操作扣减库存时进行判断,看查询库存时的库存数量与数据库中的库存数量是否相同,如果相同则进行扣减,线程1查询库存为1,修改时数据库中库存也为1,此时线程1扣减成功,这个时候线程2再去进行扣减库存时发现之前查询库存时的库存为1,但是扣减库存时数据库的库存为0,则扣减失败
4、新的问题
上述解决办法虽然解决了超卖的问题但同时又带来了新的问题,加入此时库存还有100,两个线程去进行购买操作时,都读取到库存为100,线程1进行后续扣减操做时发现之前查询的库存100与数据库中100相同 where 库存=100成立,于是扣减成功,下单成功。此时线程2再去执行扣减操作时,发现数据库中库存变为99但是查询时却是100,于是where 库存=100不成立,则产生了库存足够但不同下单的问题
5、解决
此时我们只需要在扣减库存时将原来的where 库存=查询时库存,改为where 库存>0,此时就既可以解决超卖问题,又能解决上述问题
二、一人一单
1、背景
很多时候有些商家要拿出一部分好用且贵重的产品来做促销引流,而将该物品进行低价售出,此时为了防止有人恶意低买高卖以及保证引流的效果,我们要保证一个用户只能买一次,也就一人一单
2、产生原因以及线程安全问题
一人一单的实现步骤是在原来下单逻辑的判断库存是否足够之后去查询数据库看是否该用户的是否已经存在订单,如果存在则不能下单成功,如果不存在则继续下单。由于该物品的特殊性,当开始秒杀时的并发量是极高的,这就会产生这样的问题,多个线程查询库存后判断该用户是否存已经存在订单时,此时这些线程都没有查询到订单信息说明该用户未下过单,让这些线程进行后续下单操作,但是在查询后有线程下单成功了,但是其他线程已经判断为未下过单,还在进行后续的下单操作,这就导致一个用户下单好几次
3、解决
我们可以对上述查询是否下过单与后续的库存扣减操作进行加锁,那么加锁是单纯给方法加吗?如果给方法加给一个用户的线程进来时都需要进行锁竞争,由于这是一个被高并发访问的这就会导致用户响应慢体验差,很多线程进入阻塞等待,所以单纯的给方法加锁是不行的。上述线程安全问题是一个用户的不同线程来并发访问产生的,所以我们可以对用户进行加锁,不同的用户线程是不需要去竞争锁的这样极大的提高了响应速度,我们可以将查询是否下过单与扣减库存封装成一个方法
@Transactionl
public void createVoucherOrder(Long userId) {
synchronized(userId.toString().intern()){
// 查询是否已经下单
// 扣减库存
}
}
在针对用户加锁时,我们需要将用户id转为字符串并存入字符串常量池中,如果不存在字符串常量池中则每次用户的Id在堆中的内存不同则不能达到针对用户加锁的效果
4、新的问题----集群下的并发安全问题
上述加速在很大程度上保证了一人一单的线程安全问题,但是还有一种情况,就是上述我们提到的线程1进行操作后把锁释放了,但是事务还没提交也就是数据库中已下单还没有订单信息,此时线程2获取到了锁,查询数据库中是否存在订单信息时没有查询到,于是又去进行了扣减库存操作,此时该用户一个人又下了多单
5、解决
上述问题的产生原因是锁释放在事务提交之前,所以我们要做的就是保证锁释放在事务提交之后,那么怎么保证呢?我们可以将加锁的位置进行修改,在调用该方法的地方进行加锁即可
public void test(Long userId) {
// 查询库存判断是否足够
// 查询是否已下单与扣减库存
synchronized(userId.toString().intern()){
createVoucherOrder
}
}
@Transactionl
public void createVoucherOrder(Long userId) {
// 查询是否已经下单
// 扣减库存
}
三、集群下的并发问题
1、说明
上述悲观锁解决思路可以解决单机环境下的线程安全问题,但是在集群模式下就不行了,在不同的服务器进行部署该服务时,由于不同服务器有着不同的JVM,其线程锁的监视器也不同,所以加锁不能解决集群环境下的安全问题
2、解决
针对上述集群环境我们可以采用分布式锁的解决方案,在后续的文章会提到。