Redis(八):Redis分布式锁实现

其实Redis分布式锁的介绍,前面几篇文章中都要介绍到,只是没有独立成篇,今天把其单独摘出来,便于学习和使用。

1、概述

当多个进程不在同一个系统中时,用分布式锁控制多个进程对资源的操作或者访问。

分布式锁的实现要保证几个基本点:

  • 1、互斥性:任意时刻,只有一个资源能够获取到锁
  • 2、容灾性:能够在未成功释放锁的情况下,一定时限内能够恢复锁的正常功能
  • 3、统一性:加锁和解锁保证同一资源来进行操作

分布式锁的实现方式有很多种:

  • 1、数据库乐观锁方式(数据库加一个版本号)
  • 2、基于Redis的分布式锁
  • 3、基于ZK的分布式锁(Zookeeper基础(五):分布式锁)

2、Redis单机实现

2.1 原理

https://mp.weixin.qq.com/s?__biz=MzU0OTk3ODQ3Ng==&mid=2247483893&idx=1&sn=32e7051116ab60e41f72e6c6e29876d9&chksm=fba6e9f6ccd160e0c9fa2ce4ea1051891482a95b1483a63d89d71b15b33afcdc1f2bec17c03c&mpshare=1&scene=1&srcid=0416Kx8ryElbpy4xfrPkSSdB&key=1eff032c36dd9b3716bab5844171cca99a4ea696da85eed0e4b2b7ea5c39a665110b82b4c975d2fd65c396e91f4c7b3e8590c2573c6b8925de0df7daa886be53d793e7f06b2c146270f7c0a5963dd26a&ascene=1&uin=MTg2ODMyMTYxNQ%3D%3D&devicetype=Windows+10&version=62060739&lang=zh_CN&pass_ticket=y1D2AijXbuJ8HCPhyIi0qPdkT0TXqKFYo%2FmW07fgvW%2FXxWFJiJjhjTsnInShv0ap

Redisson底层原理简单描述:
先判断一个key存在不存在,如果不存在,则set key,同时设置过期时间和value(1),
这个过程使用lua脚本来实现,可以保证多个命令的原子性,当业务完成以后,删除key;
如果存在说明已经有别的线程获取锁了,那么就循环等待一段时间后再去获取锁

如果是可重入锁呢:
先判断一个key存在不存在,如果不存在,则set key,同时设置过期时间和value(线程id:1),
如果存在,则判断value中的线程id是否是当前线程的id,如果是,说明是可重入锁,则value+1,变成(线程id:2),如果不是,说明是别的线程来获取锁,则获取失败;这个过程同样使用lua脚本一次性提交,保证原子性。

如何防止业务还没执行完,但是锁key过期呢,可以在线程加锁成功后,启动一个后台进程看门狗,去定时检查,如果线程还持有锁,就延长key的生存时间——Redisson就是这样实现的。

其实Jedis也有现成的实现方式,单机、集群、分片都有实现,底层原理是利用连用setnx、setex指令
(Redis从2.6之后支持setnx、setex连用),核心是设置value和设置过期时间包装成一个原子操作

jedis.set(key, value, "NX", "PX", expire)
image.png

注:setnx和setex都是原子性的
SETNX key value
将 key 的值设为 value ,当且仅当 key 不存在;若给定的 key 已经存在,则 SETNX 不做任何动作。
相当于是 EXISTS 、SET 两个命令连用
SETEX key seconds value
将value关联到key, 并将key的生存时间设为seconds(以秒为单位);如果key 已经存在,SETEX将重写旧值;
相当于是SET、EXPIRE两个命令连用

2.1 实现

public class RedisTool {

    private static final String LOCK_SUCCESS = "OK";
    //NX|XX, NX -- Only set the key if it does not already exist;
   //        XX -- Only set the key if it already exist.
    private static final String SET_IF_NOT_EXIST = "NX";
    //EX|PX, expire time units: EX = seconds; PX = milliseconds
    private static final String SET_WITH_EXPIRE_TIME = "PX";

   private static volatile JedisPool jedisPool = null;

    public static JedisPool getRedisPoolUtil() {
        if(null == jedisPool ){
            synchronized (RedisTool.class){
                if(null == jedisPool){
                    GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
                    poolConfig.setMaxTotal(100);
                    poolConfig.setMaxIdle(10);
                    poolConfig.setMaxWaitMillis(100*1000);
                    poolConfig.setTestOnBorrow(true);
                    jedisPool = new JedisPool(poolConfig,"192.168.10.151",6379);
                }
            }
        }
        return jedisPool;
    }



