测试了很久的redis分布式锁

前言:分布式锁一般有三种实现方式:1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁。本篇博客将介绍第二种方式,基于Redis实现分布式锁
组件依赖
首先我们要通过Maven引入Jedis开源组件,在pom.xml文件加入下面的代码:

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>

代码实现
先展示代码,再慢慢指出代码的不足处以及优化方式

public class RedislockApplication {

	public static void main(String[] args) {
		LockService lockService=new LockService();
		for (int i=0;i<10;i++){
			new ThreadRedis(lockService).start();
		}
	}

}
public class LockRedis {

	private ThreadLocal<Jedis> redisThreadLocal = new ThreadLocal();

	//redis线程池
	private JedisPool jedisPool;
	private static final String REDISLOCKKEY="REDIS_KEY";

	public LockRedis(JedisPool jedisPool){
		this.jedisPool=jedisPool;
	}

	/**
	 * redis实现分布式锁 有两个超时 时间问题
	 * 两个超时时间含义:<br>
	 * 1.在获取锁之前的超时时间----在尝试获取锁的时候,如果在规定的时间内还没有获取锁,直接放弃。<br>
	 * 2.在获取锁之后的超时时间---当获取锁成功之后,对应的key 有对应有效期,对应的key 在规定时间内进行失效
	 */

	/**
	 * 获取锁
	 * @param acquireTimeout 在获取锁之前的超时时间
	 * @param timeout 在获取锁之后的超时时间
	 * @return
	 */
	public String getRedisLockKey(Long acquireTimeout,Long timeout){
		Jedis conn=null;
		try {
			//1.建立连接
			conn=jedisPool.getResource();
			//redisThreadLocal.set(conn);
			//2.定义 redis 对应key 的value值( uuid) 作用 释放锁 随机生成value
			String value= UUID.randomUUID().toString();
			//3.定义在获取锁之后的超时时间
			int expireLock=(int) (timeout/1000);//以秒为单位
			if(conn.setnx(REDISLOCKKEY,value)==1){
				//设置对应key的有效期
				conn.expire(REDISLOCKKEY,expireLock);
				return value;
				// 为什么获取锁之后,还要设置锁的超时时间 目的是为了防止死锁
			}
		}catch (Exception e) {
			e.printStackTrace();
		} finally {
			if (conn != null) {
				conn.close();
			}
		}
		return null;
	}

	/**
	 * 释放redis锁
	 * 释放锁有两种 key自动有有效期
	 *整个程序执行完毕情况下,删除对应key
	 * @param value
	 */
	public void unRedisLock(String value){
		Jedis conn=null;
		try {
			conn=jedisPool.getResource();
			if(conn.get(REDISLOCKKEY).equals(value)){
				Long result=conn.del(REDISLOCKKEY);
				if(result==1){
					System.out.println("释放锁。。。"+Thread.currentThread().getName()+",value:"+value);
				}
			}
		}catch (Exception e){

		}finally {
			if (conn != null) {
				conn.close();
			}
		}
	}
}

public class LockService {
	private static JedisPool pool = null;
	static{
		JedisPoolConfig config = new JedisPoolConfig();
		// 设置最大连接数
		config.setMaxTotal(200);
		// 设置最大空闲数
		config.setMaxIdle(8);
		// 设置最大等待时间
		config.setMaxWaitMillis(1000 * 100);
		// 在borrow一个jedis实例时,是否需要验证,若为true,则所有jedis实例均是可用的
		config.setTestOnBorrow(true);
		pool = new JedisPool(config, "127.0.0.1", 6379, 3000);
	}

	private LockRedis lockRedis=new LockRedis(pool);

	//演示redis实现分布式锁
	public void seckill(){
		//1.获取锁
		System.out.println("尝试获取锁");
		String value=lockRedis.getRedisLockKey(5000L);
		if(value!=null){
			System.out.println(Thread.currentThread().getName() + ",获取锁成功,锁的id:" + value + ",正常执行业务了逻辑");
			try {
				Thread.sleep(1*1000);
			}catch (Exception e){

			}
			//2.释放锁
			lockRedis.unRedisLock(value);
		}
	}
}

public class ThreadRedis extends Thread {

	private LockService lockService;
	public ThreadRedis(LockService lockService) {
		this.lockService = lockService;
	}

	@Override
	public void run(){
		lockService.seckill();
	}
}

这里我开了10个线程模拟10个用户,一直在获取分布式锁
输出结果如下:

Thread-1尝试获取锁
Thread-5尝试获取锁
Thread-4尝试获取锁
Thread-3尝试获取锁
Thread-2尝试获取锁
Thread-6尝试获取锁
Thread-7尝试获取锁
Thread-9尝试获取锁
Thread-10尝试获取锁
Thread-8尝试获取锁
Thread-8,获取锁成功,锁的id:da757e48-c082-4c42-a37e-9c9216f6b3e5,正常执行业务了逻辑
释放锁。。。Thread-8,value:da757e48-c082-4c42-a37e-9c9216f6b3e5

以上加锁解锁代码乍一看没什么问题,也很符合大家的要求。不过。。。看了一位大佬的博文(博文地址)以后,我豁然开朗,以上的代码确实存在缺陷,听我细细道来。
1.setnx()方法作用就是SET IF NOT EXIST,expire()方法就是给锁加一个过期时间,由于这是两条redis命令,不具有原子性,如果程序在执行完setnx()之后突然崩溃,导致锁没有设置过期时间。那么将会发生死锁。
加锁代码的正确姿态应该是:

public class RedisTool {
 
    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";//秒用EX,毫秒用PX
 
    /**
     * 尝试获取分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
 
        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
 
        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;
 
    }
 
}

可以看到,我们加锁就一行代码:jedis.set(String key, String value, String nxxx, String expx, int time),这个set()方法一共有五个形参:

第一个为key,我们使用key来当锁,因为key是唯一的。

第二个为value,我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用UUID.randomUUID().toString()方法生成。

第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;

第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。

第五个为time,与第四个参数相呼应,代表key的过期时间。

总的来说,执行上面的set()方法就只会导致两种结果:1. 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。2. 已有锁存在,不做任何操作。

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

public class RedisTool {
 
    private static final Long RELEASE_SUCCESS = 1L;
 
    /**
     * 释放分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
 
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
 
        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;
 
    }
 
}

可以看到,我们解锁只需要两行代码就搞定了!第一行代码,我们写了一个简单的Lua脚本代码,这段编程语言在《黑客与画家》里,,第二行代码,我们将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行。
那么这段Lua代码的功能是什么呢?其实很简单,首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。那么为什么要使用Lua语言来实现呢?因为要确保上述操作是原子性的
那么为什么执行eval()方法可以确保原子性,源于Redis的特性,下面是官网对eval命令的部分解释:
简单来说,就是在eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令。

如果你的项目中Redis是多机部署的,那么可以尝试使用Redisson实现分布式锁

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值