redis分布式锁

单机体系的锁


 

源码在这

git clone https://gitee.com/nineidei/redisDeom.git

使用ReentrantLock作为锁

两个模块中的代码一致

nginx的配置

在我源码中可能端口号发生变化,所以请大家按照实际情况来处理

图1

使用Jmeter模拟高并发,通过nginx转发到两个模块的接口,最后查看redis中设置的商品库存量和控制台打印的日志

能明显看到,商品在我100个并发进来之后,明明应该是0的,此时还剩12个

观察两个模块的打印信息,发现重复消费了,我们加的lock锁没有生效,正如图1中lock锁只能针对自己的JVM上锁,如果是另一个JVM则无法管理

分布式体系下的锁

第一版

原理

在redis中使用set , get 的方式来加锁,set RedisLock xxx 之后,如果再次get RedisLock就会查到xxx的信息,等拿到redis锁的线程执行完流程之后就delete RedisLock解锁,递归的线程会再次尝试set RedisLock xxx

如图

代码如下

@RequestMapping("/sale")
    public String sale(){
        String retMessage = "";
        Object key = "RedisLock";
        Object value = UUID.randomUUID() + ":" + Thread.currentThread().getId();
        Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, value);
        //如果flag为false,就代表加锁失败,需要重新请求
        if (!flag){
            //为了减轻服务器压力,延迟20ms
            try {
                TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {throw new RuntimeException(e);}
            //然后重新请求
            sale();
        }else {
            //抢到了就正常的流程
            try{
                //1 查询库存余量
                String result = redisTemplate.opsForValue().get("inventory001").toString();
                //2判断库存是否足够
                Integer inventoryNum = result == null ? 0 : Integer.parseInt(result);
                //3 扣库存,每次减少一个
                if (inventoryNum != 0){
                    redisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNum));
                    retMessage = "成功卖出商品,库存剩余:" + inventoryNum;
                    System.out.println(inventoryNum);
                }else {
                    retMessage = "商品卖完了!";
                }

            }finally {
                //这里解锁的操作是将redis中的key值删除,其他线程进来set key的就能正常set了
                redisTemplate.delete(key);
            }
        }

        return retMessage;
    }

在大量并发下,也仅仅只能在单机模式下使用,如果是分布式下依旧会出现超卖现象

问题:

在高并发环境下,是严禁使用递归的,因为容易造成堆栈溢出

解决:

使用whlie代替if,使用自旋的方式代替递归重试

第二版

使用whlie代替if,使用自旋的方式代替递归重试

    @RequestMapping("/sale")
    public String sale(){
        String retMessage = "";
        Object key = "RedisLock";
        Object value = UUID.randomUUID() + ":" + Thread.currentThread().getId();
        //Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, value);
        //如果是true,证明抢到锁,这里去反表示抢到锁就不用进while循环了,没抢到锁flag为false,false取反,为true,进入while循环
        //使用while就不会方法调用方法,也就是sale()调用sale()导致堆栈溢出
        while(!redisTemplate.opsForValue().setIfAbsent(key, value)){
            try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {throw new RuntimeException(e);}
        }
        //如果抢到锁,就走正常逻辑
        try{
            //1 查询库存余量
            String result = redisTemplate.opsForValue().get("inventory001").toString();
            //2判断库存是否足够
            Integer inventoryNum = result == null ? 0 : Integer.parseInt(result);
            //3 扣库存,每次减少一个
            if (inventoryNum != 0){
                redisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNum));
                retMessage = "成功卖出商品,库存剩余:" + inventoryNum;
                System.out.println(inventoryNum);
            }else {
                retMessage = "商品卖完了!";
            }

        }finally {
            //这里解锁的操作是将redis中的key值删除,其他线程进来set key的就能正常set了
            redisTemplate.delete(key);
        }
        return retMessage;
}

原理

将判断直接放入while中,这样做就可以重复多次去访问redis,而不需要通过方法调方法来判断能否抢到锁,且不会造成堆栈溢出的问题

问题:

如果在while代码中发生了异常,走不到finally中,岂不是永远都无法解锁了?如果其他用户访问,将会一直陷入while循环中

解决:

每次加锁的时候赋予上一个过期时间,即便是报错了,时间一到也会自动解锁

第三版

原理

