手写Redis分布式锁(二)

手写Redis分布式锁(二)

一. 加锁解锁思想分析

在“手写Redis分布式锁(一)”中,我们已经通过“while判断并自旋重试获取锁+setnx含过期时间+Lua脚本删除锁”,但并未考虑锁的可重入性

​ 一个靠谱的分布式锁需要具备以下条件:

  • 独占性
  • 高可用
  • 防死锁
  • 不乱抢
  • 可重入性

​ 那么什么是可重入锁呢?可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。

​ synchronized的重入实现原理:

​ 1. 每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。

  1. 当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。

  2. 在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。

  3. 当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。

​ 思考synchronized的重入实现原理,我们可以用Redis中的Hash数据结构来实现锁的可重入性。通过HINCRBYs实现加锁,解锁。

加锁思想

  1. 先判断Redis分布式锁这个key是否存在(EXISTS key),判断语句返回0说明不存在,hset新建当前线程属于自己的锁(HSET key UUID:ThreadID),并设置过期时间,然后return 1,说明加锁成功;
  2. 判断语句返回1说明已经有锁,需要进一步判断该锁是不是当前线程自己的。(HEXISTS key UUID:ThreadID),判断语句返回1说明是自己的锁,自增表示重入(HINCRBY key UUID:ThreadID),然后return 1,说明加锁成功。返回0说明不是自己的,然后 return 0,说明加锁失败。

​ 将上述思路翻译为Lua脚本:

if redis.call('exists','key') == 0 then
  redis.call('hset','key','uuid:threadid',1)
  redis.call('expire','key',30)
  return 1
elseif redis.call('hexists','key','uuid:threadid') == 1 then
  redis.call('hincrby','key','uuid:threadid',1)
  redis.call('expire','key',30)
  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',30)
  return 1
else
  return 0
end

最终的Lua脚本为:

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

说明,如果不存在锁,或者存在锁且是当前线程锁持有的。那么对锁计数执行+1操作,并刷新过期时间。

解锁思想:判断“有锁且还是当前线程自己所持有的锁”(HEXISTS key uuid:ThreadID),如果判断语句返回0,说明根本就没有锁,return nil。如果判断语句返回的不是0,那么说明有锁且是自己的锁,直接进行HINCRBY -1,使锁计数减一(解锁一次),如果锁计数变为0,则删除该锁。

翻译为Lua脚本为:

if redis.call('HEXISTS',lock,uuid:threadID) == 0 then
 return nil
elseif redis.call('HINCRBY',lock,uuid:threadID,-1) == 0 then
 return redis.call('del',lock)
else 
 return 0
end

最终的Lua脚本为:

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

二. 加锁解锁实现

  1. 满足 JUC 里 AQS 对 Lock 锁的接口规范定义来实现代码,新建 RedisDistributedLock 类并实现 JUC 里面的 Lock 接口。
package com.hyw.redislock.mylock;

import cn.hutool.core.util.IdUtil;
import lombok.SneakyThrows;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.util.Arrays;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;


public class RedisDistributedLock implements Lock
{
    private StringRedisTemplate stringRedisTemplate;
    private String lockName;
    private String uuidValue;
    private long   expireTime;

    public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName,String uuidValue)
    {
        this.stringRedisTemplate = stringRedisTemplate;
        this.lockName = lockName;
        this.uuidValue = uuidValue+":"+Thread.currentThread().getId();
        this.expireTime = 30L;
    }

    @Override
    public void lock()
    {
        this.tryLock();
    }
    @Override
    public boolean tryLock()
    {
        try
        {
            return this.tryLock(-1L,TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException
    {
        if(time != -1L)
        {
            expireTime = unit.toSeconds(time);
        }

        String script =
                "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 (!stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime)))
        {
            try { TimeUnit.MILLISECONDS.sleep(60); } catch (InterruptedException e) { e.printStackTrace(); }
        }

        return true;
    }

    @Override
    public void unlock()
    {
        String script =
                "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";
        System.out.println("lockName: "+lockName+"\t"+"uuidValue: "+uuidValue);
        Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime));
        if(flag == null)
        {
            throw new RuntimeException("没有这个锁,HEXISTS查询无");
        }
    }

    //=========================================================
    @Override
    public void lockInterruptibly() throws InterruptedException
    {

    }
    @Override
    public Condition newCondition()
    {
        return null;
    }
}
  1. 为了以后方便扩展,引入工厂模式。

package com.hyw.redislock.mylock;

import cn.hutool.core.util.IdUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.locks.Lock;

