springboot利用redis实现分布式锁(redis为单机模式)
1.pom文件添加redis支持
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
2.application.properties或者(application.yml)添加redis配置
spring.redis.database=1 spring.redis.host=172.xx.xx.xx spring.redis.password=123456 spring.redis.port=6379
上面的spring.redis.host替换成自己的redis服务地址,如果没有用到密码则删除spring.redis.password配置即可
3.redis工具类
package com.example.demo; import com.example.demo.extend.FastJsonRedisSerializer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import java.util.Collections; import java.util.List; @Component public class RedisUtil { @Autowired private RedisTemplate<String,String> template; @PostConstruct public void init(){ template.setKeySerializer(template.getStringSerializer()); template.setValueSerializer(template.getStringSerializer()); template.setHashKeySerializer(template.getStringSerializer()); template.setHashValueSerializer(template.getStringSerializer()); } /** * 通过lua脚本 加锁并设置过期时间 * @param key 锁key值 * @param value 锁value值 * @param expire 过期时间,单位秒 * @return true:加锁成功,false:加锁失败 */ public boolean getLock(String key,String value,String expire){ DefaultRedisScript<String> redisScript = new DefaultRedisScript<String>(); redisScript.setResultType(String.class); String strScript = ""; strScript +=" if redis.call('setNx',KEYS[1],ARGV[1])==1 then "; strScript +=" return redis.call('expire',KEYS[1],ARGV[2]) "; strScript +=" else"; strScript +=" return 0 "; strScript +=" end "; redisScript.setScriptText(strScript); try{ Object result = this.template.execute(redisScript,template.getStringSerializer(),template.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 releaseLock(String key,String value){ DefaultRedisScript<String> redisScript = new DefaultRedisScript<>(); redisScript.setResultType(String.class); String strScript = ""; strScript +="if redis.call('get',KEYS[1]) == ARGV[1] then "; strScript +=" return redis.call('del',KEYS[1]) "; strScript +="else "; strScript +=" return 0 "; strScript +="end "; redisScript.setScriptText(strScript); try{ Object result = this.template.execute(redisScript,template.getStringSerializer(),template.getStringSerializer(), Collections.singletonList(key),value); return "1".equals(""+result); }catch (Exception e){ //可以自己做异常处理 return false; } } }
redis锁用到的是setNx命令,这个命令的意思是如果redis里面存在这个key则不再添加,如果key不存在则添加成功,当setNx设置成功之后再给这个值设置一个超期时间来防止出现极端情况(断网,服务终止)导致锁没有被及时释放的情况。
上面的加锁和解锁都是通过lua脚本进行,redis里面lua脚本执行时是原子操作,可以保证加锁和设置超时同时成功或者失败,不会出现设置值成功 添加超时时间失败的情况
4.使用
在需要加锁的地方注入RedisUtil对象即可。有问题的可以留言一起探讨细节问题
@Autowired private RedisLockUtil redisUtil;
boolean lock = this.redisUtil.getLock("FORM_SUBMIT"+formId,formId,"2"); if(!lock){ //未获得锁 throw new ServiceException("当前已经有任务在执行!"); } //---------执行业务逻辑 if(lock){ //释放锁不关心成功与否 this.redisUtil.releaseLock("FORM_SUBMIT"+formId,formId); }
上面的业务逻辑最好放在try catch中执行,释放锁的代码放到finally里面执行。
5.考虑各种情况下会不会导致bug的出现
5.1:加锁失败
拿不到锁业务逻辑不执行,没问题
5.2:加锁成功,释放锁失败
网络原因或者其他原因没有释放掉,没关系 ,超时时间过了就会自己释放,没问题
5.3:加锁成功,业务执行时间过长,锁已经被redis自己释放
此种情况需要根据业务的实际情况设置合理的超时时间,可能会出问题,原因在于 基于分布式的系统是无法避免类似的问题,具体可以参考如下博客的文章,
此文章引入了 关于Redis分布式锁的安全性问题,在分布式系统专家Martin Kleppmann和Redis的作者antirez之间发生过的一场争论,内容很精彩。https://blog.csdn.net/paincupid/article/details/75094550