每次加锁的时候赋予上一个过期时间

   @RequestMapping("/sale")
    public String sale(){
        String retMessage = "";
        Object key = "RedisLock";
        Object value = UUID.randomUUID() + ":" + Thread.currentThread().getId();
        //Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, value);
        //如果是true,证明抢到锁,这里去反表示抢到锁就不用进while循环了,没抢到锁flag为false,false取反,为true,进入while循环
        //使用while就不会方法调用方法,也就是sale()调用sale()导致堆栈溢出
        while(!redisTemplate.opsForValue().setIfAbsent(key, value)){
            try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {throw new RuntimeException(e);}
        }
        //如果建锁成功,马上加上过期时间
        redisTemplate.expire(key,30L,TimeUnit.SECONDS);
        //如果抢到锁,就走正常逻辑
        try{
            //1 查询库存余量
            String result = redisTemplate.opsForValue().get("inventory001").toString();
            //2判断库存是否足够
            Integer inventoryNum = result == null ? 0 : Integer.parseInt(result);
            //3 扣库存,每次减少一个
            if (inventoryNum != 0){
                redisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNum));
                retMessage = "成功卖出商品,库存剩余:" + inventoryNum;
                System.out.println(inventoryNum);
            }else {
                retMessage = "商品卖完了!";
            }

        }finally {
            //这里解锁的操作是将redis中的key值删除,其他线程进来set key的就能正常set了
            redisTemplate.delete(key);
        }
        return retMessage;
}

问题:

加锁和赋予key的过期时间不是原子性的,如果在高并发下加锁后立马报错,没有走到赋予时间这个方法,依旧会导致第二版的问题

解决:

将两行代码合并成一行,形成原子性操作

  @RequestMapping("/sale")
    public String sale(){
        String retMessage = "";
        Object key = "RedisLock";
        Object value = UUID.randomUUID() + ":" + Thread.currentThread().getId();
        //Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, value);
        //如果是true,证明抢到锁,这里去反表示抢到锁就不用进while循环了,没抢到锁flag为false,false取反,为true,进入while循环
        //加锁和赋予过期时间必须是同一行,保证原子性
        while(!redisTemplate.opsForValue().setIfAbsent(key, value,30L,TimeUnit.SECONDS)){
            try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {throw new RuntimeException(e);}
        }
        //如果建锁成功,马上加上过期时间
        //redisTemplate.expire(key,30L,TimeUnit.SECONDS);
        //如果抢到锁,就走正常逻辑
        try{
            //1 查询库存余量
            String result = redisTemplate.opsForValue().get("inventory001").toString();
            //2判断库存是否足够
            Integer inventoryNum = result == null ? 0 : Integer.parseInt(result);
            //3 扣库存,每次减少一个
            if (inventoryNum != 0){
                redisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNum));
                retMessage = "成功卖出商品,库存剩余:" + inventoryNum;
                System.out.println(inventoryNum);
            }else {
                retMessage = "商品卖完了!";
            }

        }finally {
            //这里解锁的操作是将redis中的key值删除,其他线程进来set key的就能正常set了
            redisTemplate.delete(key);
        }
        return retMessage;
}

第四版

原理

将两行代码合并成一行,形成原子性操作

问题:

实际业务处理时间如果超过了默认设置的过期时间,会删除其他人的锁

解决:

只允许自己删除自己的锁,不允许删除其他人的

    @RequestMapping("/sale")
    public String sale(){
        String retMessage = "";
        Object key = "RedisLock";
        Object value = UUID.randomUUID() + ":" + Thread.currentThread().getId();
        //Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, value);
        //如果是true,证明抢到锁,这里去反表示抢到锁就不用进while循环了,没抢到锁flag为false,false取反,为true,进入while循环
        //使用while就不会方法调用方法,也就是sale()调用sale()导致堆栈溢出
        while(!redisTemplate.opsForValue().setIfAbsent(key, value,30L,TimeUnit.SECONDS)){
            try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {throw new RuntimeException(e);}
        }
        //如果建锁成功,马上加上过期时间
        //redisTemplate.expire(key,30L,TimeUnit.SECONDS);
        //如果抢到锁,就走正常逻辑
        try{
            //1 查询库存余量
            String result = redisTemplate.opsForValue().get("inventory001").toString();
            //2判断库存是否足够
            Integer inventoryNum = result == null ? 0 : Integer.parseInt(result);
            //3 扣库存,每次减少一个
            if (inventoryNum != 0){
                redisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNum));
                retMessage = "成功卖出商品,库存剩余:" + inventoryNum;
                System.out.println(inventoryNum);
            }else {
                retMessage = "商品卖完了!";
            }

        }finally {
            //改进点,只能删除自己的key,不能删除他人的
            if (redisTemplate.opsForValue().get(key).toString().equalsIgnoreCase(value.toString())){
                redisTemplate.delete(key);  
            }

        }
        return retMessage;
}

