面对高并发的业务场景,分布式架构成了解决高并发场景的不二之选,高并发情况下会有各种各样意想不到的问题,比如我们今天要探讨的分布式锁的问题,预设一个场景,电商平台要搞优惠活动,拿出一百台浴霸iphone11手机搞秒杀优惠活动,一秒内会有几万甚至几十万的并发访问量,系统肯定会是分布式系统,而java中的同步锁只能作用于单台机器上面,无法对不同物理机器上的jvm做数据同步控制,会出现超卖现象,这时就是分布式锁大显身手的时候了,分布式锁的实现机制有很多,这里讨论一下redis的实现机制。
redis的string类型是个key-value的键值对,我们可以使用redis的setnx命令设置一把分布式锁,每台机器上的多个线程都来使用setnx尝试设置有关iphone11这个key的锁,设置成功后继续下面查询库存,扣减库存的操作,设置不成功的返回抢单失败提示,继续抢单,注意每次操作成功后需要及时释放锁,以便让出锁,让其他线程去获取锁。主要代码如下:
public String deductStock() throws InterruptedException {
String lockKey = "product_iphone11";
//jedis.setnx(key,value);
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"laotian");
if(!result){
return "error";
}
//jedis.get("stock")
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if(stock > 0){
int realStock = stock - 1;
//jedis.set(key,value)
stringRedisTemplate.opsForValue().set("stock",realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock + "");
}else {
System.out.println("扣减失败,库存不足");
}
stringRedisTemplate.delete(lockKey);
return "end";
}
此代码存在问题,可能导致不能及时释放锁,改进如下:
public String deductStock() throws InterruptedException {
String lockKey = "product_iphone11";
try {
//jedis.setnx(key,value);
Boolean result =
stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"laotian");
if(!result){
return "error";
}
//jedis.get("stock")
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if(stock > 0){
int realStock = stock - 1;
//jedis.set(key,value)
stringRedisTemplate.opsForValue().set("stock",realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock + "");
}else {
System.out.println("扣减失败,库存不足");
}
}finally {
stringRedisTemplate.delete(lockKey);
}
return "end";
}
经过上面的改造后,还是无法保证及时释放锁,比如后端服务器正在执行中还没执行到最后删除锁的代码挂掉了,会造成锁不能及时释放,其他线程一直拿不到锁的情况,这时可以考虑使用锁失效时间设置如下:
public String deductStock() throws InterruptedException {
String lockKey = "product_iphone11";
try {
//jedis.setnx(key,value);
//Boolean result =
//stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"laotian");
//stringRedisTemplate.expire(lockKey,30,TimeUnit.SECONDS);
//上面代码中的单独设置lockKey的失效时间不如下面代码一次性设置lockKey的value和失效时间
//上面无法保证操作的原子性
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"laotian",timeout:30,TimeUnit.SECONDS);
if(!result){
return "error";
}
//jedis.get("stock")
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if(stock > 0){
int realStock = stock - 1;
//jedis.set(key,value)
stringRedisTemplate.opsForValue().set("stock",realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock + "");
}else {
System.out.println("扣减失败,库存不足");
}
}finally {
//释放锁
stringRedisTemplate.delete(lockKey);
}
return "end";
}
现在,代码执行情况,线程A获取到锁,由于网络等原因在释放锁之前的执行代码耗费的时间超过了锁的过期时间,锁会被释放掉,但代码还没执行完,并发量大的情况下会有其他线程如线程B获取到锁,并执行相应的代码,但在线程B执行完之前,线程A执行完了逻辑代码,并把锁给释放掉了,其他线程如C又会获取锁,线程之间的相互影响这样会也导致了分布式锁不一致的情况,并最终导致超卖现象,解决办法为将锁的值设为一个随机字符串的,阻止线程之间相互删除锁的问题。代码如下:
public String deductStock() throws InterruptedException {
String lockKey = "product_iphone11";
//设置随机值
String clientId = UUID.randomUUID().toString();
try {
//jedis.setnx(key,value);
//Boolean result =
//stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"laotian");
//stringRedisTemplate.expire(lockKey,30,TimeUnit.SECONDS);
//上面代码中的单独设置lockKey的失效时间不如下面代码一次性设置lockKey的value和失效时间
//上面无法保证操作的原子性
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,clientId,timeout:30,TimeUnit.SECONDS);
if(!result){
return "error";
}
//jedis.get("stock")
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if(stock > 0){
int realStock = stock - 1;
//jedis.set(key,value)
stringRedisTemplate.opsForValue().set("stock",realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock + "");
}else {
System.out.println("扣减失败,库存不足");
}
}finally {
//释放锁
if(clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))){
stringRedisTemplate.delete(lockKey);
}
}
return "end";
}
现在再次翻看代码,还有一个参数的设置存在问题,就是锁的失效时间,失效时间多少算是合理呢?当然是不多不少,正好保证代码逻辑执行成功,再失效,但由于各种意想不到的问题,并不能知道这么好的值,所以我们需要设置一个在正常情况下的代码执行时间的均值作为失效时间,并且需要在代码执行逻辑中起一个子线程,去不断监控代码执行情况,若没执行完,就将失效时间再次设为之前设置的失效时间,知道代码执行逻辑释放锁。可以使用开源客户端redisson,代码如下:
public String deductStock() throws InterruptedException {
String lockKey = "product_iphone11";
//设置随机值
String clientId = UUID.randomUUID().toString();
RLock redisLock = redisson.getLock(lockKey);
try {
//jedis.setnx(key,value);
//Boolean result =
//stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"laotian");
//stringRedisTemplate.expire(lockKey,30,TimeUnit.SECONDS);
//上面代码中的单独设置lockKey的失效时间不如下面代码一次性设置lockKey的value和失效时间
//上面无法保证操作的原子性
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,clientId,timeout:30,TimeUnit.SECONDS);
if(!result){
return "error";
}
redisLock.lock(leaseTime:30,TimeUnit.SECONDS);
//jedis.get("stock")
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if(stock > 0){
int realStock = stock - 1;
//jedis.set(key,value)
stringRedisTemplate.opsForValue().set("stock",realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock + "");
}else {
System.out.println("扣减失败,库存不足");
}
}finally {
//释放锁
redisLock.unlock();
}
return "end";
}
现在我们的分布式锁是不是万事大吉了?当然不是,比如主从复制中的主redis挂掉了,从redis切换成主redis时,可能存在丢失数据的问题,我们的锁丢失问题该怎么解决?
zookeeper可以更好的解决这个问题,我们在以后再谈谈zookeeper作为分布式锁