關於分佈式鎖的概念網上太多了,這裏就不羅嗦了。對於開發者來講,最關心的應該是什麼狀況下使用分佈式鎖。java
使用分佈式鎖,通常要知足如下幾個條件:mysql
· 分佈式系統(關鍵是分佈式)redis
· 共享資源(各系統訪問同一資源,資源的載體多是傳統關係型數據庫或者NoSQL)sql
· 同步訪問(沒有同步訪問,誰管你資源競爭不競爭)數據庫
· 光說不練假把式,上點乾貨。管理後臺的部署架構(多臺tomcat服務器+redis+mysql)就知足使用分佈式鎖的條件。多臺服務器要訪問redis全局緩存的資源,若是不使用分佈式鎖就會出現問題。 看以下僞代碼:緩存
long N=0L; //N從redis獲取值 if(N<5){ N++; //N寫回redis }
· 從redis獲取值N,對數值N進行邊界檢查,自加1,而後N寫回redis中。 這種應用場景很常見,像秒殺,全局遞增ID、IP訪問限制等。以IP訪問限制來講,惡意攻擊者可能發起無限次訪問,併發量比較大,分佈式環境下對N的邊界檢查就不可靠,由於從redis讀的N可能已是髒數據。傳統的加鎖的作法(如java的synchronized和Lock)也沒用,由於這是分佈式環境,這個同步問題的救火隊員也一籌莫展。在這危急存亡之秋,分佈式鎖終於有用武之地了。tomcat
所謂知己知彼,百戰百勝。要想用好他,首先你要了解他。分佈式鎖能夠基於不少種方式實現,好比zookeeper、redis...。無論哪一種方式,他的基本原理是不變的:用一個狀態值表示鎖,對鎖的佔用和釋放經過狀態值來標識。服務器
這裏主要講如何用redis實現分佈式鎖。架構
Redis爲單進程單線程模式,採用隊列模式將併發訪問變成串行訪問,且多客戶端對Redis的鏈接並不存在競爭關係。redis的SETNX命令能夠方便的實現分佈式鎖。併發
SETNX命令(SET if Not eXists) 語法: SETNX key value 功能: 當且僅當 key 不存在,將 key 的值設爲 value ,並返回1;若給定的 key 已經存在,則 SETNX 不作任何動做,並返回0。 有了以上Redis知識了,寫個簡單的分佈式鎖就不是什麼難事。
直接貼java代碼!
public class RedisLock implements Lock { @Autowired protected StringRedisTemplate redisTemplate; private static final Logger logger = Logger.getLogger(RedisLock.class); // lock flag stored in redis private static final String LOCKED = "TRUE"; // timeout(ms) private static final long TIME_OUT = 30000; // lock expire time(s) public static final int EXPIRE = 60; // private Jedis jedis; private String key; // state flag private volatile boolean locked = false; private static ConcurrentMap<String, RedisLock> map = Maps.newConcurrentMap(); public RedisLock(String key) { this.key = "_LOCK_" + key; redisTemplate = (StringRedisTemplate) ApplicationContextHolder.getBean("redisTemplate"); } public static RedisLock getInstance(String key) { return map.getOrDefault(key, new RedisLock(key)); } public void lock(long timeout) { long nano = System.nanoTime(); timeout *= 1000000; final Random r = new Random(); try { while ((System.nanoTime() - nano) < timeout) { if (redisTemplate.getConnectionFactory().getConnection().setNX(key.getBytes(), LOCKED.getBytes())) { redisTemplate.expire(key, EXPIRE, TimeUnit.SECONDS); locked = true; logger.debug("add RedisLock[" + key + "]."); break; } Thread.sleep(3, r.nextInt(500)); } } catch (Exception e) { } } @Override public void unlock() { if (locked) { logger.debug("release RedisLock[" + key + "]."); redisTemplate.delete(key); } } @Override public void lock() { lock(TIME_OUT); } @Override public void lockInterruptibly() throws InterruptedException { } @Override public Condition newCondition() { return null; } @Override public boolean tryLock() { return false; } @Override public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { return false; } }
飯已OK,快來MIXI啦。
~~~等等,還得讓評審來評價下這飯作得好很差吃。
寫一段測試代碼來對比測試下。
public void concurrentTest() { final Printer outer = new Printer(); new Thread(new Runnable() { @Override public void run() { outer.output("I am a boy."); } }).start(); new Thread(new Runnable() { @Override public void run() { outer.output("You are a girl."); } }).start(); } ? 不加鎖的Printer: ? class Printer { public void output(String name) { for (int i = 0; i < name.length(); i++) { System.out.print(name.charAt(i)); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } } } ? ?使用java內置的鎖ReentrantLock: Lock lock=new ReentrantLock();class Printer { public void output(String name) { lock.lock(); for (int i = 0; i < name.length(); i++) { System.out.print(name.charAt(i)); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } lock.unlock(); } } ? ?使用分佈式鎖RedisLock: Lock lock=new ReentrantLock();class Printer { public void output(String name) { Lock lock = new RedisLock("lock1"); lock.lock(); for (int i = 0; i < name.length(); i++) { System.out.print(name.charAt(i)); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } lock.unlock(); } }
項目 |
不加鎖 |
java內置鎖ReentrantLock |
加分佈式鎖RedisLock |
測試結果 |
IY oau m aar eb oa yg.irl. |
I am a boy.You are a girl. |
You are a girl.I am a boy. |
話說RedisLock可以正常使用了,也達到了預期效果。是否是這個分佈式鎖就萬無一失呢?
這是一個悲觀鎖,RedisLock會不斷嘗試去獲取鎖,直到超時。也就是說,若是長時間獲取不到,就會獲取鎖失敗,至關於沒加鎖!具體的超時時間設置爲多長,有待後期驗證,再作優化。