单机 Redis 的分布式锁

文章目录

  • 定义锁的抽象类
  • 错误版:
  • 改进一
  • 改进二
  • 改进三 (lua原子操作+刷新过期时间)
  • 最终代码
  • 总结

定义锁的抽象类

抽象类RedisLock实现java.util.concurrent包下的Lock接口,然后对一些方法提供默认实现,子类只需实现lock方法和unlock方法即可。代码如下:

public abstract class RedisLock implements Lock {

    protected Jedis jedis;
    protected String lockKey;

    public RedisLock(Jedis jedis,String lockKey) {
        this(jedis, lockKey);
    }

    public void sleepBySencond(int sencond){
        try {
            Thread.sleep(sencond*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void lockInterruptibly(){}

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

    @Override
    public boolean tryLock() {
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit){
        return false;
    }

}

错误版:

public class LockCase1 extends RedisLock {

      public LockCase1(Jedis jedis, String name) {
          super(jedis, name);
	  }
	  
      @Override
      public void lock() {
          while(true){
              String result = jedis.set(lockKey, "value", "NX");
              if(OK.equals(result)){
			 	 System.out.println(Thread.currentThread().getId()+"加锁成功!");
				 break; 
			  }
	  	  } 
	  }
	  
      @Override
      public void unlock() {
          jedis.del(lockKey);
      }
}

假设有两个客户端A和B,A获取到分布式的锁。A执行了一会,突然A所在的服务器断电了(或者其他什么的),也就是客户端A挂了。这时出现一个问题,这个锁一直存在,且不会释放,其他客户端永远获取不到锁。
单机 Redis 的分布式锁_第1张图片

改进一

public void lock() {
	while(true){
		String result = jedis.set(lockKey, "value", NOT_EXIST,SECONDS,30);
		if(OK.equals(result)){
			System.out.println(Thread.currentThread().getId()+"加锁成功!");
			break; 
		}
	} 
}

这时又出现一个问题,问题出现的步骤如下:

  • 1.客户端A获取锁成功,过期时间30秒。
  • 2.客户端A在某个操作上阻塞了50秒。
  • 3.30秒时间到了,锁自动释放了。
  • 4.客户端B获取到了对应同一资源的锁。
  • 5.客户端A从阻塞中恢复出来,释放掉了客户端B持有的锁。

单机 Redis 的分布式锁_第2张图片

改进二

抽象类RedisLock增加lockValue字段,lockValue字段的默认值为UUID随机值假设当前线程ID。

import redis.clients.jedis.Jedis;

import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

public abstract class RedisLock implements Lock {

    protected Jedis jedis;
    protected String lockKey;
    protected String lockValue;

    public RedisLock(Jedis jedis,String lockKey) {
        this(jedis, lockKey, UUID.randomUUID().toString()+Thread.currentThread().getId());
    }

    public RedisLock(Jedis jedis, String lockKey, String lockValue) {
        this.jedis = jedis;
        this.lockKey = lockKey;
        this.lockValue = lockValue;
    }

    ....
    
}

实现类里lock和unlock方法

public void lock() {
	while(true){
		String result = jedis.set(lockKey, lockValue, NOT_EXIST,SECONDS, 30);
		if(OK.equals(result)){
			System.out.println(Thread.currentThread().getId()+"加锁成功!");
			break; 
		}
	} 
}

...

public void unlock() {
	String lockValue = jedis.get(lockKey);
	if (lockValue.equals(lockValue)){
	    jedis.del(lockKey);
	}
}

问题在于如果调用jedis.del()方法的时候,这把锁已经不属于当前客户端的时候会解除他人加的锁。那么是否真的有这种场景?答案是肯定的,比如客户端A加锁,一段时间之后客户端A解锁,在执行jedis.del()之前,锁突然过期了,此时客户端B尝试加锁成功,然后客户端A再执行del()方法,则将客户端B的锁给解除了。

根本原因是if判断和del操作不是原子性的。

改进三 (lua原子操作+刷新过期时间)

 public void unlock() {
	// 使用lua脚本进行原子删除操作
	String checkAndDelScript = "if redis.call('get', KEYS[1]) == ARGV[1] then" +
	                           "return redis.call('del', KEYS[1]) " +
	                           "else " +
	                           "return 0 " +
	                           "end";
	jedis.eval(checkAndDelScript, 1, lockKey, lockValue);
}

这时还有最后一个问题:过期时间如何保证大于业务执行时间。
抽象类中RedisLock增加一个boolean类型的属性isOpenExpirationRenewal,用来标识是否开启定时刷新过期时间。

在增加一个scheduleExpirationRenewal方法用于开启刷新过期时间的线程。

package com.zhuawa.course.biz.redis.distributedlock;


import redis.clients.jedis.Jedis;

import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

public abstract class RedisLock implements Lock {

    protected Jedis jedis;
    protected String lockKey;
    protected String lockValue;
    protected volatile boolean isOpenExpirationRenewal = true;

    public RedisLock(Jedis jedis,String lockKey) {
        this(jedis, lockKey, UUID.randomUUID().toString()+Thread.currentThread().getId());
    }

    public RedisLock(Jedis jedis, String lockKey, String lockValue) {
        this.jedis = jedis;
        this.lockKey = lockKey;
        this.lockValue = lockValue;
    }

    public void sleepBySencond(int sencond){
        try {
            Thread.sleep(sencond*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 开启定时刷新
     */
    protected void scheduleExpirationRenewal(){
        Thread renewalThread = new Thread(new ExpirationRenewal());
        renewalThread.start();
    }
    /**
     * 刷新key的过期时间
     */
    private class ExpirationRenewal implements Runnable{
        @Override
        public void run() {
            while (isOpenExpirationRenewal){
                System.out.println("执行延迟失效时间中...");

                String checkAndExpireScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                        "return redis.call('expire',KEYS[1],ARGV[2]) " +
                        "else " +
                        "return 0 end";
                jedis.eval(checkAndExpireScript, 1, lockKey, lockValue, "30");

                //休眠10秒
                sleepBySencond(10);
            }
        }
    }

    @Override
    public void lockInterruptibly(){}

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

    @Override
    public boolean tryLock() {
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit){
        return false;
    }

}

加锁代码在获取锁成功后将isOpenExpirationRenewal置为true,并且调用scheduleExpirationRenewal方法,开启刷新过期时间的线程。

public void lock() {
    while (true) {
        String result = jedis.set(lockKey, lockValue, NOT_EXIST, SECONDS, 10);
        if (OK.equals(result)) {
            System.out.println("线程id:"+Thread.currentThread().getName() + "加锁成功!时间:"+LocalTime.now());

            //开启定时刷新过期时间
            isOpenExpirationRenewal = true;
            scheduleExpirationRenewal();
            break;
        }
        System.out.println("线程id:"+Thread.currentThread().getName() + "获取锁失败,休眠10秒!时间:"+LocalTime.now());
        //休眠10秒
        sleepBySencond(10);
    }
}

解锁代码增加一行代码,将isOpenExpirationRenewal属性置为false,停止刷新过期时间的线程轮询。

public void unlock() {
    System.out.println("线程id:"+Thread.currentThread().getId() + "解锁!时间:"+LocalTime.now());

    String checkAndDelScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                                "return redis.call('del', KEYS[1]) " +
                                "else " +
                                "return 0 " +
                                "end";
    jedis.eval(checkAndDelScript, 1, lockKey, lockValue);
    isOpenExpirationRenewal = false;

}

最终代码

常量类:

public class LockConstants {
    public static final String OK = "OK";

    /** NX|XX, NX -- Only set the key if it does not already exist. XX -- Only set the key if it already exist. **/
    public static final String NOT_EXIST = "NX";
    public static final String EXIST = "XX";

    /** expx EX|PX, expire time units: EX = seconds; PX = milliseconds **/
    public static final String SECONDS = "EX";
    public static final String MILLISECONDS = "PX";

    private LockConstants() {}
}

抽象类:

import redis.clients.jedis.Jedis;

import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

public abstract class RedisLock implements Lock {

    protected Jedis jedis;
    protected String lockKey;
    protected String lockValue;
    protected volatile boolean isOpenExpirationRenewal = true;

    public RedisLock(Jedis jedis,String lockKey) {
        this(jedis, lockKey, UUID.randomUUID().toString()+Thread.currentThread().getId());
    }

    public RedisLock(Jedis jedis, String lockKey, String lockValue) {
        this.jedis = jedis;
        this.lockKey = lockKey;
        this.lockValue = lockValue;
    }

    public void sleepBySencond(int sencond){
        try {
            Thread.sleep(sencond*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 开启定时刷新
     */
    protected void scheduleExpirationRenewal(){
        Thread renewalThread = new Thread(new ExpirationRenewal());
        renewalThread.start();
    }
    /**
     * 刷新key的过期时间
     */
    private class ExpirationRenewal implements Runnable{
        @Override
        public void run() {
            while (isOpenExpirationRenewal){
                System.out.println("执行延迟失效时间中...");

                String checkAndExpireScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                        "return redis.call('expire',KEYS[1],ARGV[2]) " +
                        "else " +
                        "return 0 end";
                jedis.eval(checkAndExpireScript, 1, lockKey, lockValue, "30");

                //休眠10秒
                sleepBySencond(10);
            }
        }
    }

    @Override
    public void lockInterruptibly(){}

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

    @Override
    public boolean tryLock() {
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit){
        return false;
    }

}

实现类:

import redis.clients.jedis.Jedis;

import java.time.LocalTime;

import static com.zhuawa.course.biz.redis.distributedlock.LockConstants.NOT_EXIST;
import static com.zhuawa.course.biz.redis.distributedlock.LockConstants.OK;
import static com.zhuawa.course.biz.redis.distributedlock.LockConstants.SECONDS;


public class RedisDistributedLock extends RedisLock {

    public RedisDistributedLock(Jedis jedis, String lockKey) {
        super(jedis, lockKey);

    }

    @Override
    public void lock() {
        while (true) {
            String result = jedis.set(lockKey, lockValue, NOT_EXIST, SECONDS, 10);
            if (OK.equals(result)) {
                System.out.println("线程id:"+Thread.currentThread().getName() + "加锁成功!时间:"+LocalTime.now());

                //开启定时刷新过期时间
                isOpenExpirationRenewal = true;
                scheduleExpirationRenewal();
                break;
            }
            System.out.println("线程id:"+Thread.currentThread().getName() + "获取锁失败,休眠10秒!时间:"+LocalTime.now());
            //休眠10秒
            sleepBySencond(10);
        }
    }

    @Override
    public void unlock() {
        System.out.println("线程id:"+Thread.currentThread().getId() + "解锁!时间:"+LocalTime.now());

        String checkAndDelScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                                    "return redis.call('del', KEYS[1]) " +
                                    "else " +
                                    "return 0 " +
                                    "end";
        jedis.eval(checkAndDelScript, 1, lockKey, lockValue);
        isOpenExpirationRenewal = false;

    }
}

总结

setnx+lua
4大要点:

  • set命令要用set key value px milliseconds nx
  • value要具有唯一性
  • 释放锁时要验证value的值,不能误解锁
  • 定时刷新过期时间保障过期时间大于业务执行时间

你可能感兴趣的:(redis)