springboot 中单机 redis 实现分布式锁
程序员文章站
2022-03-25 18:48:07
在微服务中经常需要使用分布式锁,来执行一些任务。例如定期删除过期数据,在多个服务中只需要一个去执行即可。 以下说明非严格意义的分布式锁,因为 redis 实现严格意义的分布式锁还是比较复杂的,对于日常简单使用使用如下简单方法即可。即偶尔不执行任务不影响业务。 实现要点 1)获得锁、释放锁需要是原子操 ......
在微服务中经常需要使用分布式锁,来执行一些任务。例如定期删除过期数据,在多个服务中只需要一个去执行即可。
以下说明非严格意义的分布式锁,因为 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