@Component
public class DistributedLockFactory
{
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    private String lockName;
    private String uuidValue;
    public DistributedLockFactory()
    {
        this.uuidValue = IdUtil.simpleUUID();//UUID
    }

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

        if(lockType.equalsIgnoreCase("REDIS")){
            lockName = "RedisLock";
            return new RedisDistributedLock(stringRedisTemplate,lockName, uuidValue);
        } else if(lockType.equalsIgnoreCase("ZOOKEEPER")){
            //TODO zookeeper版本的分布式锁实现
            //return new ZookeeperDistributedLock();
            return null;
        } else if(lockType.equalsIgnoreCase("MYSQL")){
            //TODO mysql版本的分布式锁实现
            return null;
        }

        return null;
    }
}
  1. 那么 InventoryService 就可以使用我们编写的锁,并对锁的可重入性进行测试。

    package com.hyw.redislock.service;
    
    import cn.hutool.core.util.IdUtil;
    import com.hyw.redislock.mylock.DistributedLockFactory;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.data.redis.core.script.DefaultRedisScript;
    import org.springframework.stereotype.Service;
    
    import java.util.Arrays;
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    @Service
    @Slf4j
    public class InventoryService
    {
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
        @Value("${server.port}")
        private String port;
        @Autowired
        private DistributedLockFactory distributedLockFactory;
    
        public String sale()
        {
            String retMessage = "";
            Lock redisLock = distributedLockFactory.getDistributedLock("redis");
            // 加锁
            redisLock.lock();
            try
            {
                //1 查询库存信息
                String result = stringRedisTemplate.opsForValue().get("inventory001");
                //2 判断库存是否足够
                Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
                //3 扣减库存
                if(inventoryNumber > 0) {
                    stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
                    retMessage = "成功卖出一个商品,库存剩余: "+inventoryNumber;
                    System.out.println(retMessage);
                    this.testReEnter();
                }else{
                    retMessage = "商品卖完了,o(╥﹏╥)o";
                }
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                // 解锁
                redisLock.unlock();
            }
            return retMessage+"\t"+"服务端口号:"+port;
        }
    
        private void testReEnter()
        {
            Lock redisLock = distributedLockFactory.getDistributedLock("redis");
            redisLock.lock();
            try
            {
                System.out.println("################测试可重入锁####################################");
            }finally {
                redisLock.unlock();
            }
        }
    }
    

三. 自动续期

​ 考虑业务如果在锁过期了还没执行完毕,那么就需要对锁进行自动续期。

自动续期思想:如果锁还存在,说明业务没有执行完,则重新设置锁的过期时间。

​ Lua脚本为:

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

并在 RedisDistributedLock 中加入定时扫描业务是否完成的业务

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 (stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))) {
                    renewExpire();
                }
            }
        },(this.expireTime * 1000)/3);
    }

则 RedisDistributedLock 修改更为:

package com.hyw.redislock.mylock;

import cn.hutool.core.util.IdUtil;
import lombok.SneakyThrows;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.util.Arrays;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;


public class RedisDistributedLock implements Lock
{
    private StringRedisTemplate stringRedisTemplate;
    private String lockName;
    private String uuidValue;
    private long   expireTime;

    public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName,String uuidValue)
    {
        this.stringRedisTemplate = stringRedisTemplate;
        this.lockName = lockName;
        this.uuidValue = uuidValue+":"+Thread.currentThread().getId();
        this.expireTime = 30L;
    }

    @Override
    public void lock()
    {
        this.tryLock();
    }
    @Override
    public boolean tryLock()
    {
        try
        {
            return this.tryLock(-1L,TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException
    {
        if(time != -1L)
        {
            expireTime = unit.toSeconds(time);
        }

        String script =
                "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 (!stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime)))
        {
            try { TimeUnit.MILLISECONDS.sleep(60); } catch (InterruptedException e) { e.printStackTrace(); }
        }
        // 自动续期
        this.renewExpire();
        return true;
    }

    @Override
    public void unlock()
    {
        String script =
                "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";
        System.out.println("lockName: "+lockName+"\t"+"uuidValue: "+uuidValue);
        Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime));
        if(flag == null)
        {
            throw new RuntimeException("没有这个锁,HEXISTS查询无");
        }
    }
    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 (stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))) {
                    renewExpire();
                }
            }
        },(this.expireTime * 1000)/3);
    }

    //=========================================================
    @Override
    public void lockInterruptibly() throws InterruptedException
    {

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

四. 总结

至此我们手写的Redis分布式锁已经具备了独占性、高可用、防死锁、不乱抢、重入性。

但是当我们用来进行加锁解锁操作的Redis挂掉怎么办?Redisson 分布式锁可以解决这个问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值