萌芽
對于分布式鎖骗露,有一個不錯的客戶端Redisson荡碾,是會自動鎖續(xù)約的荡澎,詳情請自行搜索
但均践,對于這個客戶端的使用方式,個人不是很喜歡摩幔,還是更傾向Lettuce
對于鎖續(xù)約原理 不復雜(參考Redisson)彤委,無非就是用個WatchDog線程,時不時對比一下鎖的超時時間還剩余多少或衡,如果小于某個值就續(xù)約(預設時間30s,已經(jīng)過去20s了焦影,就刷新超時時間)
分析源碼
這里使用的是sping integration包里的分布式鎖(理解成官方),詳情請查閱
我們的目標是獲取存在的分布式鎖封断,通過這些鎖的key進行續(xù)約
代碼片段如下(精簡了部分內(nèi)容)
public final class RedisLockRegistry implements ExpirableLockRegistry, DisposableBean {
private static final String OBTAIN_LOCK_SCRIPT =
"local lockClientId = redis.call('GET', KEYS[1])\n" +
"if lockClientId == ARGV[1] then\n" +
" redis.call('PEXPIRE', KEYS[1], ARGV[2])\n" +
" return true\n" +
"elseif not lockClientId then\n" +
" redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2])\n" +
" return true\n" +
"end\n" +
"return false";
private final Map<String, RedisLock> locks = new ConcurrentHashMap<>();
這里我截取了兩個關鍵部分
- 申請鎖的指令
- 鎖的存放位置(私有Map)
那么目標就很明確了
第一個小目標
- 續(xù)約指令
分析申請鎖指令可得
1.如果當前發(fā)指令的clientId 就是鎖的持有者斯辰,則續(xù)約
2.如果當前clientId不存在,則鎖上
3.否則鎖失敗
這也是為啥在分布式鎖續(xù)約問題坡疼,我可以使用重入鎖的的方式續(xù)約的原因
那么問題來了彬呻,為啥這里不能直接重入鎖去刷新呢?
因為重入鎖是需要業(yè)務代碼上調(diào)用的,這樣對業(yè)務代碼侵入性太強了闸氮,而WatchDog調(diào)用剪况,本身在不同線程上,就連本地鎖都沒辦法重入(為了性能,分布式鎖也維持了一把本地鎖)
清楚指令意思之后就好辦了蒲跨,編寫續(xù)約代碼
private static final String OBTAIN_LOCK_SCRIPT_VERSION = "aa1fc9ae99657e86372b45452e5d6f71";
private static final String RENEW_LOCK_SCRIPT =
"local lockClientId = redis.call('GET', KEYS[1])\n" +
"if lockClientId == ARGV[1] then\n" +
" redis.call('PEXPIRE', KEYS[1], ARGV[2])\n" +
" return true\n" +
"end\n" +
"return false";
代碼意思也是顯然而已的
1.鎖是自己的译断,續(xù)約
2.否則,失敾虮(業(yè)務代碼完成了孙咪,主動釋放鎖了)
當然,為了保險起見巡语,這邊把申請鎖的代碼md5了翎蹈,作為分布式鎖的版本,用來判斷當前的插件代碼是否還合適
- 侵入RedisLockRegistry捌臊,獲取locks
這里沒什么好說杨蛋,直接反射,強制讀取就行了(雖然官方已經(jīng)不建議這樣用理澎,但是現(xiàn)在太多太多框架都這樣做了,現(xiàn)在還沒強制曙寡,不是么糠爬?)
完整代碼
上面的是廢話,看代碼就好了
/**
* (why) 提供【自動續(xù)約】功能
* (what)本類以【暴力】鎖進行續(xù)約
* (how)自動初始化举庶,并以插件方式運行
*
* @Todo 若 RedisLockRegistry 提供續(xù)約功能执隧,應使用官方功能
* @author Wind
*/
@Slf4j
public class LockWatchdog {
/**
* 這個是RedisLockRegistry的script, 用于確認版本是否正確
*/
private static final String OBTAIN_LOCK_SCRIPT_VERSION = "aa1fc9ae99657e86372b45452e5d6f71";
/**
* renew鎖使用
* 和 OBTAIN_LOCK_SCRIPT 最大的區(qū)別就是如果lockClientId不存在,不會創(chuàng)建一條
*/
private static final String RENEW_LOCK_SCRIPT =
"local lockClientId = redis.call('GET', KEYS[1])\n" +
"if lockClientId == ARGV[1] then\n" +
" redis.call('PEXPIRE', KEYS[1], ARGV[2])\n" +
" return true\n" +
"end\n" +
"return false";
/**
* RedisLockRegistry(static).FIELDS
*/
private static final String FIELD_OBTAIN_LOCK_SCRIPT = "OBTAIN_LOCK_SCRIPT";
/**
* redisLockRegistry(object).FIELDS
*/
private static final String FIELD_CLIENT_ID = "clientId";
private static final String FIELD_EXPIRE_AFTER = "expireAfter";
private static final String FIELD_LOCKS = "locks";
/**
* redisLock(object).FIELDS
*/
private static final String FIELD_LOCK_KEY = "lockKey";
private static final String FIELD_LOCKED_AT = "lockedAt";
private final RedisLockRegistry lockRegistry;
private final StringRedisTemplate redisTemplate;
private final RedisScript<Boolean> renewLockScript;
private final String clientId;
private final long expireAfter;
public LockWatchdog(RedisLockRegistry lockRegistry, StringRedisTemplate redisTemplate) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException {
//依賴
this.lockRegistry = lockRegistry;
this.redisTemplate = redisTemplate;
//刷新腳本
this.renewLockScript = new DefaultRedisScript<>(RENEW_LOCK_SCRIPT, Boolean.class);
//代理類的參數(shù)
this.clientId = (String) UnsafeBeanUtils.getProperty(lockRegistry, FIELD_CLIENT_ID);
Assert.hasText(this.clientId, "client id is required!");
this.expireAfter = (Long) UnsafeBeanUtils.getProperty(lockRegistry, FIELD_EXPIRE_AFTER);
Assert.notNull(this.expireAfter, "expire after is required!");
Assert.isTrue(this.expireAfter > 0, "expire after <= 0");
//check version
String script = (String) UnsafeBeanUtils.getProperty(RedisLockRegistry.class, FIELD_OBTAIN_LOCK_SCRIPT);
Assert.isTrue(CipherUtils.md5(script).equalsIgnoreCase(OBTAIN_LOCK_SCRIPT_VERSION),"verion error");
log.info("init success clientId {}, expireAfter {}", clientId, expireAfter);
}
/**
* 續(xù)約鎖
* 續(xù)約成功后會修改lockedAt字段,避免鎖被超時回收了
*
* @param redisLock
* @return
* @throws IllegalAccessException
* @throws NoSuchMethodException
* @throws InvocationTargetException
*/
private boolean renewLock(Object redisLock) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException {
// lock key 和 map key不同的户侥,需要重新獲取
String lockKey = (String) UnsafeBeanUtils.getProperty(redisLock, FIELD_LOCK_KEY);
if(log.isDebugEnabled()) {
log.debug("LockWatchdog:try to renew {}", lockKey);
}
Boolean success =
redisTemplate.execute(renewLockScript,
Collections.singletonList(lockKey), clientId,
String.valueOf(expireAfter));
boolean result = Boolean.TRUE.equals(success);
if (result) {
UnsafeBeanUtils.setProperty(redisLock, FIELD_LOCKED_AT, System.currentTimeMillis());
if(log.isDebugEnabled()) {
log.debug("LockWatchdog:{} renew success!", lockKey);
}
} else {
if(log.isDebugEnabled()) {
log.debug("LockWatchdog:{} renew fail!", lockKey);
}
}
return result;
}
/**
* 定時器(10s執(zhí)行一次續(xù)約)
* 這里直接獲取到Map里的內(nèi)容嘗試續(xù)約
* Map里的鎖是會自動刪除的(ExpirableLockRegistry)
* 而且使用分布式鎖的場景不算多镀琉,所以已經(jīng)解鎖的,也去嘗試續(xù)約也是沒問題的
*
* @throws NoSuchMethodException
* @throws InvocationTargetException
* @throws IllegalAccessException
*/
@Scheduled(cron = "*/10 * * * * ?")
private void scheduled() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
if(log.isDebugEnabled()){
log.debug("LockWatchdog:renew lock");
}
Map<String, Lock> locks = (Map<String, Lock>) UnsafeBeanUtils.getProperty(lockRegistry, FIELD_LOCKS);
if(log.isDebugEnabled()) {
log.debug("LockWatchdog:locks {}", locks.size());
}
if (MapUtils.isNotEmpty(locks)) {
Iterator<Map.Entry<String, Lock>> iter = locks.entrySet().iterator();
Map.Entry<String, Lock> entry = null;
while (iter.hasNext()) {
entry = iter.next();
renewLock(entry.getValue());
}
} else {
if(log.isDebugEnabled()) {
log.debug("LockWatchdog:not need to renew!");
}
}
if(log.isDebugEnabled()) {
log.debug("LockWatchdog:renew lock finish!");
}
}
/**
* 不安全的類操作
*/
private final static class UnsafeBeanUtils {
public static Object getProperty(final Class clazz, final String name) throws IllegalAccessException {
Field field = FieldUtils.getDeclaredField(clazz, name, true);
return field.get(clazz);
}
public static Object getProperty(final Object bean, final String name) throws IllegalAccessException {
Field field = FieldUtils.getDeclaredField(bean.getClass(), name, true);
return field.get(bean);
}
public static void setProperty(final Object bean, final String name, final Object value) throws IllegalAccessException {
Field field = FieldUtils.getDeclaredField(bean.getClass(), name, true);
field.set(bean, value);
}
}
}
代碼說明
- 使用了Scheduled 做定時任務蕊唐,并且10s續(xù)約一次
對屋摔,沒判斷當前鎖時間,直接10s一次替梨,本身就是臨時解決方案钓试,能達到目的就好了,系統(tǒng)設置了30s的鎖時間副瀑,也就是沒10s自動把超時時間重置為30s弓熏,注意,是重置糠睡,并不是延長挽鞠,所以多次調(diào)用效果是沒太大差別的,并且業(yè)務完成后主動釋放鎖是好習慣,再次信认,一個應用這種排他鎖也就10把8把串稀,多調(diào)用幾次(續(xù)約)影響忽略不計
有興趣可以優(yōu)化(按Redisson的判斷下鎖定時間,超時時間什么的) - 對于已經(jīng)解鎖的分布式鎖狮杨,還會存在map里的母截,所以代碼會出現(xiàn)續(xù)約失敗的情況
看代碼是會一段時間清理的,目前看也可以在續(xù)約之前試試trylock橄教,成功了 說明對方已經(jīng)解鎖了清寇,不用續(xù)約了,但是成功了還需要解鎖护蝶,多次調(diào)用redis华烟,還不如直接一開始就嘗試續(xù)約好了,等spring自己清理(這邊就自然不會續(xù)了) - 臨時方案持灰,臨時方案盔夜,臨時方案,如果官方支持自動續(xù)約了堤魁,就用官方的好了