1. 程式人生 > 實用技巧 >Java之反序列化

Java之反序列化

技術標籤: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。