这才是Redis分布式锁真正实现方式

非SpringBoot项目

基于jedis

package com.blog.www.util.lock;

import lombok.extern.slf4j.Slf4j;
import redis.clients.jedis.Jedis;

import java.util.Collections;
import java.util.UUID;


/**
 * Redis实现分布式锁,基于 jedis
 * <br/>
 * 请使用最新实现 {@link RedisLock}
 * <br/>
 * 分布式锁一般有三种实现方式:1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁。<br/>
 * 可靠性:<br/>
 * 为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:<br/>
 * <ul>
 * <li>
 * 1. 互斥性。在任意时刻,只有一个客户端能持有锁。
 * </li>
 * <li>
 * 2. 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
 * </li>
 * <li>
 * 3. 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
 * </li>
 * <li>
 * 4. 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
 * </li>
 * </ul>
 * 参考:<br/>
 * <ul>
 * <li>
 * <a href='https://www.cnblogs.com/linjiqin/p/8003838.html'>Redis分布式锁的正确实现方式</a>
 * </li>
 * <li>
 * <a href='https://www.cnblogs.com/cmyxn/p/9047848.html'>什么是分布式锁及正确使用redis实现分布式锁</a>
 * </li>
 * <li>
 * <a href='https://blog.csdn.net/crystalqy/article/details/89024653'>基于Spring boot 2.1 使用redisson实现分布式锁</a>
 * </li>
 * </ul>
 *
 * @author :leigq
 * @date :2019/7/2 11:22
 */
@Slf4j
@Deprecated
public class RedisLockForJedis {

	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";
	private static final Long RELEASE_SUCCESS = 1L;

	/**
	 * 尝试获取分布式锁
	 *
	 * @param jedis      Redis客户端
	 * @param lockKey    锁
	 * @param requestId  请求标识 可以使用UUID.randomUUID().toString()方法生成
	 * @param expireTime 超期时间 单位毫秒
	 * @return 是否获取成功
	 */
	public static boolean lock(Jedis jedis, String lockKey, String requestId, int expireTime) {
		String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
		return LOCK_SUCCESS.equals(result);
	}

	/**
	 * 释放分布式锁
	 *
	 * @param jedis     Redis客户端
	 * @param lockKey   锁
	 * @param requestId 请求标识 可以使用UUID.randomUUID().toString()方法生成
	 * @return 是否释放成功
	 */
	public static boolean unLock(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));
		return RELEASE_SUCCESS.equals(result);
	}


    /**
     * 测试加锁解锁(测试通过)
     */
	public static void main(String[] args) {
        String requestId = UUID.randomUUID().toString();


	    /* 单机 jedis连接使用参考:https://blog.csdn.net/qianqian666888/article/details/79087930*/
		Jedis jedis = new Jedis("127.0.0.1", 6379);
        // 设置密码
        jedis.auth("111111");
        // 加锁
        boolean locked = lock(jedis, "lockKey", requestId, 60 * 60 * 1000);
        log.warn("locked result is : [{}]", locked);
        // 解锁
        boolean unLocked = unLock(jedis, "lockKey", requestId);
        log.warn("unLocked result is : [{}]", unLocked);

    }

}

SpringBoot项目

客户端选用 jedisLettuce 均可

package com.blog.www.util.lock;

import com.blog.www.config.RedisConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;

import java.util.Collections;
import java.util.Objects;


/**
 * Redis实现分布式锁,基于 RedisTemplate
 * <br/>
 * jedis 实现请看:<a href='https://blog.csdn.net/qq_28397259/article/details/80839072'>基于redisTemplate的redis的分布式锁正确打开方式</a>
 * <br/>
 * 分布式锁一般有三种实现方式:1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁。<br/>
 * 可靠性:<br/>
 * 为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:<br/>
 * <ul>
 * <li>
 * 1. 互斥性。在任意时刻,只有一个客户端能持有锁。
 * </li>
 * <li>
 * 2. 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
 * </li>
 * <li>
 * 3. 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
 * </li>
 * <li>
 * 4. 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
 * </li>
 * </ul>
 * 参考:<br/>
 * <ul>
 * <li>
 * <a href='https://www.cnblogs.com/linjiqin/p/8003838.html'>Redis分布式锁的正确实现方式</a>
 * </li>
 * <li>
 * <a href='https://blog.csdn.net/long2010110/article/details/82911168'>springboot的RedisTemplate实现分布式锁</a>
 * </li>
 * </ul>
 * <a href='https://blog.csdn.net/weixin_38399962/article/details/82753763'>使用示例参考</a>:
 * <pre>
 * {@link @Autowired}
 * private {@link RedisLock} redisLock;
 *
 * boolean locked = redisLock.lock(lockKey, requestId, expireTime);
 * if (locked) {
 *     // 执行逻辑操作
 *     ......
 *     ......
 *     redisLock.unLock(lockKey, requestId);
 * } else {
 *     // 设置失败次数计数器, 当到达5次时, 返回失败
 *     int failCount = 1;
 *     while(failCount <= 5){
 *         // 等待100ms重试
 *         try {
 *             Thread.sleep(100l);
 *         } catch (InterruptedException e) {
 *             e.printStackTrace();
 *         }
 *         if (redisLock.lock(lockKey, requestId, expireTime)){
 *            // 执行逻辑操作
 *            ......
 *            ......
 *            redisLock.unLock(lockKey, requestId);
 *         }else{
 *             failCount ++;
 *         }
 *     }
 *     throw new RuntimeException("现在创建的人太多了, 请稍等再试");
 * }
 * </pre>
 *
 * @author :leigq
 * @date :2019/7/2 11:22
 */
