1、使用分佈式鎖要知足的幾個條件:java
2、應用的場景例子mysql
管理後臺的部署架構(多臺tomcat服務器+redis【多臺tomcat服務器訪問一臺redis】+mysql【多臺tomcat服務器訪問一臺服務器上的mysql】)就知足使用分佈式鎖的條件。多臺服務器要訪問redis全局緩存的資源,若是不使用分佈式鎖就會出現問題。redis
從redis獲取值N,對數值N進行邊界檢查,自加1,而後N寫回redis中。 這種應用場景很常見,像秒殺,全局遞增ID、IP訪問限制等。以IP訪問限制來講,惡意攻擊者可能發起無限次訪問,併發量比較大,分佈式環境下對N的邊界檢查就不可靠,由於從redis讀的N可能已是髒數據。傳統的加鎖的作法(如java的synchronized和Lock)也沒用,由於這是分佈式環境,這個同步問題的救火隊員也一籌莫展。在這危急存亡之秋,分佈式鎖終於有用武之地了。算法
分佈式鎖能夠基於不少種方式實現,好比zookeeper、redis...。無論哪一種方式,他的基本原理是不變的:用一個狀態值表示鎖,對鎖的佔用和釋放經過狀態值來標識。spring
這裏主要講如何用redis實現分佈式鎖。sql
3、使用redis的setNX命令實現分佈式鎖數據庫
一、實現的原理apache
Redis爲單進程單線程模式,採用隊列模式將併發訪問變成串行訪問,且多客戶端對Redis的鏈接並不存在競爭關係。redis的SETNX命令能夠方便的實現分佈式鎖。spring-mvc
二、基本命令解析緩存
1)setNX(SET if Not eXists)
語法:
SETNX key value
將 key 的值設爲 value ,當且僅當 key 不存在。
若給定的 key 已經存在,則 SETNX 不作任何動做。
SETNX 是『SET if Not eXists』(若是不存在,則 SET)的簡寫
返回值:
redis> EXISTS job # job 不存在 (integer) 0 redis> SETNX job "programmer" # job 設置成功 (integer) 1 redis> SETNX job "code-farmer" # 嘗試覆蓋 job ,失敗 (integer) 0 redis> GET job # 沒有被覆蓋 "programmer"
因此咱們使用執行下面的命令
SETNX lock.foo <current Unix time + lock timeout + 1>
如返回1,則該客戶端得到鎖,把lock.foo的鍵值設置爲時間值表示該鍵已被鎖定,該客戶端最後能夠經過DEL lock.foo來釋放該鎖。
如返回0,代表該鎖已被其餘客戶端取得,這時咱們能夠先返回或進行重試等對方完成或等待鎖超時。
2)getSET
語法:
GETSET key value
將給定 key 的值設爲 value ,並返回 key 的舊值(old value)。
當 key 存在但不是字符串類型時,返回一個錯誤。
返回值:
GET key
返回值:
上面的鎖定邏輯有一個問題:若是一個持有鎖的客戶端失敗或崩潰了不能釋放鎖,該怎麼解決?
咱們能夠經過鎖的鍵對應的時間戳來判斷這種狀況是否發生了,若是當前的時間已經大於lock.foo的值,說明該鎖已失效,能夠被從新使用。
發生這種狀況時,可不能簡單的經過DEL來刪除鎖,而後再SETNX一次(講道理,刪除鎖的操做應該是鎖擁有這執行的,這裏只須要等它超時便可),當多個客戶端檢測到鎖超時後都會嘗試去釋放它,這裏就可能出現一個競態條件,讓咱們模擬一下這個場景:
C0操做超時了,但它還持有着鎖,C1和C2讀取lock.foo檢查時間戳,前後發現超時了。 C1 發送DEL lock.foo C1 發送SETNX lock.foo 而且成功了。 C2 發送DEL lock.foo C2 發送SETNX lock.foo 而且成功了。 這樣一來,C1,C2都拿到了鎖!問題大了!
幸虧這種問題是能夠避免的,讓咱們來看看C3這個客戶端是怎樣作的:
C3發送SETNX lock.foo 想要得到鎖,因爲C0還持有鎖,因此Redis返回給C3一個0 C3發送GET lock.foo 以檢查鎖是否超時了,若是沒超時,則等待或重試。 反之,若是已超時,C3經過下面的操做來嘗試得到鎖: GETSET lock.foo <current Unix time + lock timeout + 1> 經過GETSET,C3拿到的時間戳若是仍然是超時的,那就說明,C3如願以償拿到鎖了。 若是在C3以前,有個叫C4的客戶端比C3快一步執行了上面的操做,那麼C3拿到的時間戳是個未超時的值,這時,C3沒有如期得到鎖,須要再次等待或重試。留意一下,儘管C3沒拿到鎖,但它改寫了C4設置的鎖的超時值,不過這一點很是微小的偏差帶來的影響能夠忽略不計。
注意:爲了讓分佈式鎖的算法更穩鍵些,持有鎖的客戶端在解鎖以前應該再檢查一次本身的鎖是否已經超時,再去作DEL操做,由於可能客戶端由於某個耗時的操做而掛起,操做完的時候鎖由於超時已經被別人得到,這時就沒必要解鎖了。
5、代碼實現
expireMsecs 鎖持有超時,防止線程在入鎖之後,無限的執行下去,讓鎖沒法釋放
timeoutMsecs 鎖等待超時,防止線程飢餓,永遠沒有入鎖執行代碼的機會
注意:項目裏面須要先搭建好redis的相關配置
package test.miaosha; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.dao.DataAccessException; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.StringRedisSerializer; public class RedisLock { private static Logger logger = LoggerFactory.getLogger(RedisLock.class); private RedisTemplate<String,Object> redisTemplate; private static final int DEFAULT_ACQUIRY_RESOLUTION_MILLIS = 100; /** * Lock key path. */ private String lockKey; /** * 鎖超時時間,防止線程在入鎖之後,無限的執行等待 */ private int expireMsecs = 60 * 1000; /** * 鎖等待時間,防止線程飢餓 */ private int timeoutMsecs = 10 * 1000; private volatile boolean locked = false; /** * Detailed constructor with default acquire timeout 10000 msecs and lock * expiration of 60000 msecs. * * @param lockKey * lock key (ex. account:1, ...) */ public RedisLock(RedisTemplate<String,Object> redisTemplate, String lockKey) { this.redisTemplate = redisTemplate; this.lockKey = lockKey + "_lock"; } /** * Detailed constructor with default lock expiration of 60000 msecs. * */ public RedisLock(RedisTemplate<String,Object> redisTemplate, String lockKey, int timeoutMsecs) { this(redisTemplate, lockKey); this.timeoutMsecs = timeoutMsecs; } /** * Detailed constructor. * */ public RedisLock(RedisTemplate<String,Object> redisTemplate, String lockKey, int timeoutMsecs, int expireMsecs) { this(redisTemplate, lockKey, timeoutMsecs); this.expireMsecs = expireMsecs; } /** * @return lock key */ public String getLockKey() { return lockKey; } public String get(final String key) { Object obj = null; try { obj = redisTemplate.execute(new RedisCallback<Object>() { @Override public Object doInRedis(RedisConnection connection) throws DataAccessException { StringRedisSerializer serializer = new StringRedisSerializer(); byte[] data = connection.get(serializer.serialize(key)); connection.close(); if (data == null) { return null; } return serializer.deserialize(data); } }); } catch (Exception e) { logger.error("get redis error, key : {}", key); } return obj != null ? obj.toString() : null; } public String set(final String key,final String value) { Object obj = null; try { obj = redisTemplate.execute(new RedisCallback<Object>() { @Override public Object doInRedis(RedisConnection connection) throws DataAccessException { StringRedisSerializer serializer = new StringRedisSerializer(); connection.set(serializer.serialize(key), serializer.serialize(value)); return serializer; } }); } catch (Exception e) { logger.error("get redis error, key : {}", key); } return obj != null ? obj.toString() : null; } public boolean setNX(final String key, final String value) { Object obj = null; try { obj = redisTemplate.execute(new RedisCallback<Object>() { @Override public Object doInRedis(RedisConnection connection) throws DataAccessException { StringRedisSerializer serializer = new StringRedisSerializer(); Boolean success = connection.setNX(serializer.serialize(key), serializer.serialize(value)); connection.close(); return success; } }); } catch (Exception e) { logger.error("setNX redis error, key : {}", key); } return obj != null ? (Boolean) obj : false; } private String getSet(final String key, final String value) { Object obj = null; try { obj = redisTemplate.execute(new RedisCallback<Object>() { @Override public Object doInRedis(RedisConnection connection) throws DataAccessException { StringRedisSerializer serializer = new StringRedisSerializer(); byte[] ret = connection.getSet(serializer.serialize(key), serializer.serialize(value)); connection.close(); return serializer.deserialize(ret); } }); } catch (Exception e) { logger.error("setNX redis error, key : {}", key); } return obj != null ? (String) obj : null; } /** * 得到 lock. 實現思路: 主要是使用了redis 的setnx命令,緩存了鎖. reids緩存的key是鎖的key,全部的共享, * value是鎖的到期時間(注意:這裏把過時時間放在value了,沒有時間上設置其超時時間) 執行過程: * 1.經過setnx嘗試設置某個key的值,成功(當前沒有這個鎖)則返回,成功得到鎖 * 2.鎖已經存在則獲取鎖的到期時間,和當前時間比較,超時的話,則設置新的值 * * @return true if lock is acquired, false acquire timeouted * @throws InterruptedException * in case of thread interruption */ public synchronized boolean lock() throws InterruptedException { int timeout = timeoutMsecs; while (timeout >= 0) { long expires = System.currentTimeMillis() + expireMsecs + 1; String expiresStr = String.valueOf(expires); // 鎖到期時間 if (this.setNX(lockKey, expiresStr)) { // lock acquired locked = true; return true; } String currentValueStr = this.get(lockKey); // redis裏的時間 if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) { // 判斷是否爲空,不爲空的狀況下,若是被其餘線程設置了值,則第二個條件判斷是過不去的 // lock is expired String oldValueStr = this.getSet(lockKey, expiresStr); // 獲取上一個鎖到期時間,並設置如今的鎖到期時間, // 只有一個線程才能獲取上一個線上的設置時間,由於jedis.getSet是同步的 if (oldValueStr != null && oldValueStr.equals(currentValueStr)) { // 防止誤刪(覆蓋,由於key是相同的)了他人的鎖——這裏達不到效果,這裏值會被覆蓋,可是由於什麼相差了不多的時間,因此能夠接受 // [分佈式的狀況下]:如過這個時候,多個線程剛好都到了這裏,可是隻有一個線程的設置值和當前值相同,他纔有權利獲取鎖 // lock acquired locked = true; return true; } } timeout -= DEFAULT_ACQUIRY_RESOLUTION_MILLIS; /* * 延遲100 毫秒, 這裏使用隨機時間可能會好一點,能夠防止飢餓進程的出現,即,當同時到達多個進程, * 只會有一個進程得到鎖,其餘的都用一樣的頻率進行嘗試,後面有來了一些進行,也以一樣的頻率申請鎖,這將可能致使前面來的鎖得不到知足. * 使用隨機的等待時間能夠必定程度上保證公平性 */ Thread.sleep(DEFAULT_ACQUIRY_RESOLUTION_MILLIS); } return false; } /** * Acqurired lock release. */ public synchronized void unlock() { if (locked) { redisTemplate.delete(lockKey); locked = false; } } }
MsService.java
package test.miaosha; import org.springframework.data.redis.core.RedisTemplate; public class MsService { /*** * 搶購代碼 * @param redisTemplate * @param key pronum 首先用客戶端設置數量 * @return */ public boolean seckill(RedisTemplate<String,Object> redisTemplate, String key) { RedisLock lock = new RedisLock(redisTemplate, key, 10000, 20000); try { if (lock.lock()) { // 須要加鎖的代碼 String pronum=lock.get("pronum"); //修改庫存 if(Integer.parseInt(pronum)-1>=0) { lock.set("pronum",String.valueOf(Integer.parseInt(pronum)-1)); System.out.println("庫存數量:"+pronum+" 成功!!!"+Thread.currentThread().getName()); }else { System.out.println("手慢拍大腿"); } return true; } } catch (InterruptedException e) { e.printStackTrace(); } finally { // 爲了讓分佈式鎖的算法更穩鍵些,持有鎖的客戶端在解鎖以前應該再檢查一次本身的鎖是否已經超時,再去作DEL操做,由於可能客戶端由於某個耗時的操做而掛起, // 操做完的時候鎖由於超時已經被別人得到,這時就沒必要解鎖了。 ————這裏沒有作 lock.unlock(); } return false; } }
ThreadB.java
package test.miaosha; import org.springframework.data.redis.core.RedisTemplate; public class ThreadB extends Thread { private MsService service; private RedisTemplate<String,Object> redisTemplate; private String key; public ThreadB(MsService service,RedisTemplate<String,Object> redisTemplate,String key) { this.service = service; this.redisTemplate=redisTemplate; this.key=key; } @Override public void run() { service.seckill(redisTemplate, key); } }
測試類 直接開100個線程 模擬100我的搶購
package test.miaosha; import org.apache.xbean.spring.context.ClassPathXmlApplicationContext; import org.springframework.data.redis.core.RedisTemplate; public class MsTest { static ClassPathXmlApplicationContext appCtx = new ClassPathXmlApplicationContext("test/miaosha/spring-redis.xml"); public static void main(String[] args) { RedisTemplate<String, Object> redisTemplate = (RedisTemplate<String, Object>)appCtx.getBean("redisTemplate",RedisTemplate.class); //RedisUtil redisUtil=(RedisUtil) appCtx.getBean("redisUtil"); System.out.println("開始"); MsService service = new MsService(); for (int i = 0; i < 100; i++) { ThreadB threadA = new ThreadB(service, redisTemplate, "MSKEY"); threadA.start(); } } }
spring-redis.xml
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:util="http://www.springframework.org/schema/util" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.0.xsd"> <!--[redis-JedisPoolConfig配置](http://blog.csdn.net/liang_love_java/article/details/50510753)--> <!-- jedis-2.7.2.jar 依賴jar包 commons-pool2-2.3.jar jedis基於 commons-pool2-2.3.jar 本身實現了一個資源池。 配置參數 詳見 http://blog.csdn.net/liang_love_java/article/details/50510753 --> <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig"> <property name="maxIdle" value="8" /> <property name="maxTotal" value="200" /> <property name="blockWhenExhausted" value="true" /> <property name="maxWaitMillis" value="30000" /> <property name="testOnBorrow" value="true" /> </bean> <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"> <property name="hostName" value="127.0.0.1" /> <property name="port" value="6379"/> <property name="poolConfig" ref="jedisPoolConfig" /> <property name="usePool" value="true"/> </bean> <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate"> <property name="connectionFactory" ref="jedisConnectionFactory" /> <property name="keySerializer"> <bean class="org.springframework.data.redis.serializer.StringRedisSerializer" /> </property> <property name="valueSerializer"> <bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer" /> </property> <property name="hashKeySerializer"> <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/> </property> <property name="hashValueSerializer"> <bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"/> </property> </bean> <!--自定義redis工具類,在須要緩存的地方注入此類 --> </beans>