• 这才是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/
    任何傻瓜都能写出计算机可以理解的代码。好的程序员能写出人能读懂的代码。

     
  • 相关阅读:
    JDK,JRE,JVM,三者,你知道它们的关系么
    Linux防火墙机制
    mysql数据可以连接到myeclipse当中需要知道的语法
    mysql数据库常用命令
    修改mysql默认编码重启后又还原,且在修改my.ini配置文件出现1067错误的解决办法。
    MySQL 5.5 Command Line Client 打开出现闪退(错误)的解决办法
    修改mysql 的初始密码
    linux命令的续行,linux shell 参数换行(标准说法:续行)
    java面试(反射)05
    java面试(进程和线程)04
  • 原文地址:https://www.cnblogs.com/leigq/p/13406519.html
Copyright © 2020-2023  润新知