分佈式redis鎖,spring-boot-starter-data-redis,RedisTemplatehtml
公司聊天的聊天系統,近期出現多個客服併發接待同一個客戶的記錄,經排查,是因爲代碼加的同步鎖在集羣環境下不適用,java
咱們的客服系統是2條服務器,redis中緩存session,故考慮經過加redis分佈式鎖來解決該問題.redis
根據實際狀況,只針對單臺redis實例,代碼邏輯先獲取鎖再執行操做.spring
代碼體系用的是spring-boot-starter-data-redis,因此訪問redis的客戶端就是 RedisTemplate<String, String> redisTemplate緩存
具體的加鎖和解鎖方法,用到了lua腳原本操做,儘量的保證操做的原子性.服務器
腳本封裝入 DefaultRedisScript對象中,再使用RedisTemplate來執行,官方介紹 session
https://docs.spring.io/spring-data/redis/docs/1.6.2.RELEASE/reference/html/併發
lua腳本操做redis相關命令理解,請參考 http://redisdoc.com/string/index.html分佈式
1,獲取鎖lua腳本 spring-boot
redis.call('set', KEYS[1], ARGV[1],ARGV[2],ARGV[3],ARGV[4]) if redis.call('get',KEYS[1]) == ARGV[1] then return true else return false end
參數對應 set( key,reqId,NX,PX,時間毫秒)
原子操做 set NX參數,不存在key則設置key-value, PX 並同時設置key超時毫秒數,這裏爲了返回參數判斷,加入get 獲取key的值是否等於我設置的值,是則獲取鎖成功.
注意 expireTime 是 int類型的,RedisTemplate 底層要轉字符串的,因此入參是記得加 String.valueOf(expireTime),不然會出現轉換異常,固然只是我在處理得時候遇到了,特別說下.
固然這是個人蔘考實現,要有更好建議,還請提出.
2,釋放鎖lua腳本
if redis.call('get', KEYS[1]) == ARGV[1] then redis.call('del', KEYS[1]) return true else return false end
參數對應 get( key) == reqId 是true則直接刪除key釋放鎖
這個實現參考了另外一篇博客,那個是用Jedis作客戶端而我在RedisTemplate底層調用中沒找到類似功能的方法,因此才改變思路都用lua腳本操做,後面附博客連接。
3,貼上代碼以下
package com.xxxxx.kefu.utils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import java.util.Collections;
public class RedisLockTool {
/**不存在則設置k - v 值*/
private static final String SET_IF_NOT_EXIST = "NX";
/**設置毫秒超時*/
private static final String SET_WITH_EXPIRE_TIME = "PX";
/**
* 嘗試獲取分佈式鎖
*/
public static boolean getLock(RedisTemplate<String, String> redisTemplate, String lockKey, String requestId, int expireTime) {
String script = "redis.call('set', KEYS[1], ARGV[1],ARGV[2],ARGV[3],ARGV[4]) if redis.call('get',KEYS[1]) == ARGV[1] then return true else return false end";
DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<Boolean>(script,Boolean.class);
Boolean result = redisTemplate.execute(redisScript,Collections.singletonList(lockKey), requestId,SET_IF_NOT_EXIST,SET_WITH_EXPIRE_TIME,String.valueOf(expireTime));
if (result != null && result) {
return true;
}
return false;
}
/**
* 釋放分佈式鎖
*/
public static boolean unLock(RedisTemplate<String, String> redisTemplate, String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then redis.call('del', KEYS[1]) return true else return false end";
DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<Boolean>(script,Boolean.class);
Boolean ok = redisTemplate.execute(redisScript,Collections.singletonList(lockKey),requestId);
if(ok != null && ok){
return true;
}else{
return false;
}
}
}
4,參考博客
https://yq.aliyun.com/articles/307547
裏面指出了,其餘實現方法的一些問題,在某些狀況下不能確保操做原子性,看了理解以後,我發現同事以前用的redis鎖在刪除鎖時沒有判斷客戶端id,也就是裏面指出的錯誤示例,極端狀況下會出現。
參考改造出了這個實現,我的以爲是沒有什麼大問題的,若有發現不妥之處,還請指教.
5,查閱的其餘連接
https://docs.spring.io/spring-data/redis/docs/1.6.2.RELEASE/reference/html/ RedisTemplate調用lua腳本示例
http://redisdoc.com/string/index.html lua腳本操做redis 示例
http://ifeve.com/redis-lock/ 官方redis文檔,提到單實例和多實例的分佈式解決方案