redis分布式锁机制,及其原理,及其实现

redis分布式锁机制,及其原理

分布式系统加锁时出现的问题及其解决方案

问题1:由于网络延迟,对锁的获取造成幂等性问题

  描述:线程 A 检查锁是否存在(get)—>否—>加锁(set),在 A 发起加锁命令但是还没有加锁成功的时候,可能线程 B 已经完成了 set 操作,锁被 B 获得,但是 A 也发起了加锁请求,由于 set 命令并不检查 key 的存在,B 的锁很可能会被 A 的 set 操作破坏。
  解决:使用setx幂等性操作,解决由于网络延时,造成value值的覆盖问题

问题二:当获得锁的服务崩溃,导致锁无法正常释放(重点问题)

  描述:如果加锁的线程出现异常 crash 了而不能及时删除锁,则会导致锁一直无法被正确释放,资源处于一直被占有,别的线程处于一直等待的状态。
  解决:使用 redis 的超时机制来达到这个目的。超时时间视不同的业务场景而定,一般是最大允许等待时间。

问题三:加锁后,如何有效的防止锁被篡改

  解决:redis 提供了 pipeline 和事务操作来保证多个命令可以在一个事务内全部完成从而减少多次网络请求带来的开销,watch 命令又可以在事务开始执行之前对所要操作的 key 执行监测,从而保证了事务的完整性和一致性。因此,为了防止锁篡改,可以在加锁完成之后对锁进行 watch 操作,一旦锁发生变化,则终止事务,回滚操作。

问题四:提供锁的宿主机( redis 服务器) crash 导致锁不能被正确建立和释放该如何处理

  描述:客户端A获取锁成功,过期时间30秒,A在某个操作上阻塞了50秒。30秒时间到了,锁自动释放了。客户端B获取到了对应同一个资源的锁。客户端A从阻塞中恢复过来,释放掉了客户端B持有的锁。
  解决:1.过期时间保证大于业务执行时间,2.保证锁不会被误删除。这个问题可以通过设置value(uuid),且保证在足够长的一段时间内在所有客户端的所有获取锁的请求中都是唯一的。删除时,需要对比本地和缓存的value(uuid)是否相同。

加锁和解锁的流程

加锁流程:

  1. 使用setx命令,设置超时时间和value(uuid),

解锁流程:

  1. 使用lua脚本将get和del的操作整合成原子性操作

代码

public class RedisLock {
    private static final Logger logger = LoggerFactory.getLogger(RedisLock.class);

    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";

    /**
     * 尝试获取分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param uuid 请求标识(用于防止,解锁异常)
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String uuid, int expireTime) {

        String result = jedis.set(lockKey, uuid, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;

    }

    private static final Long RELEASE_SUCCESS = 1L;

    /**
     * 释放分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param uuid 请求标识
     * @return 是否释放成功
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String uuid) {
//        使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为uuid。eval()方法是将Lua代码交给Redis服务端执行。
        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(uuid));

        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;

    }

}

猜你喜欢

转载自blog.csdn.net/c_royi/article/details/86584064