第五版

原理

在删除自己key的同时做一个判断,查看是否是自己的key,否则不能删除

问题:

finally代码块中的del方法不是原子性的

但是,如果代码走到if中,去查询这个key突然发生问题,停在了if,这个时候无法走到delet删除这个key,也就是说必须是原子性的操作,在if中查的同时,如果判断为true就立马删除

解决:

使用lua脚本作为502粘合剂,将if中的判断和del粘合起来~

什么是lua脚本?

这里简单使用lua脚本进行几个案例的实操

这是最简单的lua脚本入门

EVAL命令就是调一段脚本,“return”就表示我需要返回的什么东西,我这里就是'hello lua'

如果我想把这三个指令写成一个应该如何写呢?使用lua脚本实现

redis.call() : 只要我们想通过lua脚本调用redis中的命令,就需要使用这种方式

那么该如何使用动态传参的方式改写这个lua脚本呢?

不知道各位有没有看到后面numkeys的传参,如果我在后面加上0,就表示什么都不用传

如果是2,就表示有两个key和两个val,通过KEYS[1],和ARGV[1]获取我们需要传入的参数

再进一步学习Lua脚本

这次加上if和else判断,这是官网的例子,我们改造一下

EVAL "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end" 1 redisLock 111112222

 进行get(key)的判断,如果等于我传入的value值那么就删除,否则删不了

那么根据Lua脚本,我们将finally代码块中的查询以及删除操作整合为原子性操作

    @RequestMapping("/sale")
    public String sale(){
        String retMessage = "";
        Object key = "RedisLock";
        Object value = UUID.randomUUID() + ":" + Thread.currentThread().getId();
        //Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, value);
        //如果是true,证明抢到锁,这里去反表示抢到锁就不用进while循环了,没抢到锁flag为false,false取反,为true,进入while循环
        //使用while就不会方法调用方法,也就是sale()调用sale()导致堆栈溢出
        while(!redisTemplate.opsForValue().setIfAbsent(key, value,30L,TimeUnit.SECONDS)){
            try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {throw new RuntimeException(e);}
        }
        //如果建锁成功,马上加上过期时间
        //redisTemplate.expire(key,30L,TimeUnit.SECONDS);
        //如果抢到锁,就走正常逻辑
        try{
            //1 查询库存余量
            String result = redisTemplate.opsForValue().get("inventory001").toString();
            //2判断库存是否足够
            Integer inventoryNum = result == null ? 0 : Integer.parseInt(result);
            //3 扣库存,每次减少一个
            if (inventoryNum != 0){
                redisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNum));
                retMessage = "成功卖出商品,库存剩余:" + inventoryNum;
                System.out.println(inventoryNum);
            }else {
                retMessage = "商品卖完了!";
            }

        }finally {

            //改进点,修改Lua脚本的redis分布式锁调用,必须保证原子性
            String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1] then " +
                    "return redis.call('del' , KEYS[1])" +
                    "else " +
                    "return 0 " +
                    "end";
            redisTemplate.execute(new DefaultRedisScript(luaScript,Boolean.class), Arrays.asList(key),value);

        }
        return retMessage;
    }

代码写到这里,对于一些小的自研已经是够用了,但是针对大厂还是有不足的地方

还需要设计成可重入锁以及增加锁的自动续期功能

第六版:

如何写好一个锁

一个靠谱的分布式锁需要具备:

独占性

高可用

防死锁:必须要加过期时间,如果java宕机了,redis那边到期自动消失

不乱抢:使用Lua脚本,查询redis中自己占有的锁,然后删除这一系列操作的原子性

重入性:只要是自己持有自己的锁,再进去的话,就不用重新申请一把锁,相当于一把锁能开所有自己权限内所有的门,而不是每到一个门就要用一个新的锁,从而造成锁套锁

可以想象成回家之后进大门需要一把锁,只要我进入了大门,那么其他房间的门我就都能进了,而不是说卧室也要像大门一样再新建一个防盗门,这就是可重入性

一句话:同一个线程中多个流程可以获得同一把锁,持有这把同步锁的线程能再次进入。

