springboot 中单机 redis 实现分布式锁

loviezhang 2020-01-03

在微服务中经常需要使用分布式锁,来执行一些任务。例如定期删除过期数据,在多个服务中只需要一个去执行即可。

以下说明非严格意义的分布式锁,因为 redis 实现严格意义的分布式锁还是比较复杂的,对于日常简单使用使用如下简单方法即可。即偶尔不执行任务不影响业务。

实现要点

1)获得锁、释放锁需要是原子操作。要么获取成功,要么失败。释放要么成功,要么失败

2)任务完成需要自己释放自己的锁,不能释放别人的锁。

3)锁要有过期时间限制,防止任务崩溃没有释放锁,导致其他节点无法获得锁。

4)执行节点超时长时间不释放锁,到下次任务开始执行并行存在的情况

要考虑的风险点

1)获取锁失败,偶尔不执行任务要不影响业务或告警人工干预

2)redis 宕机,导致无法获取锁

方案一:低版本使用 jedis 实现

1 添加依赖,高版本或低版本有些方法可能没有

<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><scope>compile</scope></dependency>
<dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>2.10.2</version></dependency>

2 实现方法

@Slf4j
public abstract class AbsDistributeLock {

    private Jedis jedis;

    public void initDistributeLock(String ip, int port, Integer database, String password) {
        jedis = new Jedis(ip, port);
        if (password != null && !password.isEmpty()) {
            jedis.auth(password.trim());
        }
        if (database == null || database < 0 || database > 15) {
            database = 0;
        }
        jedis.select(database);
    }

    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;

    /**
     * 具体的任务需要子类去实现
     *
     * @throws RTException 分布式锁异常
     */
    public abstract void taskService() throws RTException;

    /**
     * 任一执行,ANY OF
     * 所有节点任意一个执行任务 taskService 即可,没有获得锁的节点不执行任务
     *
     * @param lockKey    lockKey
     * @param keyValue   keyValue
     * @param expireTime 过期时间 ms
     */
    public void onlyOneNodeExecute(String lockKey, String keyValue, int expireTime) {
        boolean getLock = false;
        try {
            if ((getLock = getDistributedLock(jedis, lockKey, keyValue, expireTime))) {
                taskService();
            }
        } finally {
            if (getLock) {
                releaseDistributedLock(jedis, lockKey, keyValue);
            }
        }
    }

    /**
     * 所有串行执行,ALL IN LINE
     * 所有节点都必须执行该任务,每次只能一个执行。
     *
     * @param lockKey    lockKey
     * @param keyValue   keyValue
     * @param expireTime 过期时间 ms
     */
    public void allNodeExecute(String lockKey, String keyValue, int expireTime) {
        try {
            while (!(getDistributedLock(jedis, lockKey, keyValue, expireTime))) {
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    log.info(e.getMessage());
                }
            }
            taskService();
        } finally {
            releaseDistributedLock(jedis, lockKey, keyValue);
        }
    }

    /**
     * @param jedis      客户端
     * @param lockKey    key
     * @param keyValue   key值
     * @param expireTime 过期时间,ms
     */
    public static boolean getDistributedLock(Jedis jedis, String lockKey, String keyValue, int expireTime) {
        String result = jedis.set(lockKey, keyValue, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
        if (LOCK_SUCCESS.equals(result)) {
            log.info("ip:[{}] get lock:[{}], value:[{}], getLock result:[{}]", IpUtil.getLocalIpAddr(), lockKey, keyValue, result);
            return true;
        } else {
            return false;
        }
    }

    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String keyValue) {
        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(keyValue));
        log.info("ip:[{}] release lock:[{}], value:[{}], release result: [{}]", IpUtil.getLocalIpAddr(), lockKey, keyValue, result);
        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }

}

方案二:高版本的springboot,使用 lua 脚本执行

1 添加依赖

<dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
        <version>2.2.0.RELEASE</version>
      </dependency>

