Redis分布式锁的注意点总结 + 参考代码

推荐阅读:Redis分布式锁的正确实现方式介绍

直接说重点:

setnx命令,key自然是锁名。

  • value是requestId,即请求标识,能够区分不同的客户端,保证锁只能由加锁的客户端释放
  • expireTime 直接作为setnx的参数,而不是单独的设置过期时间,保证原子性
  • 释放锁时,判断value是否相等,然后再释放锁。但**关键是用lua脚本,保证原子性**

因为文章里都讲得很好很清楚,我就不赘述了。

感叹一下在实际做项目前,准备面试点,只理解到setnx一同设置过期时间,还觉得这个问题很简单…实践才知道这么多坑。

拓展:集群时分布式锁的问题

客户A在主节点上拿到了锁,然后主节点挂了(此时锁的数据还没同步到从节点),而从节点成为新的主节点,随后客户B获取到了锁。

这样就造成了两个客户同时拥有了锁,而且客户A无法感知主节点挂了。

解决方法: Redlock算法,具体自行百度


附源码:

/**
     *
     * @param prefix
     * @param key
     * @param value  客户端的唯一标识,保证:只有加锁的客户端可以解锁
     * @param expireTime  过期时间,单位:毫秒 (与PX、EX有关)
     * @param lockWaitTime  等待锁的时间。单位:毫秒
     * @return
     */
    public boolean lock(KeyPrefix prefix, String key, String value,
                        Long expireTime, Long lockWaitTime) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            String realKey = prefix.getPrefix() + key;
            Long deadTimeLine = System.currentTimeMillis() + lockWaitTime;

            // 关键是自旋,用的就是setnx而已。
            for (; ; ) {
//                PX:key过期时间单位,PX:毫秒,EX:秒
                String result = jedis.set(realKey, value, "NX", "PX", expireTime);

                if ("OK".equals(result)) {
                    return true;
                }

                lockWaitTime = deadTimeLine - System.currentTimeMillis();
                // 等待锁的限定时间已过,仍未获取锁,返回false
                if (lockWaitTime <= 0L) {
                    // 这里break也可以
                    return false;
                }
            }
        } catch (Exception ex) {
            System.out.println("lock error");
        } finally {
            returnToPool(jedis);
        }

        return false;
    }

    // 关键: 用Lua脚本保证 : 判断value相等 + 删除key ,这两步的原子性
    // 防止出现: A判断相等 -> 而删除前,锁过期。B检测到锁过期,获取了锁 -> A删除锁
    // 这种情况下,A删除的就是B的锁,而不是自己的锁(已过期)
    public boolean unlock(KeyPrefix prefix, String key, String value) {

        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            String realKey = prefix.getPrefix() + key;

            String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

            Object result = jedis.eval(luaScript, Collections.singletonList(realKey),
                    Collections.singletonList(value));

            if ("1".equals(result)) {
                return true;
            }

        } catch (Exception ex) {
            System.out.println("unlock error");
        } finally {
            returnToPool(jedis);
        }
        return false;
    }

// 释放资源
 private void returnToPool(Jedis jedis) {
        if (jedis != null) {
            jedis.close();
        }
    }

依赖:

<dependency>
      <groupId>redis.clientsgroupId>
      <artifactId>jedisartifactId>
dependency>

KeyPrefix 是项目中的接口,因为直接用锁名(string) 很容易就会重复。因此抽象出前缀,配合类名,可以保证一致性。不需要的话,去掉直接用key即可,无区别。

附KeyPrefix的相关代码:

public interface KeyPrefix {
	public int expireSeconds();
	public String getPrefix();
}


public abstract class BasePrefix implements KeyPrefix{

	/***
	  * @Description: 单位:秒s ,0代表永不过期
	  * @Author: hermanCho
	  * @Date: 2020-08-15
	  * @Param null:
	  * @return: null
	  **/
	private int expireSeconds;
	
	private String prefix;
	
	public BasePrefix(String prefix) {//0代表永不过期
		this(0, prefix);
	}
	
	public BasePrefix( int expireSeconds, String prefix) {
		this.expireSeconds = expireSeconds;
		this.prefix = prefix;
	}
	
	public int expireSeconds() {
		return expireSeconds;
	}

    // 通过 className + prefix ,作为key的前缀,可以保证不重复
	public String getPrefix() {
		String className = getClass().getSimpleName();
		return className+":" + prefix;
	}

	public String getSimPrefix(){
		return prefix;
	}

}

你可能感兴趣的:(笔记,java,redis)