目前有两个分支,目的是保证同一时期,只能有一个线程持有锁进入redis做库存扣减的动作

一 保证加锁解锁,lock/unlock

二 扣减库存redis命令的原子性

第一步:先判断锁是否存在 (EXISTS key)

第二步:不存在就说明hset新建当前线程属于自己的锁BY UUID:ThreadID

HSET RedisLock 5hk26g3jh7fgh36f6g26gk2l:1  1

RedisLock为key   

5hk26g3jh7fgh36f6g26gk2l:1 为value

1为可重入次数

如果返回 1 就说明已经有锁,需要进一步判断是不是当前线程自己的

EXISTS RedisLock 5hk26g3jh7fgh36f6g26gk2l:1
返回0表示不是自己的
返回1说明是自己的锁,自增1次表示重入
HINCRBY RedisLock 5hk26g3jh7fgh36f6g26gk2l:1  1
//v1
//加锁脚本
if redis.call('exists','key') == 0 then
    redis.call('hset','key','uuid:threadID') == 1
    redis.call('expire','key',50)
    return 1
elseif redis.call('hexists','key','uuid:threadID') == 1 then
    redis.call('hincrby','key','uuid:threadID',1)
    redis.call('expire','key',50)
    return 1
else
    return 0
end


//先查key值,如果为0就表示当前没有锁,就根据当前线程创建一个锁,并且将锁的存活时间这是为50s
//如果存在就查询这个key值以及当前线程是否返回1,如果返回1就证明是当前线程持有的锁,在1的基础上加1,表示重入一次,然后将锁的过期时间设置为50s
//如果到最后的else中,就表示不是自己的锁,返回0,接着走while循环抢锁

 还可以优化一下

//使用hincrby代替hset
if redis.call('exists','key') == 0 or redis.call('hexists','key','uuid:threadID') == 1 then
    redis.call('hincrby','key','uuid:threadID',1)
    redis.call('expire','key',50)
    return 1
else
    return 0
end

 再优化一下,将参数换上去

if redis.call('exists','key') == 0 or redis.call('hexists','key','uuid:threadID') == 1 then
    redis.call('hincrby','key','uuid:threadID',1)
    redis.call('expire','key',50)
    return 1
else
    return 0
end



//key  -->> KEYS[1]  -->> RedisLock   
//value  -->> ARGV[1]  -->> 4j32lj42hghjgbs45sf6:1
//过期时间值  -->> ARGV[2]  -->> 30 s


if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then
    redis.call('hincrby',KEYS[1],ARGV[1],1)
    redis.call('expire',KEYS[1],ARGV[2])
    return 1
else
    return 0
end

然后放到redis中测试下

同时也满足了可重入的要求

解锁Lua脚本

if redis.call('hexists',key,uuid:threadId) == 0 then
    return nil
elseif redis.call('hincrby',key,uuid:threadId,-1) == 0 then
    return redis.call('del',key)
else
    return 0
end

优化一下

if redis.call('hexists',KEYS[1],ARGV[1]) == 0 then
    return nil
elseif redis.call('hincrby',KEYS[1],ARGV[1],-1) == 0 then
    return redis.call('del',KEYS[1])
else
    return 0
end

 java微服务整合lua脚本加锁解锁

@RestController
public class HelloController {

    @Resource
    private StringRedisTemplate redisTemplate;
    
    Lock myRedisLock = new RedisDistributedLock(redisTemplate,"RedisLock");

    @RequestMapping("/sale")
    public String sale(){
        String retMessage = "";
        myRedisLock.lock();
        try{
            //1 查询库存余量
            String result = redisTemplate.opsForValue().get("inventory001").toString();
            //2判断库存是否足够
            Integer inventoryNum = result == null ? 0 : Integer.parseInt(result);
            //3 扣库存,每次减少一个
            if (inventoryNum != 0){
                redisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNum));
                retMessage = "成功卖出商品,库存剩余:" + inventoryNum;
                System.out.println(inventoryNum);
            }else {
                retMessage = "商品卖完了!";
            }



        }finally {
            myRedisLock.unlock();
        }
        return retMessage;
    }
}
//自研的redis分布式锁
public class RedisDistributedLock implements Lock {
    @Autowired
    private StringRedisTemplate redisTemplate;



    private String lockName;//KEYS[1]
    private String uuidValue;//ARGV[1]
    private Long expireTime;//ARGV[2]

