畢業後一直做.Net工作,我喜歡C#更優美簡潔的文法(雖然有些關鍵字或者類的命名有點隐晦)。當然Java也不能丢掉,Java的很多開源技術更能讓我拓展視野,在分布式方面也更容易上手。空餘時間正在将自己的一個個人項目用java重寫,設計為一個分布式的項目,其中有減庫存的操作。要做到全局同步,分布式鎖正好用于解決此問題,在分布式環境下,多線程共享臨界資源的場景下,分布式鎖是一種非常重要的元件。Redis的單線程,setnx指令也是得天獨厚。
我定義了一個接口,希望在将來做Redisson RedLock算法實作和ZK的分布式鎖實作。下面lock是阻塞鎖,實作應包含重試機制,trylock是非阻塞鎖。
1 public interface DistributedLocker {
2
3 boolean lock(String key) throws InterruptedException;
4
5 boolean lock(String key,int expireSecond,int waitSecond) throws InterruptedException;
6
7 boolean tryLock(String key);
8
9 boolean tryLock(String key,int expireSecond);
10
11 boolean releaseLock(String key);
12 }
1 public class RedisLocker implements DistributedLocker {
2
3 private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
4 private JedisClient jedisClient;
5
6 public void setClient(JedisClient client) {
7 this.jedisClient = client;
8 }
9
10 private final int defaultExpireSeconds = 5;
11
12 private final int defaultWaitSeconds = 100;
13
14 @Override
15 public boolean lock(String key) throws InterruptedException {
16
17 return lock(key, defaultExpireSeconds, defaultWaitSeconds);
18 }
19
20 @Override
21 public boolean lock(String key, int expireSecond, int waitSecond) throws InterruptedException {
22 int maxDelayMillis = waitSecond * 1000;
23 boolean isLockSucceed=false;
24 while (maxDelayMillis>0){
25 int delayTime = (int) (Math.random() * 20);
26 maxDelayMillis-=delayTime;
27 Thread.sleep(delayTime);
28 System.out.println("wait "+delayTime);
29 long startTime = System.currentTimeMillis();
30 isLockSucceed= tryLock(key,expireSecond);
31 long endTime = System.currentTimeMillis();
32 System.out.println("network"+(endTime-startTime));
33 maxDelayMillis=maxDelayMillis-delayTime-(int)(endTime-startTime);
34 System.out.println("剩餘"+maxDelayMillis);
35 if (isLockSucceed){
36 System.out.println("lock ok 剩餘"+maxDelayMillis);
37 break;
38 }
39 }
40
41 return isLockSucceed;
42 }
43
44 @Override
45 public boolean tryLock(String key) {
46 return tryLock(key, defaultExpireSeconds);
47 }
48
49 @Override
50 public boolean tryLock(String key, int expireSeconds) {
51
52 String lockToken = threadLocal.get();
53 if (StringUtils.isBlank(lockToken)) {
54 System.out.println("token為空" + lockToken);
55 lockToken = UUID.randomUUID().toString();
56 threadLocal.set(lockToken);
57 }
58
59 boolean isLockSucceed = jedisClient.setNX(key, lockToken);
60 if (isLockSucceed) { //如果加鎖成功
61 jedisClient.expire(key, expireSeconds);
62 } else {//如果加鎖失敗 判斷是否應該重入
63 String tokenFromRedis = jedisClient.get(key);
64 System.out.println("tokenFromRedis:" + tokenFromRedis);
65 if (lockToken.equals(tokenFromRedis)) {
66 isLockSucceed = true;//可重入
67 System.out.println("重入成功");
68 }
69 }
70 System.out.println("擷取鎖結果" + isLockSucceed + " token為" + lockToken);
71 return isLockSucceed;
72 }
73
74 @Override
75 public boolean releaseLock(String key) {
76 return jedisClient.del(key);
77 }
78 }
可靠性分析:
1. key是一定要設定過期時間的,setnx原生指令不包含expire選項,需要使用key的指令。非原子操作遇到expire指令不成功也許是個災難。原生的指令好像支援直接帶expire 在jedis中不确定是不是版本問題 需要再确認。
2. 釋放鎖,也就是del key的時候,指令可能會執行失敗,導緻其他線程長期拿不到鎖。
3. 鎖丢失,為了解決單點問題,可能引入主從加哨兵(master&&slave&&sentinel),或者叢集(redis cluster)。拿主從來說,如果master setnx指令執行成功,在資料未同步給slave的瞬間, master挂掉,從升主。這時 一個setnx的key,可能會被兩個線程set成功,也就是兩個線程都拿到了鎖。
4. 臨界時間,如果一個線程使用鎖後,準備del 鎖,這時key過期了,其他線程立即建立key 持有鎖,現在del指令到達redis并删除了剛建立的key,就很慘了。