最近写电商的demo项目遇到一些比较重要的知识点进行分享一下。
高并发库存扣减超卖问题
首先很多人应该会想到乐观锁或者写sql。
这里乐观锁可以采用mybatisplus中的乐观锁,很简单的实现。但是存在ABA问题。
ABA问题:就是说你在扣减库存前,其他线程已经对该商品进行了增加,扣减的操作,当前线程再来进行扣减的操作,这个商品库存已经被修改了,当前线程不知道。
显然这个并不会对这个结果有什么影响。
写sql:就是通过在where条件语句下进行限制,不能小于0,这样扣减的时候,就会产生错误,导致执行失败。
相比之下写sql防止超卖比加版本号性能更高一些。因为减少了比较版本号的操作。
单用户超领问题
超卖我们解决了。但超领的问题还是没有解决。因为,高并发下,不做限制,就会产生一个用户领取了多张同一个优惠券,即使限制了领取张数。也就是做了判断。也会超领。
这里有人可能会想到加锁。
没错,加锁是能解决,但是加什么锁也需要我们思考!
如果加synchronize、lock等这些锁在当前进程内,集群部署下依旧存在问题。
这里就会考虑分布式锁一般由redis、zookeeper等实现。
这里如果考虑性能,大家都会首先想到redis来实现。
redis里面有setnx。它的含义就是SETifNotExists,有两个参数setnx(key,value),该方法是原子性操作
如果key不存在,则设置当前key成功,返回1;
如果当前key已经存在,则设置当前key失败,返回0。
这样的话就可以使:
得到锁的线程执行完任务,需要释放锁,以便其他线程可以进入,调用del(key)
但是
客户端奔溃或者网络中断,资源将会永远被锁住,即死锁,因此需要给key配置过期时间,以保证即使没有被显式释放,这把锁也要在一定时间后自动释放。
也就是说用了setnx 后之后还要用expire设置过期时间。
但是
多个命令之间不是原子性操作,如setnx 和expire之间,如果setnx成功,但是expire失败,且岩机了,则这个资源就是死锁。
java可以通过
stringRedisTemplate.opsForValue().setIfAbsent(key,“1”,30, TimeUnit.SECONDS)
setnx 时直接设置过期时间。
但是
业务超时,存在其他线程误删,key30秒过期,假如线程A执行很慢超过30秒,则key就被释放了,其他线程B就得到了锁,这个时候线程A执行完成,而B还没执行完成,结果就是线程A删除了线程B加的锁
解决方案:
可以在del之前做一个判断,验证当前线程是不是自己加的,那value应该是存当前线程的标识或者uuid。
但是
进一步细化误删(get(key)要花时间不是原子性的)
当线程A获取到正常值时,返回带代码中判断期间锁过期了,线程B刚好重新设置了新值,线程A那边有判断value是自己的标识,然后调用del方法,结果就是删除了新设置的线程B的值”核心还是判断和删除命令不是原子性操作导致
解决方案:
配合lua脚本
将get命令和del命令变为原子性操作!
但是
锁的过期时间,如何实现锁的自动续期或者避免业务执行时间过长,锁过期了?
一般把锁的过期时间设置久一点,比如10分钟时间
这就需要开发者每一个加锁的地方就需要自己去设置续期等操作。就很麻烦。因为需要维护一个线程去检查这个方法执行完没有!
最终解决办法Redisson
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.10.1</version>
</dependency>
@Data
@Configuration
public class Appconfig {
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.port}")
private String redisPort;
@Value("${spring.redis.password}")
private String redisPwd;
@Bean
public RedissonClient redissonClient(){
Config config = new Config();
//单机方式
config.useSingleServer().setPassword(redisPwd).setAddress("redis://"+redisHost+":"+redisPort);
// config.useClusterServers().addNodeAddress();//集群
RedissonClient client = Redisson.create(config);
return client;
}
}
Redis锁的过期时间小于业务的执行时间该如何续期?
redisson里面有个watchdog看门狗机制
负责储存这个分布式锁的Redisson节点岩机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。或者业务执
行时间过长导致锁过期,为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁
的有效期。
Redisson中客户端一旦加锁成功,就会启动一个watchdog看门狗。watchdog是一个后台线程,会每隔10秒检查一下,如果客户端还持有锁key,那么就会不断的延长锁key的生存时间默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改config.lockwatchdogTimeout来另行指定。