    public RedisDistributedLock(StringRedisTemplate redisTemplate, String lockName) {
        this.redisTemplate = redisTemplate;
        this.lockName = lockName;
        this.uuidValue = UUID.randomUUID() + ":" +Thread.currentThread().getId();
        this.expireTime = 50L;
    }

    @Override
    public void lock() {
        tryLock();
    }

    @Override
    public void unlock() {
        String strict =
                "if redis.call('hexists',KEYS[1],ARGV[1]) == 0 then " +
                    "return nil " +
                "elseif redis.call('hincrby',KEYS[1],ARGV[1],-1) == 0 then " +
                    "return redis.call('del',KEYS[1]) " +
                "else return 0 " +
                "end";

        Long flag = redisTemplate.execute(new DefaultRedisScript<>(strict, Long.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime));
        if (null == flag){
            throw new RuntimeException("this lock doesn't exists!!!");
        }

    }


    @Override
    public boolean tryLock() {
        try {
            tryLock(-1L,TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        if (time == -1L){
            String strict =
                    "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then " +
                        "redis.call('hincrby',KEYS[1],ARGV[1],1) redis.call('expire',KEYS[1],ARGV[2]) " +
                        "return 1 " +
                    "else " +
                        "return 0 " +
                    "end";

            while (!redisTemplate.execute(new DefaultRedisScript<>(strict,Boolean.class),Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))){
                //如果加锁失败,等待60s再次加锁
                try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {throw new RuntimeException(e);}
            }
            return true;
        }

        return false;
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }


    @Override
    public Condition newCondition() {
        return null;
    }
}

通过我自己创建的RedisDistributedLock实现Lock接口,重写lockunlocktryLock方法,使用Lua脚本的方式实现原子性操作,并且保证可重入性,以及以自旋的方式加锁,但是依旧有缺陷,因为我们把redis获得锁写死了,假如以后有zookeepermysql做分布式锁,还需要再加入工厂模式

为什么使用工厂模式?

1. 如果我想要一条狗,我是不是得自己new一个?

Dog dog  =  new dog();

这样不就写死了吗,没有任何通用性,只能得到一条狗

2. 利用多态

创造一个Animal a = new 鸡(); 鸭(); 鱼(); 狗(); 

左边是接口,右边是具体的实体类,经典的就是

List list = new ArrayList(); 

List list = new LinkList();等等

3. 我们希望左边是固定的,右边是动态的。多态+动态,提交给spring容器管理,或者池化技术

4. 设计模式,通过工厂模式,直接通过传参从工厂获得

5. 设计一个分布式锁的工厂 

@Component  //纳入spring容器管理
public class RedisDistributedLockFactory {

    private String lockName;
    @Autowired
    private StringRedisTemplate redisTemplate;

    public Lock getDistributedLock(String lockType){
        if (lockType== null){
            return null;
        }

        if (lockType.equalsIgnoreCase("REDIS")){
            this.lockName = "RedisLock";
            return new RedisDistributedLock(redisTemplate,lockName);
        }

        if (lockType.equalsIgnoreCase("ZOOKEEPER")){
            this.lockName = "ZookeeperLockNode";
            //TODO
        }

        if (lockType.equalsIgnoreCase("MYSQL")){
            this.lockName = "MysqlLock";
            //TODO
        }
        return null;
    }
}
@RequestMapping("/sale")
    public String sale(){
        String retMessage = "";
        //这里使用工厂拿到redis分布式锁
        Lock myRedisLock = lockFactory.getDistributedLock("REDIS");
        myRedisLock.lock();
        try{
            //1 查询库存余量
            String result = redisTemplate.opsForValue().get("inventory001").toString();
            //2判断库存是否足够
            Integer inventoryNum = result == null ? 0 : Integer.parseInt(result);
            //3 扣库存,每次减少一个
            if (inventoryNum != 0){
                redisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNum));
                retMessage = "成功卖出商品,库存剩余:" + inventoryNum;
                System.out.println(inventoryNum);
            }else {
                retMessage = "商品卖完了!";
            }



        }finally {
            myRedisLock.unlock();
        }
        return retMessage;
    }

测试可重入性

 @RequestMapping("/sale")
    public String sale(){
        String retMessage = "";
        Lock myRedisLock = lockFactory.getDistributedLock("REDIS");
        myRedisLock.lock();
        try{
            
            testReEntry();

        }finally {
            myRedisLock.unlock();
        }
        return retMessage;
    }


    private void testReEntry(){
        Lock redisLock = lockFactory.getDistributedLock("REDIS");

        redisLock.lock();
        try {
            System.out.println("=========测试可重入锁============");
        }finally {
            redisLock.unlock();
        }

    }



 出现了bug

 

 这是因为工厂模式导致的,每一次都是new过来的,每new一次,线程id就变一次

 