@Slf4j
@Service
@SuppressWarnings(value = "unchecked")
public final class RedisLock {

	private final RedisTemplate redisTemp;

	/**
	 * 使用 RedisConfig 中的 redisTemp,自定义序列化 及 兼容 java8 时间
	 * @see RedisConfig#getRedisTemplate(RedisConnectionFactory)
	 */
	public RedisLock(@Qualifier("redisTemp") RedisTemplate redisTemp) {
		this.redisTemp = redisTemp;
	}

	/**
	 * 尝试获取分布式锁
	 *
	 * @param lockKey    锁key
	 * @param requestId  请求标识 可以使用UUID.randomUUID().toString()方法生成
	 * @param expireTime 超期时间 单位秒
	 * @return 是否获取成功
	 */
	public boolean lock(String lockKey, String requestId, int expireTime) {
		// 使用脚本,保证原子性
		RedisScript redisScript = RedisScript.of(LOCK_LUA, Long.class);
		Object lockResult = redisTemp.execute(redisScript, Collections.singletonList(lockKey), requestId, expireTime);
		log.warn("lock executeResult is [{}]", lockResult);
		return Objects.equals(SUCCESS, lockResult);
		// 不符合原子性
//		Boolean setIfAbsentResult = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, requestId);
//		Boolean setExpireResult = stringRedisTemplate.expire(lockKey, expireTime, TimeUnit.SECONDS);
//		return setIfAbsentResult && setExpireResult;
	}

	/**
	 * 释放分布式锁
	 *
	 * @param lockKey   锁key
	 * @param requestId 请求标识 可以使用UUID.randomUUID().toString()方法生成
	 * @return 是否释放成功
	 */
	public boolean unLock(String lockKey, String requestId) {
		RedisScript redisScript = RedisScript.of(UNLOCK_LUA, Long.class);
		Object unLockResult = redisTemp.execute(redisScript, Collections.singletonList(lockKey), requestId);
		log.warn("unLock executeResult is [{}]", unLockResult);
		return Objects.equals(SUCCESS, unLockResult);
	}

	private static final Long SUCCESS = 1L;

	// 加锁 Lua 脚本
	private static final String LOCK_LUA;

	// 解锁 Lua 脚本
	private static final String UNLOCK_LUA;

	static {
		// if redis.call('setNx', KEYS[1], ARGV[1]) then if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('expire', KEYS[1], ARGV[2]) else return 0 end end
		LOCK_LUA = "if redis.call('setNx', KEYS[1], ARGV[1]) " +
				"then " +
				"    if redis.call('get', KEYS[1]) == ARGV[1] " +
				"    then " +
				"        return redis.call('expire', KEYS[1], ARGV[2]) " +
				"    else " +
				"        return 0 " +
				"    end " +
				"end ";

		// if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
		UNLOCK_LUA = "if redis.call('get', KEYS[1]) == ARGV[1] " +
				"then " +
				"    return redis.call('del', KEYS[1]) " +
				"else " +
				"    return 0 " +
				"end ";
	}

}

redisTemp 请在这获取:SpringBoot2.0.X配置Redis

测试

package com.blog.www.util.lock;

import com.blog.www.base.BaseApplicationTests;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.UUID;

/**
 * RedisLock Tester.
 *
 * @author leigq
 * @version 1.0
 * @since <pre>10/17/2019</pre>
 */
public class RedisLockTest extends BaseApplicationTests {

	@Autowired
	private RedisLock redisLock;

	/**
	 * Redis分布式锁测试,基于RedisTemplate,测试通过
	 */
	@Test
	public void testLockAndUnLock() throws Exception {
		String requestId = UUID.randomUUID().toString();

		// 加锁
		boolean locked = redisLock.lock("lockKey", requestId, 40);
		log.warn("locked result is : [{}]", locked);
		// 解锁
		boolean unLocked = redisLock.unLock("lockKey", requestId);
		log.warn("unLocked result is : [{}]", unLocked);
	}

}

BaseApplicationTests.java

package com.blog.www.base;

import org.junit.After;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

/**
 * 测试基类,其他类继承此类
 * <br/>
 * @author     :leigq
 * @date       :2019/8/13 17:17
 */
@RunWith(SpringRunner.class)
@SpringBootTest
public abstract class BaseApplicationTests {

    protected Logger log = LoggerFactory.getLogger(this.getClass());

    private Long time;

    @Before
    public void setUp() {
        this.time = System.currentTimeMillis();
        log.info("==> 测试开始执行 <==");
    }

    @After
    public void tearDown() {
        log.info("==> 测试执行完成,耗时:{} ms <==", System.currentTimeMillis() - this.time);
    }
}

测试结果如下:

20191018111839.png


作者:不敲代码的攻城狮
出处:https://www.cnblogs.com/leigq/
任何傻瓜都能写出计算机可以理解的代码。好的程序员能写出人能读懂的代码。

 
原文地址:https://www.cnblogs.com/leigq/p/13406519.html