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