Java之反序列化
阿新 • • 發佈:2020-12-02
技術標籤:linux/docker/redis/nginx等
一、分散式鎖的核心概念
分散式鎖的實現有很多,比如基於資料庫、memcached、Redis、系統檔案、zookeeper等。 核心思想都差不多,無非是做某些操作的時候,給某些資源設定一個標記, 當前執行緒還沒有執行完操作的時候,其他執行緒過來執行,就會讀取到這個標記(表示已經被佔用了), 就會進行等待,等待佔用的資源被釋放(標記清除),然後新的執行緒再執行這個操作。 這樣就等保障資料的隔離性,一段時間內只能有一個執行緒做某些操作。 為了確保分散式鎖可用,我們至少要確保鎖的實現同時滿足以下四個條件: 1、互斥性。在任意時刻,只有一個客戶端能持有鎖。 2、不會發生死鎖。即使有一個客戶端在持有鎖的期間崩潰而沒有主動解鎖,也能保證後續其他客戶端能加鎖。 3、具有容錯性。只要大部分的Redis節點正常執行,客戶端就可以加鎖和解鎖。 4、解鈴還須繫鈴人。加鎖和解鎖必須是同一個客戶端,客戶端自己不能把別人加的鎖給解了。
二、redis如何實現分散式鎖
1、加鎖
加鎖實際上就是在redis中,給Key鍵設定一個值,為看保證當前執行緒才能解鎖,所以value為當前執行緒生成的一個唯一id,為避免死鎖,就給定一個過期時間。
SET lock_key random_value NX PX 5000
random_value 是客戶端生成的唯一的字串。
NX:和setNx作用一樣,當key不存在時,才加入這個kv,並返回true。不存在就不插入並返回false。
PX :設定過期時間,時間是5000毫秒。
2、解鎖
解鎖的過程就是將Key鍵刪除。但也不能亂刪,不能說客戶端1的請求將客戶端2的鎖給刪除掉。 這時候random_value的作用就體現出來。 為了保證解鎖操作的原子性,我們用LUA指令碼完成這一操作。 先判斷當前鎖的字串(key對應的value)是否與傳入的值(ARGV[1])相等,是的話就刪除Key,解鎖成功。
三、實現
/** * @author koma <[email protected]> * @date 2018-09-19 11:24 */ @Slf4j @Service public class CacheService { private static final Long RELEASE_SUCCESS = 1L; private static final String LOCK_SUCCESS = "OK"; private static final String SET_IF_NOT_EXIST = "NX"; private static final String SET_WITH_EXPIRE_TIME = "EX"; private static final String RELEASE_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; @Autowired private StringRedisTemplate redisTemplate; /** * 該加鎖方法僅針對單例項 Redis 可實現分散式加鎖 * 對於 Redis 叢集則無法使用 * * 支援重複,執行緒安全 * * @param lockKey 加鎖鍵 * @param clientId 加鎖客戶端唯一標識(採用UUID) * @param seconds 鎖過期時間 * @return */ public Boolean tryLock(String lockKey, String clientId, long seconds) { redisTemplate.opsForValue().set(); return redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> { Jedis jedis = (Jedis) redisConnection.getNativeConnection(); String result = jedis.set(lockKey, clientId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, seconds); if (LOCK_SUCCESS.equals(result)) { return Boolean.TRUE; } return Boolean.FALSE; }); } /** * 與 tryLock 相對應,用作釋放鎖 * * @param lockKey * @param clientId * @return */ public Boolean releaseLock(String lockKey, String clientId) { return redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> { Jedis jedis = (Jedis) redisConnection.getNativeConnection(); Object result = jedis.eval(RELEASE_LOCK_SCRIPT, Collections.singletonList(lockKey),Collections.singletonList(clientId)); if (RELEASE_SUCCESS.equals(result)) { return Boolean.TRUE; } return Boolean.FALSE; }); } }
核心就是:
加鎖:
不能用這種方式:
if (redisTemplate.opsForValue().setIfAbsent(lockKey, clientId)) {
//這裡存在宕機風險,導致設定有效期失敗
redisTemplate.expire(lockKey, seconds, TimeUnit.SECONDS);
}
因為不是原子操作,redisTemplated是set方法底層呼叫的是redis 的 setNx 命令,
但是redis 的 setNx 命令不能設定key 的有效期,所以還需要另外設定key 的有效期。這樣就可能會出現設定key後宕機了,key沒有效期。
因此需要通過 execute 方法呼叫 RedisCallback 去拿到底層的 Jedis 物件,
來直接呼叫 set 命令->SET lock_key random_value NX(和setnx一樣) PX(過期時間按秒算) 5000。
解鎖:
不能用這種方式:
if (clientId.equals(redisTemplate.opsForValue().get(lockKey))) {
redisTemplate.delete(lockKey);
}
因為這不是原子操作,可能會判斷的時候傳送宕機,就導致後面的沒有刪除。
因此要通過Lua 指令碼來達到釋放鎖的原子操作
上述程式碼實現,僅對 redis 單例項架構有效,當面對 redis 叢集時就無效了。
但是一般情況下,我們的 redis 架構多數會做成“主備”模式,
然後再通過 redis 哨兵實現主從切換,
這種模式下我們的應用伺服器直接面向主機,也可看成是單例項,
因此上述程式碼實現也有效。
但是當在主機宕機,從機被升級為主機的一瞬間的時候,
如果恰好在這一刻,由於 redis 主從複製的非同步性,
導致從機中資料沒有即時同步,那麼上述程式碼依然會無效,
導致同一資源有可能會產生兩把鎖,違背了分散式鎖的原則。
如果存在主備且可以忍受小概率的鎖出錯,
那麼就可以直接使用上述程式碼,
當然最嚴謹的方式還是使用官方的 Redlock 演算法實現。
其中 Java 包推薦使用 redisson。