首先看一段模拟扣减库存的代码:

import lombok.extern.slf4j.Slf4j;
import org.redisson.Redisson;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("test")
@Slf4j
public class TestController {

    @Autowired
    private Redisson redisson;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    //模拟扣减库存业务
    @RequestMapping("deduct_stock")
    public String deductStock(){
        //拿出当前商品的库存
        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
        if(stock>0){
            int realStock = stock-1;
            //减完当前商品的库存之后  将库存值写回去
            stringRedisTemplate.opsForValue().set("stock",realStock+"");
        } else{
            throw new RuntimeException("库存不足,扣减失败");
        }
        return "ens";
    }
}

以上代码符合秒杀的时候扣减Redis中库存的逻辑,但是在高并发的情况下,会有超卖问题出现。首先想到的是使用加锁的方式。

synchronized

首先加入synchronized锁,这个时候有且仅有一个线程能执行同步代码块里面的代码。但是在分布式情况下,还是存在超卖问题。在分布式情况下,synchronized锁不住,因为synchronized只在JVM进程内部有效,只在一个tomcat内部有效,分布式的情况下,项目肯定不是部署在一个tomcat服务器上,所以在分布式环境下,synchronized锁不住。

SETNX

格式:setnx key value

将key的值设定为value,当前仅当key不存在。

若给定的key已经存在,则SETNX不做任何动作。

SETNX是SET if Not exists,如果不存在,则SET。

将上面代码使用SETNX加锁:

但是上面的写法还是存在问题,如果当前线程在执行以上方法的时候出现异常,那么就可能执行不到解锁的语句:

stringRedisTemplate.delete(lockKey);

 这样子的话,下一个线程来访问的时候就拿不到锁,执行不了减库存的代码。那么就继续改造上面的代码:

之前是担心出现异常,导致无法解锁,那么现在使用try{}finally{}捕捉异常,就算出现异常,也一定会解锁。那么再继续考虑,如果当前线程在执行try块中的代码出现宕机,这种情况下finally快中的代码无法执行,还是解不了锁,如何解决?继续改写代码:

给锁设置超时时间,这个样子,就算在执行try块中的代码出现宕机也没关系,因为这把锁的有效时间只有10s,到了这个时间,这把锁就会被Redis清理掉。但是还是有问题:如果这个时候正好执行到下面这条语句:

Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"lavender");

还没来得及给锁设置超时时间,程序就挂了,那么还是解决不了问题,所以希望加锁和设置超时时间是一个原子操作。继续改写以上代码:

使用绿框出的代码就行。但是还会存在问题,可能会出现锁永久失效的问题。看一种场景:1号线程在执行这个方法,并且这个线程需要15s才能执行完,但是上面的代码中锁的有效时间只有10s,意味着当前1号线程在执行到10s后,这把锁就失效了,那么2号线程就会进来加锁,假设2号线程执行完这个方法需要8s,那么当1号线程执行到finally里面的方法时,就会把2号线程加的锁释放掉,那么马上3号线程又进来了。。。以此类推,下一个线程可能一直在删除掉上一个线程加的锁,那么这把锁就会出现永久失效的问题。

继续改写代码:

这个样子就可以保证自己加的锁只能自己解。但是还是存在问题,就是锁的过期时间到底设置多少比较合适,还是上面的场景:假设1号线程执行这段代码需要15s,那么在执行完10s后,锁就失效了,还是其他的线程会进来加锁执行上面的代码,还是存在超卖问题。继续改写,使用Redisson。

Redisson

锁续命:当1号线程加锁成功,执行代码的时候,另外启动一个分线程每隔一段时间来监听1号线程是否还没释放锁,如果还没释放锁以为这1号线程的逻辑代码还没执行完,这个时候分线程会重新设置锁的超时时间,给锁续命,防止出现上面的情况。

想使用Redisson,首先得先在Spring容器中注入Redisson对象:

然后改写之前的代码:

Redisson加锁流程:

Redisson中加锁的关键源码:

大概能看出exists、hset、pexpire这些命令是加锁以及涉及过期时间的,不用担心原子性问题,lua脚本具备原子性。

Redis主从结构锁失效问题(Redlock)

一般实际业务中使用redis是集群架构,而不是单机,集群结构中问题比较多。

假设一种情况:1号线在redis主节点中获得锁,并且去加锁,这个时候redis主节点会立刻返回给1号线程true,表示加锁成功,就可以继续执行扣减库存的逻辑代码。但是当redis主节点向从节点中写入数据的时候,主节点宕机了,这个时候哨兵模式中会选一个从节点作为主节点。那么当2号线程过来的时候,就会往新的主节点中尝试获得锁,毫无疑问这个时候会成功,因为原先主节点中的锁情况还没来得及同步到新的主节点中,那么就会认为之前没有线程获得锁,而真实情况是1号线程已经获得了锁,在它还没有释放之前,其他线程是不能加锁的。这就是redis主从结构锁失效的问题。

还有一种方式可以实现分布式锁:zookeeper,也有主从架构。

CAP理论:C表示一致性、A表示可用性、P表示分区容错性。

Redis集群满足:AP

zookeeper集群满足:CP

Redis中只要在主节点中加锁成功,马上返回给客户端告诉其加锁成功;

zookepper中在主节点中加锁成功之后,不会马上返回给客户端告诉其加锁成功,而是内部要先把数据同步给其它节点,至少集群中要有半数的节点数据同步成功了,才会告诉客户端其加锁成功。

Redlock

以上redis节点没有主从区别,而是平等的节点位置。性能牺牲了,也不一定能解决主从锁失效的问题。

Logo

更多推荐