2 代码实现

@Slf4j
public abstract class AbsDistributeLockLua {

    private RedisTemplate<String, String> redisTemplate;

    public AbsDistributeLockLua(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    /**
     * 具体的任务需要子类去实现
     *
     * @throws RTException 分布式锁异常
     */
    public abstract void taskService() throws RTException;

    /**
     * 任一执行,ANY OF
     * 所有节点任意一个执行任务 taskService 即可,没有获得锁的节点不执行任务
     *
     * @param lockKey    lockKey
     * @param keyValue   keyValue
     * @param expireTime 过期时间 ms
     */
    public void onlyOneNodeExecute(String lockKey, String keyValue, int expireTime) {
        boolean getLock = false;
        try {
            if ((getLock = getDistributeLock(redisTemplate, lockKey, keyValue, expireTime))) {
                taskService();
            }
        } finally {
            if (getLock) {
                releaseDistributeLock(redisTemplate, lockKey, keyValue);
            }
        }
    }

    /**
     * 所有串行执行,ALL IN LINE
     * 所有节点都必须执行该任务,每次只能一个执行。
     *
     * @param lockKey    lockKey
     * @param keyValue   keyValue
     * @param expireTime 过期时间 ms
     */
    public void allNodeExecute(String lockKey, String keyValue, int expireTime) {
        try {
            while (!(getDistributeLock(redisTemplate, lockKey, keyValue, expireTime))) {
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    log.info(e.getMessage());
                }
            }
            taskService();
        } finally {
            releaseDistributeLock(redisTemplate, lockKey, keyValue);
        }
    }

    /**
     * 通过lua脚本 加锁并设置过期时间
     *
     * @param key    锁key值
     * @param value  锁value值
     * @param expire 过期时间,单位毫秒
     * @return true:加锁成功,false:加锁失败
     */
    public boolean getDistributeLock(RedisTemplate<String, String> redisTemplate, String key, String value, int expire) {
        DefaultRedisScript<String> redisScript = new DefaultRedisScript<String>();
        redisScript.setResultType(String.class);
        String strScript = "if redis.call(‘setNx‘,KEYS[1],ARGV[1])==1 then return redis.call(‘pexpire‘,KEYS[1],ARGV[2]) else return 0 end";
        redisScript.setScriptText(strScript);
        try {
            Object result = redisTemplate.execute(redisScript, redisTemplate.getStringSerializer(), redisTemplate.getStringSerializer(), Collections.singletonList(key), value, expire);
            System.out.println("redis返回:" + result);
            return "1".equals("" + result);
        } catch (Exception e) {
            //可以自己做异常处理
            return false;
        }
    }

    /**
     * 通过lua脚本释放锁
     *
     * @param key   锁key值
     * @param value 锁value值(仅当redis里面的value值和传入的相同时才释放,避免释放其他线程的锁)
     * @return true:释放锁成功,false:释放锁失败(可能已过期或者已被释放)
     */
    public boolean releaseDistributeLock(RedisTemplate<String, String> redisTemplate, String key, String value) {
        DefaultRedisScript<String> redisScript = new DefaultRedisScript<>();
        redisScript.setResultType(String.class);
        String strScript = "if redis.call(‘get‘, KEYS[1]) == ARGV[1] then return redis.call(‘del‘, KEYS[1]) else return 0 end";
        redisScript.setScriptText(strScript);
        try {
            Object result = redisTemplate.execute(redisScript, redisTemplate.getStringSerializer(), redisTemplate.getStringSerializer(), Collections.singletonList(key), value);
            return "1".equals("" + result);
        } catch (Exception e) {
            //可以自己做异常处理
            return false;
        }
    }
}

代码地址:https://github.com/crazyCodeLove/distribute-lock

参考文献:

https://www.cnblogs.com/bcde/p/11132479.html

https://blog.csdn.net/u013985664/article/details/94459529

相关推荐

DiamondTao / 0评论 2020-08-30