畢業后一直做.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,就很慘了。