    /**
     * 尝试获取分布式锁
     * @param lockKey 锁
     * @param requestId 请求标识
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    public static boolean tryGetDistributedLock(String lockKey, String requestId, int expireTime) {
        Jedis  jedis = jedisPool.getResource();

        try {
            String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

            if (LOCK_SUCCESS.equals(result)) {
                return true;
            }
            return false;
        }catch (Exception e){
            return false;
        }finally {
            jedisPool.returnResource(jedis);
        }

    }

    private static final Long RELEASE_SUCCESS = 1L;

    /**
     * 释放分布式锁
     * @param lockKey 锁
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public static boolean releaseDistributedLock(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";
        //如果使用的是切片shardedJedis,那么需要先获取到jedis,
        //Jedis jedis = shardedJedis.getShard(key);
        Jedis  jedis = jedisPool.getResource();

        try {
            Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
            if (RELEASE_SUCCESS.equals(result)) {
                return true;
            }
            return false;
        }catch (Exception e){
            return false;
        }finally {
            jedisPool.returnResource(jedis);
        }
    }
}

从jedis源码中可以发现上面的加锁/释放锁指令在单机jedis/ShardedJedis/JedisCluster下都能实现(jedis版本为3.0以上),但是ShardedJedis可以直接加锁,但是不能直接释放锁(没有提供eval工具方法),但是可以先
Jedis jedis = shardedJedis.getShard(key) 获得jedis,然后使用jedis.evel()来释放锁。

注:关于redisTool工具类的更优化实现见Java 函数式接口编程实例

3 、Cluster集群实现

上面介绍的分布式锁的实现在Redis Cluster集群模式下,是存在问题的,Redis Cluster集群模式介绍见Redis(四):集群模式

整个过程如下:

  1. 客户端1在Redis的节点A上拿到了锁;
  2. 节点A宕机后,客户端2发起获取锁key的请求,这时请求就会落在节点B上;
  3. 节点B由于之前并没有存储锁key,所以客户端2也可以成功获取锁,即客户端1和客户端2同时持有了同一个资源的锁。

针对这个问题。Redis作者antirez提出了RedLock算法来解决这个问题

3.1 RedLock算法

RedLock算法思路如下:

  1. 获取当前时间的毫秒数startTime;

  2. 按顺序依次向N个Redis节点执行获取锁的操作,这个获取锁的操作和前面单Redis节点获取锁的过程相同,同时锁超时时间应该远小于锁的过期时间

  3. 如果客户端向某个Redis节点获取锁失败/超时后,应立即尝试下一个Redis节点;
    失败包括Redis节点不可用或者该Redis节点上的锁已经被其他客户端持有

  4. 如果客户端成功获取到超过半数的锁时,记录当前时间endTime,同时计算整个获取锁过程的总耗时costTime = endTime - startTime,如果获取锁总共消耗的时间远小于锁的过期时间(即costTime < expireTime),则认为客户端获取锁成功,否则,认为获取锁失败

  5. 如果获取锁成功,需要重新计算锁的过期时间。它等于最初锁的有效时间减去第三步计算出来获取锁消耗的时间,即expireTime - costTime

  6. 如果最终获取锁失败,那么客户端立即向所有Redis发起释放锁的操作。(和单机释放锁的逻辑一样)

3.2 缺陷

RedLock算法虽然可以解决单点Redis分布式锁的安全性问题,但如果集群中有节点发生崩溃重启,还是会对锁的安全性有影响的。

假设一共有5个Redis节点:A, B, C, D, E。设想发生了如下的事件序列:

  1. 客户端1成功锁住了A, B, C,获取锁成功(但D和E没有锁住);
  2. 节点C崩溃重启了,但客户端1在C上加的锁没有持久化下来,丢失了;
  3. 节点C重启后,客户端2锁住了C, D, E,获取锁成功;

这样,客户端1和客户端2同时获得了锁(针对同一资源)。针对这样场景,解决方式也很简单,也就是让Redis崩溃后延迟重启,并且这个延迟时间大于锁的过期时间就好。这样等节点重启后,所有节点上的锁都已经失效了。也不存在以上出现2个客户端获取同一个资源的情况了

还有一种情况,如果客户端1获取锁后,访问共享资源操作执行任务时间过长(要么逻辑问题,要么发生了GC),导致锁过期了,而后续客户端2获取锁成功了,这样就会导致客户端1和客户端2同时操作共享资源,相当于同一个时刻出现了2个客户端获得了锁的情况。这也就是上面锁过期时间要远远大于加锁消耗的时间的原因。
服务器台数越多,出现不可预期的情况也越多,所以针对分布式锁的应用的时候需要多测试。
如果系统对共享资源有非常严格要求得情况下,还是建议需要做数据库锁的方案来补充,如飞机票或火车票座位得情况。
对于一些抢购获取,针对偶尔出现超卖,后续可以通过人工介入来处理,毕竟redis节点不是天天奔溃,同时数据库锁的方案
性能又低。

3.3 实现

redisson包已经有对redlock算法封装

public interface DistributedLock {
    /**
     * 获取锁
     * @author zhi.li
     * @return 锁标识
     */
    String acquire();