@Component  //纳入spring容器管理
public class RedisDistributedLockFactory {

    private String lockName;
    private String uuid;

    public RedisDistributedLockFactory() {
        this.uuid = UUID.randomUUID().toString();
    }

    @Autowired
    private StringRedisTemplate redisTemplate;

    public Lock getDistributedLock(String lockType){
        if (lockType== null){
            return null;
        }

        if (lockType.equalsIgnoreCase("REDIS")){
            this.lockName = "RedisLock";
            return new RedisDistributedLock(redisTemplate,lockName,uuid);
        }

        if (lockType.equalsIgnoreCase("ZOOKEEPER")){
            this.lockName = "ZookeeperLockNode";
            //TODO
        }

        if (lockType.equalsIgnoreCase("MYSQL")){
            this.lockName = "MysqlLock";
            //TODO
        }
        return null;
    }
}

 

测试成功!!! 

第七版

增加自动续期功能

if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 1 then
    return redis.call('expire',KEYS[1],ARGV[2])
else 
    return 0
end

 

 将lua脚本放入代码中

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        if (time == -1L){
            String strict =
                    "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then " +
                        "redis.call('hincrby',KEYS[1],ARGV[1],1) redis.call('expire',KEYS[1],ARGV[2]) " +
                        "return 1 " +
                    "else " +
                        "return 0 " +
                    "end";
            System.out.println("加锁 - lockName :" + lockName +"\t" +"uuidValue" + uuidValue);
            while (!redisTemplate.execute(new DefaultRedisScript<>(strict,Boolean.class),Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))){
                //如果加锁失败,等待60s再次加锁
                try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {throw new RuntimeException(e);}
                renewExpire();
            }
            return true;
        }

        return false;
    }




    private void renewExpire() {
        String script =
                "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 1 then " +
                        "return redis.call('expire',KEYS[1],ARGV[2]) " +
                        "else " +
                        "return 0 " +
                        "end";
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                if (redisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))) {
                    renewExpire();
                }
            }
        },((this.expireTime * 1000) /3));
    }

总结一下

redis分布式锁我们需要考虑:

一. 宕机与过期

二. 防死锁

三. 防止误删key

四. Lua脚本保证原子性

五. 可重入锁(使用hset ,hash类型来处理)

六. 锁自动续期

lock() 加锁的关键逻辑

加锁:实际上就是在redis中,给Key设置一个值,为避免死锁,给一个过期时间

自旋

续期

unlock() 加锁的关键逻辑

考虑到可重入性的递减,加锁几次就要解锁几次

最后到0了,直接del

Redisson分布式锁的使用

RedisConfig配置类

@Configuration
public class RedisConfig {

   

    @Bean
    public Redisson redisson(){
        Config config = new Config();
        config.useSingleServer()
                // use "rediss://" for SSL connection
                .setAddress("redis://127.0.0.1:6379").setDatabase(0);
        return (Redisson) Redisson.create(config);
    }
}

为了方便直接在控制层写出来

    @Autowired
    private Redisson redisson;

    //v9.0 引入官网Redisson对应的官网推荐RedLock算法实现类
    @RequestMapping("/saleNew")
    public String saleNew(){
        String retMessage = "";
        RLock lock = redisson.getLock("RedisLock");
        lock.lock();
        try{
            //1 查询库存余量
            String result = redisTemplate.opsForValue().get("inventory001").toString();
            //2判断库存是否足够
            Integer inventoryNum = result == null ? 0 : Integer.parseInt(result);
            //3 扣库存,每次减少一个
            if (inventoryNum != 0){
                redisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNum));
                retMessage = "成功卖出商品,库存剩余:" + inventoryNum;
                System.out.println(inventoryNum);
            }else {
                retMessage = "商品卖完了!";
            }

        }finally {
            //改进点,只能删除属于自己的key,不能删除别人的
            if (lock.isLocked() && lock.isHeldByCurrentThread()){
                lock.unlock();
            }

        }
        return retMessage;
    }

Redisson简单好用,更多关于Redisson的使用说明可以去官网
​​​​​​​https://github.com/redisson/redisson#quick-start

  • 9
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值