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