    /**
     * 释放锁
     * @author zhi.li
     * @param indentifier
     * @return
     */
    boolean release(String indentifier);
}

public class RedisDistributedRedLock implements DistributedLock {

    /**
     * redis 客户端
     */
    private RedissonClient redissonClient;

    /**
     * 分布式锁的键值
     */
    private String lockKey;

    private RLock redLock;

    /**
     * 锁的有效时间 10s
     */
    int expireTime = 10 * 1000;

    /**
     * 获取锁的超时时间
     */
    int acquireTimeout  = 500;

    public RedisDistributedRedLock(RedissonClient redissonClient, String lockKey) {
        this.redissonClient = redissonClient;
        this.lockKey = lockKey;
    }

    @Override
    public String acquire() {
        redLock = redissonClient.getLock(lockKey);
        boolean isLock;
        try{
            isLock = redLock.tryLock(acquireTimeout, expireTime, TimeUnit.MILLISECONDS);
            if(isLock){
                System.out.println(Thread.currentThread().getName() + " " + lockKey + "获得了锁");
                return null;
            }
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }

    @Override
    public boolean release(String indentifier) {
        if(null != redLock){
            redLock.unlock();
            return true;
        }

        return false;
    }
}

4、项目中调用

RedisTool 中加锁/释放锁实现后,在项目中怎么调用呢,如果直接在业务代码中调用,那一方面太麻烦了,另一方面耦合太多,如果有一天需要改动其中的逻辑,那在项目中需要改动很多地方。

这里我们使用AOP+注解来实现调用,即在需要加锁的方法上添加注解,然后再AOP中,统一加锁,释放锁。

4.1 自定义注解

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisLockAnnotation {
    int expire() default 5;

    String field() default "";

}

4.2 自定义切面

@Aspect
@Service
public class RedisLockAspect {

      //方法切点
     @Pointcut("@annotation(redisLock.RedisLockAnnotation)")
     public  void methodAspect() {

     }

    @Around("methodAspect()")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
        Signature signature = joinPoint.getSignature();
        Method method = ((MethodSignature) signature).getMethod();
        Method realMethod = joinPoint.getTarget().getClass().getDeclaredMethod(signature.getName(),method.getParameterTypes());
        RedisLockAnnotation redisLockAnnotation = realMethod.getAnnotation(RedisLockAnnotation.class);
        int expireTime = redisLockAnnotation.expire();
        String field = redisLockAnnotation.field();

        Map params = getNameAndValue(joinPoint, field);
        if (params==null){
            throw new RuntimeException("params is not allowed null");
        }
        String url = method.getDeclaringClass().getSimpleName() + "." + method.getName();
        String reqParam = JSONObject.toJSONString(params);

        //redis加锁
        String localKey = url + ":" + reqParam;
        String requestFlag = UUID.randomUUID().toString();
        boolean lock = RedisTool.tryGetDistributedLock(localKey, requestFlag, expireTime);
        if(!lock){
            return "锁已存在";
        }

        //加锁成功
        Object result = null;
        try {
            //执行方法
            result =joinPoint.proceed();
        } finally {
            //方法执行完之后进行解锁
            RedisTool.releaseDistributedLock(localKey, requestFlag);
        }

        return result;
    }


    /**
     * 获取参数Map集合
     */
    private Map getNameAndValue(ProceedingJoinPoint joinPoint, String filedList) {
        Map param = new HashMap();
        Object[] paramValues = joinPoint.getArgs();
        String[] paramNames = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
        for (int i = 0; i < paramValues.length; i++) {
            List targetFields = Arrays.asList(filedList.split(","));
            JSONObject valueDetialsJson = (JSONObject) JSONObject.toJSON(paramValues[i]);
            //得到属性
            for (int j = 0; j < targetFields.size(); j++) {
                if (valueDetialsJson.get(targetFields.get(i))!=null){
                    param.put(targetFields.get(i), valueDetialsJson.get(targetFields.get(i)));
                }
            }

        }
        if (param != null && param.size() > 0) {
            return param;
        }
        for (int i = 0; i < paramNames.length; i++) {
            param.put(paramNames[i], paramValues[i]);
        }

        return param;
    }

}

4.3 使用

public class RedidLockTest1 {

    @RedisLockAnnotation(field = "userId")
    public Object test1(String userId){
        return userId+"==";
    }

}

你可能感兴趣的:(Redis(八):Redis分布式锁实现)