Redis分布式锁实现

Redis分布式锁实现

分布式系统中一般都会存在资源竞争,java存在的锁和并发控制只能保证同一个jvm下的资源分配。分布式系统就要使用其他方式来保证资源的按顺序获取。早期系统比较常见的就是通过数据库中行级锁加乐观锁的方式实现,但是这种方式效率比较低,主要是数据库是硬盘级别的io读取速率,效率不高。在redis并广泛使用以后,redis就是分布式锁更合理的解决方案。

分布式锁的使用场景

Redis自编码实现分布式锁

可以自己编码通过redis来实现分布式锁,实际在生成环境不一定要用这个方式。自我实现的好处是对redis实现分布式锁的原理可以有比较好的认识,同时也能比较好的理解什么事分布式锁,同时认识的不同的实现方式存在什么样的问题。

先看一个基础实现例子

public boolean lock(String key, V v, int expireTime){
     
        int retry = 0;
        //获取锁失败最多尝试10次
        while (retry < failRetryTimes){
     
            //获取锁
            Boolean result = redis.setNx(key, v, expireTime);
            if (result){
     
                return true;
            }
            try {
     
                //获取锁失败间隔一段时间重试
                TimeUnit.MILLISECONDS.sleep(sleepInterval);
            } catch (InterruptedException e) {
     
                Thread.currentThread().interrupt();
                return false;
            }
        }
        return false;
    }
    public boolean unlock(String key){
     
        return redis.delete(key);
    }
    public static void main(String[] args) {
     
        Integer productId = 324324;
        RedisLock<Integer> redisLock = new RedisLock<Integer>();
        redisLock.lock(productId+"", productId, 1000);
    }
}

这个简单例子基本实现了分布式所有核心功能。其实就是锁的获取和锁的释放。

但是这个实现存在不少问题:

  • 可能会导致当前线程被其他线程释放
  • 不能实现可重入(可重入理解)
  • 客户端如果第一次设置成功,但是由于超时返回失败,伺候客户端尝试会一直失败。

可以做几个基本的修改

  • 增加value的判断,value和具体线程有关
  • 增加锁计数器,如果已经有所,直接计数器加1并返回true
private static volatile int count = 0;
public boolean lock(String key, V v, int expireTime){
     
    int retry = 0;
    //获取锁失败最多尝试10次
    while (retry < failRetryTimes){
     
        //1.先获取锁,如果是当前线程已经持有,则直接返回
        //2.防止后面设置锁超时,其实是设置成功,而网络超时导致客户端返回失败,所以获取锁之前需要查询一下
        V value = redis.get(key);
        //如果当前锁存在,并且属于当前线程持有,则锁计数+1,直接返回
        if (null != value && value.equals(v)){
     
            count ++;
            return true;
        }
        //如果锁已经被持有了,那需要等待锁的释放
        if (value == null || count <= 0){
     
            //获取锁
            Boolean result = redis.setNx(key, v, expireTime);
            if (result){
     
                count = 1;
                return true;
            }
        }
        try {
     
            //获取锁失败间隔一段时间重试
            TimeUnit.MILLISECONDS.sleep(sleepInterval);
        } catch (InterruptedException e) {
     
            Thread.currentThread().interrupt();
            return false;
        }
    }
    return false;
}
public boolean unlock(String key, String requestId){
     
    String value = redis.get(key);
    if (Strings.isNullOrEmpty(value)){
     
        count = 0;
        return true;
    }
    //判断当前锁的持有者是否是当前线程,如果是的话释放锁,不是的话返回false
    if (value.equals(requestId)){
     
        if (count > 1){
     
            count -- ;
            return true;
        }
        boolean delete = redis.delete(key);
        if (delete){
     
            count = 0;
        }
        return delete;
    }
    return false;
}

但是这个实现本身也有问题,就是先取出来判断造成了整个操作不是原子级的。

可以采用lua脚本的方式保证整个操作的原子级

 public final int failRetryTimes = 10;
    public final int waitIntervalInMS = 100;

    public boolean lock(String key, String t, int expireTemp) {
     
        int retry = 0;
        boolean result;
        try {
     
            while (retry < failRetryTimes) {
     
                retry++;
                result = redisTemplate.opsForValue().setIfAbsent(key, t, expireTemp, TimeUnit.SECONDS);
                if (result) {
     
                    return true;
                }
                TimeUnit.MILLISECONDS.sleep(waitIntervalInMS);
            }
        } catch (Exception e) {
     
            log.error(e.getMessage());
        }
        return false;
    }

    public void unLock(String key, String value) {
     
        String script =
                "if redis.call('get',KEYS[1]) == ARGV[1] then" +
                        "   return redis.call('del',KEYS[1]) " +
                        "else" +
                        "   return 0 " +
                        "end";
        redisTemplate.execute((RedisCallback<Boolean>) redisConnection ->
                redisConnection.eval(script.getBytes(),
                        ReturnType.BOOLEAN, 1, key.getBytes(StandardCharsets.UTF_8),
                        value.getBytes(StandardCharsets.UTF_8)));
    }

Redission实现分布式锁

在实际的使用过程中可以试用Redission框架来使用他帮我们实现好的分布式锁解决方案。而且这个解决方案支持集群式架构的redis。

  • 首先是增加依赖,注意springboot2.0以后redission也做了修改
<dependency>
    <groupId>org.redissongroupId>
    <artifactId>redisson-spring-data-22artifactId>
    <version>3.12.2version>
dependency>
  • 然后需要一个配置文件作为基础配置
singleServerConfig:
  idleConnectionTimeout: 10000
  pingTimeout: 1000
  connectTimeout: 10000
  timeout: 3000
  retryAttempts: 3
  retryInterval: 1500
  reconnectionTimeout: 3000
  failedAttempts: 3
  password: null
  subscriptionsPerConnection: 5
  clientName: null
  address: "redis://192.168.0.128:6379"
  subscriptionConnectionMinimumIdleSize: 1
  subscriptionConnectionPoolSize: 50
  connectionMinimumIdleSize: 32
  connectionPoolSize: 64
  database: 0
  #在最新版本中dns的检查操作会直接报错 所以我直接注释掉了
  #dnsMonitoring: false
  dnsMonitoringInterval: 5000
threads: 0
nettyThreads: 0
codec: ! {
     }
transportMode : "NIO"
  • 转配类,类似springconfig
@Bean
    RedissonClient redissonClient() throws IOException {
     
        Config config = new Config();
        //单机模式  依次设置redis地址和密码
        config.useSingleServer().
                setAddress("redis://192.168.0.128:6379");
        config.setCodec(new StringCodec());
        return Redisson.create(config);
    }

主要要注意redission针对不同的redis部署方式有不同的装配方式

  • 基本的使用范例
public void lockDemo() throws InterruptedException {
     
        System.out.println("我要开始处理东西了");
        RLock rLock = redissonClient.getLock("red-key");

        if (rLock.tryLock(5, 20, TimeUnit.SECONDS)) {
     
            log.info("我在这里处理了点事情" + Thread.currentThread().getName());
        }
        try {
     
            Thread.sleep(10);
            System.out.println("我处理完了");
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        } finally {
     
            if (rLock.isLocked()) {
     
                rLock.unlock();
            }
        }
    }

可以写个测试方法测试下

@Test
public void lockTest() throws InterruptedException {
     
    for (int i = 0; i < 10; i++) {
     
        log.info("开始-----------------");
        new Thread(() -> {
     
            try {
     
                demoService.lockDemo();
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            }
        }).start();
    }
    Thread.sleep(1000);
}

你可能感兴趣的:(并发,分布式,redis)