Redis分布式鎖
* 分布鎖滿足兩個條件,一個是加有效時間的鎖,一個是高性能解鎖
* 采用redis命令setnx(set if not exist)、setex(set expire value)實現

* 【千萬記住】解鎖流程不能遺漏,否則導致任務執行一次就永不過期
* 將加鎖代碼和任務邏輯放在try,catch代碼塊,將解鎖流程放在finally
package com.concurrent.schedule; import com.concurrent.util.IpUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import javax.management.MalformedObjectNameException; import java.util.concurrent.TimeUnit; /** * Created by zhangjiawen on 2019/8/13. */ @Component public class RedisSchedule { private Logger logger = LoggerFactory.getLogger(this.getClass()); @Autowired private StringRedisTemplate redisTemplate; private static final String publicKey="CommonKey"; Boolean lock=false; @Scheduled(cron="*/5 * * * * *") public void setNx() { System.out.println("TimeUnit.SECONDS"+TimeUnit.SECONDS); try { String ipConfig=IpUtil.getIp() + ":" + IpUtil.getPort(); lock= redisTemplate.opsForValue().setIfAbsent(publicKey, ipConfig); if(!lock){ String value=redisTemplate.opsForValue().get(publicKey); logger.warn("key have exist belong to:"+value); }else{ redisTemplate.opsForValue().set(publicKey,ipConfig,60, TimeUnit.SECONDS); //獲取鎖成功 logger.info("start lock lockNxExJob success"); Thread.sleep(5000); } }catch (Exception e){ logger.error("setNx error!"); e.printStackTrace(); }finally { redisTemplate.delete(publicKey); } } }
分布式鎖setnx、setex的缺陷,在setnx和setex中間發生了服務down機
* 從Redis宕機講解分布式鎖執行的異常場景流程
* 從Server服務宕機講解分布式鎖執行的異常場景流程
* 在setnx和setex中間發生了服務down機 那么key將沒有超時時間 會一直存在,新的請求永遠進不來

解決方案:
由於setnx與setex是分步進行,那么我們將兩步合成一步,放在同一個原子中即可
* 怎么一次性執行過一條命令而不會出現問題,采用Lua腳本
* Redis從2.6之后支持setnx、setex連用
* Lua簡介
* 從 Redis 2.6.0 版本開始,通過內置的 Lua 解釋器,可以使用 EVAL 命令對 Lua 腳本進行求值。
* Redis 使用單個 Lua 解釋器去運行所有腳本,並且, Redis 也保證腳本會以原子性(atomic)的方式執行:當某個腳本正在運行的時候,不會有其他腳本或 Redis 命令被執行。這和使用 MULTI / EXEC 包圍的事務很類似。在其他別的客戶端看來,腳本的效果(effect)要么是不可見的(not visible),要么就是已完成的(already completed)。
* Lua腳本配置流程
* 1、在resource目錄下面新增一個后綴名為.lua結尾的文件
* 2、編寫lua腳本
local lockKey = KEYS[1] local lockTime = KEYS[2] local lockValue = KEYS[3] -- setnx info local result_1 = redis.call('SETNX', lockKey, lockValue) if result_1 == 1 then local result_2= redis.call('SETEX', lockKey,lockTime, lockValue) return result_2 else return 'faild' end
* 3、傳入lua腳本的key和arg
* 4、調用redisTemplate.execute方法執行腳本
* Lua腳本結合RedisTempalte實戰演練
package com.concurrent.schedule; import com.concurrent.util.IpUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.ClassPathResource; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scripting.support.ResourceScriptSource; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.List; /** * Created by zhangjiawen on 2019/8/13. */ @Component public class LuaRedisSchedulePlus { private Logger logger = LoggerFactory.getLogger(this.getClass()); @Autowired private RedisTemplate redisTemplate; private static final String publicKey="DEMO_COMMON_KEY"; Boolean lock=false; private DefaultRedisScript<String> lockScript; @Scheduled(cron="*/5 * * * * *") public void setNx() { try { String ipConfig=IpUtil.getIp() + ":" + IpUtil.getPort(); lock= luaExpress(publicKey,"30",ipConfig); if(!lock){ String value=(String) redisTemplate.opsForValue().get(publicKey); logger.warn("key have exist belong to:"+value); }else{ //獲取鎖成功 logger.info("start lock lockNxExJob success"); // Thread.sleep(5000); } }catch (Exception e){ logger.error("setNx error!"); e.printStackTrace(); }finally { // redisTemplate.delete(publicKey); } } /** * 獲取lua結果 * @param key * @param value * @return */ public Boolean luaExpress(String key,String time,String value) { lockScript = new DefaultRedisScript<String>(); lockScript.setScriptSource( new ResourceScriptSource(new ClassPathResource("add.lua"))); lockScript.setResultType(String.class); // 封裝參數 List<Object> keyList = new ArrayList<Object>(); keyList.add(key); keyList.add(time); keyList.add(value); String result= (String)redisTemplate.execute(lockScript, keyList); System.out.println(result); logger.info("redis set result:"+result); if (!"ok".equals(result.toLowerCase())){ return false; } return true; } }
* Lua腳本其他工作場景剖析和演練
* lua eval http://doc.redisfans.com/script/eval.html
lua腳本做高可用分布式鎖的優化
-當某個鎖需要持有的時間小於鎖超時時間時會出現兩個進程同時執行任務的情況,
這時候如果進程沒限制只有占有這把鎖的人才能解鎖的原則就會出現,
A解了B的鎖。
解決方案:刪除key的時候判斷一下value是否是當前value,是的話刪除,否則不執行刪除,將原來代碼finally修改如下
}catch (Exception e){ logger.error("setNx error!"); e.printStackTrace(); }finally { // redisTemplate.delete(publicKey); // releaseLock(publicKey,ipConfig); }
/** * 釋放鎖操作 * @param key * @param value * @return */ private boolean releaseLock(String key, String value) { lockScript = new DefaultRedisScript<Boolean>(); lockScript.setScriptSource( new ResourceScriptSource(new ClassPathResource("unlock.lua"))); lockScript.setResultType(Boolean.class); // 封裝參數 List<Object> keyList = new ArrayList<Object>(); keyList.add(key); keyList.add(value); Boolean result = (Boolean) redisTemplate.execute(lockScript, keyList); return result; }
添加解鎖lua腳本
local lockKey = KEYS[1] local lockValue = KEYS[2] -- get key local result_1 = redis.call('get', lockKey) if result_1 == lockValue then local result_2= redis.call('del', lockKey) return result_2 else return false end
源碼地址:https://gitee.com/jiawenzhang/gaobingfa.git
