先看这样一段代码,购买商品,扣减库存的逻辑代码
当用户下单,并且调用扣减库存的接口时,先判断商品库存是否还有,因为是秒杀场景下,太多请求都打到数据库,可能会导致数据库崩溃,所以会使用redis缓存,保存商品库存数。
由于是微服务架构,为了实现高并发,我们的商品业务代码(包含库存这部分的代码)不可能只部署在一台服务器上,肯定会在多台服务器上都运行。
场景:
假设:当多个请求同时执行到从redis缓存中取库存值的时候,stock=100,判断库存>0成立,然后进行下面的逻辑,当扣减1个库存时,都会把stock=99写回redis缓存的时候,这时,明明卖了2个,但是redis缓存中的数据stock却等于99,就超卖了。
改进:
我们使用同步代码块,使用Java的synchronized锁,只能解决商品业务代码部署在一台部署上的场景,因为synchronized锁是JVM进程级别的锁,对于多台服务器部署的场景,每台服务器都可以获得它的进程的synchronized锁,还是不能解决微服务架构下的超卖问题。
继续改进:
我们使用redis提供的原生命令setnx实现分布式锁(Java客户端jedis提供的方法setIfAbsent和原生命令setnx是一样的),这个锁加在了redis服务器上,一次只有一个请求能获得,当代码执行完成后,释放锁。好像解决了超卖问题,但是如果代码执行到中间时,代码抛出异常了,锁就一直存在在redis中,其他服务永远无法进入redis,就死锁了。
继续改进:
使用try...finally,就算代码中间抛了异常,也能将锁释放掉。
但是当服务器执行到中间,服务器挂掉了,锁还是不会释放,怎么办?
继续改进:
一般会给锁设置一个超时时间,就算服务器挂掉了,redis服务器中的锁还是会自动释放。
但是,当服务器获取了锁之后,正准备设置超时呢,由于是两条命令分开执行,还是可能会突然服务器挂了,是有可能没设置成功超时时间的,这把锁还是永远在redis中
所以一般使用一条命令,设置锁的同时设置超时时间,发送到redis服务器。
继续改进:
这样好像就没啥问题了,但是在高并发场景下,接口的响应可能会变慢。
当一个请求获取到锁时,处理业务时间由于超过了10s,还没有完成扣减库存,锁过了10s,自动释放了,由于高并发场景下,同时有源源不断的请求过来,其他请求就获得了锁,这时,又会导致超卖问题。这时,可能不只是超卖一个两个的问题,第二个请求加锁成功后,如果第一个请求完成了,执行 解锁 操作,会把第二个请求加的锁给释放掉了,其实这时第二个请求还没执行完成,然后请求三又会获得锁,造成一系列的超卖问题。
造成这个问题的原因,就是线程1把线程2的锁给释放掉了,线程2把线程3的锁给释放掉了,锁一直失效。
继续改进:
中小公司,当并发量不是特别大的时候,是没问题的。
但是这段代码仍然存在线程1加的锁,当线程1没执行完成,锁自动超时释放的情况。在高并发场景下,其他请求又可以获得锁,进入扣减库存操作了,一旦重复扣减同一个数字,又造成超卖问题了。
解决方案:
当线程1获得锁之后(假设设置超时时间t = 30s),在当前线程1的代码内部会开辟一个异步线程2,每过10s (1/3*t) 就会检查线程1是否执行完毕,当线程1还没执行完毕,异步线程2就会重新把这个锁的超时时间设置成30s,这个机制叫watchDog。
异步线程2为什么能获得这个锁的使用权?因为redis的分布式锁是可重入锁,线程2在线程1内部,能直接获得redis中的这个锁。
这个watchDog的机制自己实现起来比较复杂,所以推荐使用Redisson分布式锁。
Redisson分布式锁的实现机制就是基于以上这些逻辑。