分布式锁的实现

分布式锁的概念

	日常开发中,秒杀下单、抢红包等等业务场景,都需要用到分布式锁。而Redis非常适合作为分布式锁使用。大系统,多系统迎来的是多应用之间的一致性。

实现分布式锁的方式

1.基于数据库

2.基于redis

3.基于redis的redission

4.基于zookeeper

具体实现redis分布式锁的方式

1.SETNX + EXPIRE

// 获取锁 基于 setnx 和 expire 此方法不会保证原子性 可以使用lua脚本(redis 又演变出 set加过期时间的方式)
 public boolean getLockNx(Jedis jedis, String lockeKey, String requestId, Long expireTime) {
        Long setnx = jedis.setnx(lockeKey, requestId);
        if (Objects.equals(setnx, 1)) {
            jedis.expire(lockeKey, new Long(expireTime).intValue());
            return true;
        }
        return false;
    }

问题:
操作化为两步操作,导致形成了非原子性。假如setnx执行后,机器挂机了,就会导致锁一直在锁,无法释放,形成死锁。

2.SETNX + value值是(系统时间+过期时间)

/**
     *  最终加强分布式锁
     *
     * @param lock key值
     * @return 是否获取到
     */
    public boolean lock(String lock, long expireTime){
        // 利用lambda表达式
        return (Boolean) redisTemplate.execute((RedisCallback) connection -> {

            long expireAt = System.currentTimeMillis() + expireTime + 1;
            Boolean acquire = connection.setNX(lock.getBytes(), String.valueOf(expireAt).getBytes());


            if (acquire) {
                return true;
            } else {
                //锁存在 查看是否过期时间
                byte[] value = connection.get(lock.getBytes());

                if (Objects.nonNull(value) && value.length > 0) {

                    long expire = Long.parseLong(new String(value));
                    // 如果锁已经过期 判断过期时间是否超时
                    if (expire < System.currentTimeMillis()) {
                        // 重新加锁,防止死锁
                        //锁超时 重新加锁 使用getset 原子操作
                        //重新加锁 防止死锁 判断重新加锁时间戳,确定是否加锁成功 old
                        //查看老的时间值是否被其他线程修改 值是否被其他线程修改

                        // 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才可以加锁
                        // 该锁没有保存持有者的唯一标识,可能被别的客户端释放/解锁

                        //到这一步其实还是有问题的,如果一个请求更新缓存的时间比锁的有效期还要长,导致在缓存更新过程中锁就失效了,此时另一个请求就会获取到锁,但前一个请求在缓存更新完毕的时候,直接删除锁的话就会出现误删其它请求创建的锁的情况。
                        byte[] oldValue = connection.getSet(lock.getBytes(), String.valueOf(System.currentTimeMillis() + expireTime + 1).getBytes());
                        return Long.parseLong(new String(oldValue)) < System.currentTimeMillis();
                    }
                }
            }
            return false;
        });
    }

问题:
1.「锁被别的线程误删」。假设线程a执行完后,去释放锁。但是它不知道当前的锁可能是线程b持有的(线程a去释放锁时,有可能过期时间已经到了,此时线程b进来占有了锁)。那线程a就把线程b的锁释放掉了,但是线程b临界区业务代码可能都还没执行完呢。
2.「锁过期释放了,业务还没执行完」。假设线程a获取锁成功,一直在执行临界区的代码。但是100s过去后,它还没执行完。但是,这时候锁已经过期了,此时线程b又请求过来。显然线程b就可以获得锁成功,也开始执行临界区的代码。那么问题就来了,临界区的业务代码都不是严格串行执行的啦。

3.使用Lua脚本(包含SETNX + EXPIRE两条指令)

   /**
     * 释放分布式锁 基于lua脚本释放 保证了原子性 和释放锁是符合自己的 解决并发问题
     *
     * @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 (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }

问题:
例如,操作共享资源的时间「最慢」可能需要 15s,而我们却只设置了 10s 过期,那这就存在锁提前过期的风险。

过期时间太短,那增大冗余时间,例如设置过期时间为 20s,这样总可以了吧?

这样确实可以「缓解」这个问题,降低出问题的概率,但依旧无法「彻底解决」问题。

为什么?

原因在于,客户端在拿到锁之后,在操作共享资源时,遇到的场景有可能是很复杂的,例如,程序内部发生异常、网络请求超时等等。

既然是「预估」时间,也只能是大致计算,除非你能预料并覆盖到所有导致耗时变长的场景,但这其实很难。

有什么更好的解决方案吗?

别急,关于这个问题,我会在后面详细来讲对应的解决方案。

4.SET的扩展命令(SET EX PX NX)

保证SETNX + EXPIRE两条指令的原子性,我们还可以巧用Redis的SET指令扩展参数!(SET key value[EX seconds][PX milliseconds][NX|XX]),它也是原子性的!

/**
     * set 扩展命令执行
     * 问题一:「锁过期释放了,业务还没执行完」。假设线程a获取锁成功,一直在执行临界区的代码。但是100s过去后,它还没执行完。但是,这时候锁已经过期了,此时线程b又请求过来。显然线程b就可以获得锁成功,也开始执行临界区的代码。那么问题就来了,临界区的业务代码都不是严格串行执行的啦。
     * 问题二:「锁被别的线程误删」。假设线程a执行完后,去释放锁。但是它不知道当前的锁可能是线程b持有的(线程a去释放锁时,有可能过期时间已经到了,此时线程b进来占有了锁)。那线程a就把线程b的锁释放掉了,但是线程b临界区业务代码可能都还没执行完呢。
     * @param lock
     * @param value
     * @param expireTime
     * @return
     */
    public Boolean lock(String lock, String value, long expireTime){

        return (Boolean)redisTemplate.execute((RedisCallback<Boolean>) connection -> {

            RedisSerializer keySerializer = redisTemplate.getKeySerializer();
            RedisSerializer valueSerializer = redisTemplate.getValueSerializer();

            Object execute = connection.execute("set",
                    keySerializer.serialize(lock),
                    valueSerializer.serialize(value),
                    SafeEncoder.encode("NX"),
                    SafeEncoder.encode("EX"),
                    Protocol.toByteArray(expireTime)
            );
            return Objects.nonNull(execute);
        });

    }

业务执行


if(jedis.set(key_resource_id, lock_value, "NX", "EX", 100s) == 1{ //加锁
    try {
        do something  //业务处理
    }catch(){
  }
  finally {
       jedis.del(key_resource_id); //释放锁
    }
}

问题:
问题一:「锁过期释放了,业务还没执行完」。假设线程a获取锁成功,一直在执行临界区的代码。但是100s过去后,它还没执行完。但是,这时候锁已经过期了,此时线程b又请求过来。显然线程b就可以获得锁成功,也开始执行临界区的代码。那么问题就来了,临界区的业务代码都不是严格串行执行的啦。
问题二:「锁被别的线程误删」。假设线程a执行完后,去释放锁。但是它不知道当前的锁可能是线程b持有的(线程a去释放锁时,有可能过期时间已经到了,此时线程b进来占有了锁)。那线程a就把线程b的锁释放掉了,但是线程b临界区业务代码可能都还没执行完呢。

5.SET EX PX NX + 校验唯一随机值,再释放锁

伪代码

if(jedis.set(key_resource_id, uni_request_id, "NX", "EX", 100s) == 1{ //加锁
    try {
        do something  //业务处理
    }catch(){
  }
  finally {
       //判断是不是当前线程加的锁,是才释放
       if (uni_request_id.equals(jedis.get(key_resource_id))) {
        jedis.del(lockKey); //释放锁
        }
    }
}

问题:「判断是不是当前线程加的锁」和「释放锁」不是一个原子操作。如果调用jedis.del()释放锁的时候,可能这把锁已经不属于当前客户端,会解除他人加的锁。

伪代码

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

问题:

「锁过期释放了,业务还没执行完」。假设线程a获取锁成功,一直在执行临界区的代码。但是100s过去后,它还没执行完。但是,这时候锁已经过期了,此时线程b又请求过来。显然线程b就可以获得锁成功,也开始执行临界区的代码。那么问题就来了,临界区的业务代码都不是严格串行执行的啦。

6.Redisson框架

还是可能存在「锁过期释放,业务没执行完」的问题。有些小伙伴认为,稍微把锁过期时间设置长一些就可以啦。其实我们设想一下,是否可以给获得锁的线程,开启一个定时守护线程,每隔一段时间检查锁是否还存在,存在则对锁的过期时间延长,防止锁过期提前释放。

分布式锁的实现_第1张图片
只要线程一加锁成功,就会启动一个watch dog看门狗,它是一个后台线程,会每隔10秒检查一下,如果线程1还持有锁,那么就会不断的延长锁key的生存时间。因此,Redisson就是使用Redisson解决了「锁过期释放,业务没执行完」问题。

多机实现的分布式锁Redlock+Redisson

这个不会

Redisson实现了redLock版本的锁,有兴趣的小伙伴,可以去了解一下哈~

你可能感兴趣的:(分布式,java,redis)