鎖是用來解決什麼問題的;java
在單進程中,咱們能夠用到synchronized、lock之類的同步操做去解決,可是對於分佈式架構下多進程的狀況下,如何作到跨進程的鎖。就須要藉助一些第三方手段來完成linux
分佈式鎖的解決方案redis
lock(數據庫
id int(11)apache
methodName varchar(100),緩存
memo varchar(1000)安全
modifyTime timestamp服務器
unique key mn (method) --惟一約束網絡
)多線程
獲取鎖的僞代碼
try{
exec insert into lock(methodName,memo) values(‘method’,’desc’); method
return true;
}Catch(DuplicateException e){
return false;
}
釋放鎖
delete from lock where methodName=’’;
利用zookeeper的惟一節點特性或者有序臨時節點特性得到最小節點做爲鎖. zookeeper 的實現相對簡單,經過curator客戶端,已經對鎖的操做進行了封裝,原理以下
1. 可靠性高、實現簡單
2. zookeeper由於臨時節點的特性,若是由於其餘客戶端由於異常和zookeeper鏈接中斷了,那麼節點會被刪除,意味着鎖會被自動釋放
3. zookeeper自己提供了一套很好的集羣方案,比較穩定
4. 釋放鎖操做,會有watch通知機制,也就是服務器端會主動發送消息給客戶端這個鎖已經被釋放了
redis中有一個setNx命令,這個命令只有在key不存在的狀況下爲key設置值。因此能夠利用這個特性來實現分佈式鎖的操做
釋放鎖的代碼
linux的內核會把全部外部設備都看做一個文件來操做,對一個文件的讀寫操做會調用內核提供的系統命令,返回一個 file descriptor(文件描述符)。對於一個socket的讀寫也會有響應的描述符,稱爲socketfd(socket 描述符)。而IO多路複用是指內核一旦發現進程指定的一個或者多個文件描述符IO條件準備好之後就通知該進程
IO多路複用又稱爲事件驅動,操做系統提供了一個功能,當某個socket可讀或者可寫的時候,它會給一個通知。當配合非阻塞socket使用時,只有當系統通知我哪一個描述符可讀了,我纔去執行read操做,能夠保證每次read都能讀到有效數據。操做系統的功能經過select/pool/epoll/kqueue之類的系統調用函數來使用,這些函數能夠同時監視多個描述符的讀寫就緒狀況,這樣多個描述符的I/O操做都能在一個線程內併發交替完成,這就叫I/O多路複用,這裏的複用指的是同一個線程
多路複用的優點在於用戶能夠在一個線程內同時處理多個socket的 io請求。達到同一個線程同時處理多個IO請求的目的。而在同步阻塞模型中,必須經過多線程的方式才能達到目的
Lua是一個高效的輕量級腳本語言,用標準C語言編寫並以源代碼形式開放, 其設計目的是爲了嵌入應用程序中,從而爲應用程序提供靈活的擴展和定製功能
到官網下載lua的tar.gz的源碼包
tar -zxvf lua-5.3.0.tar.gz
進入解壓的目錄:
cd lua-5.2.0
make linux (linux環境下編譯)
make install
若是報錯,說找不到readline/readline.h, 能夠經過yum命令安裝
yum -y install readline-devel ncurses-devel
安裝完之後再make linux / make install
最後,直接輸入 lua命令便可進入lua的控制檯
在Lua腳本中調用Redis命令,可使用redis.call函數調用。好比咱們調用string類型的命令
redis.call(‘set’,’hello’,’world’)
redis.call 函數的返回值就是redis命令的執行結果。前面咱們介紹過redis的5中類型的數據返回的值的類型也都不同。redis.call函數會將這5種類型的返回值轉化對應的Lua的數據類型
在不少狀況下咱們都須要腳本能夠有返回值,在腳本中可使用return 語句將值返回給redis客戶端,經過return語句來執行,若是沒有執行return,默認返回爲nil。
Redis提供了EVAL命令可使開發者像調用其餘Redis內置命令同樣調用腳本。
[EVAL] [腳本內容] [key參數的數量] [key …] [arg …]
能夠經過key和arg這兩個參數向腳本中傳遞數據,他們的值能夠在腳本中分別使用KEYS和ARGV 這兩個類型的全局變量訪問。好比咱們經過腳本實現一個set命令,經過在redis客戶端中調用,那麼執行的語句是:
lua腳本的內容爲: return redis.call(‘set’,KEYS[1],ARGV[1]) //KEYS和ARGV必須大寫
eval "return redis.call('set',KEYS[1],ARGV[1])" 1 hello world
EVAL命令是根據 key參數的數量-也就是上面例子中的1來將後面全部參數分別存入腳本中KEYS和ARGV兩個表類型的全局變量。當腳本不須要任何參數時也不能省略這個參數。若是沒有參數則爲0
eval "return redis.call(‘get’,’hello’)" 0
考慮到咱們經過eval執行lua腳本,腳本比較長的狀況下,每次調用腳本都須要把整個腳本傳給redis,比較佔用帶寬。爲了解決這個問題,redis提供了EVALSHA命令容許開發者經過腳本內容的SHA1摘要來執行腳本。該命令的用法和EVAL同樣,只不過是將腳本內容替換成腳本內容的SHA1摘要
經過如下案例來演示EVALSHA命令的效果
script load "return redis.call('get','hello')" 將腳本加入緩存並生成sha1命令
evalsha "a5a402e90df3eaeca2ff03d56d99982e05cf6574" 0
咱們在調用eval命令以前,先執行evalsha命令,若是提示腳本不存在,則再調用eval命令
實現一個針對某個手機號的訪問頻次, 如下是lua腳本,保存爲phone_limit.lua
local num=redis.call('incr',KEYS[1])
if tonumber(num)==1 then
redis.call('expire',KEYS[1],ARGV[1])
return 1
elseif tonumber(num)>tonumber(ARGV[2]) then
return 0
else
return 1
end
經過以下命令調用
./redis-cli --eval phone_limit.lua rate.limiting:13700000000 , 10 3
語法爲 ./redis-cli –eval [lua腳本] [key…]空格,空格[args…]
redis的腳本執行是原子的,即腳本執行期間Redis不會執行其餘命令。全部的命令必須等待腳本執行完之後才能執行。爲了防止某個腳本執行時間過程致使Redis沒法提供服務。Redis提供了lua-time-limit參數限制腳本的最長運行時間。默認是5秒鐘。
當腳本運行時間超過這個限制後,Redis將開始接受其餘命令但不會執行(以確保腳本的原子性),而是返回BUSY的錯誤
打開兩個客戶端窗口
在第一個窗口中執行lua腳本的死循環
eval 「while true do end」 0
在第二個窗口中運行get hello
最後第二個窗口的運行結果是Busy, 能夠經過script kill命令終止正在執行的腳本。若是當前執行的lua腳本對redis的數據進行了修改,好比(set)操做,那麼script kill命令沒辦法終止腳本的運行,由於要保證lua腳本的原子性。若是執行一部分終止了,就違背了這一個原則
在這種狀況下,只能經過 shutdown nosave命令強行終止
java代碼
RedisManager.java
import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; public class RedisManager { private static JedisPool jedisPool; static { JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); jedisPoolConfig.setMaxTotal(20); jedisPoolConfig.setMaxIdle(10); jedisPool = new JedisPool(jedisPoolConfig, "120.79.174.118", 6379); } public static Jedis getJedis() throws Exception { if (null != jedisPool) { return jedisPool.getResource(); } throw new Exception("Jedispool was not init"); } }
RedisLock.java 簡單實現分佈式鎖
import java.util.List; import java.util.UUID; import redis.clients.jedis.Jedis; import redis.clients.jedis.Transaction; public class RedisLock { public String getLock(String key, int timeout) { try { Jedis jedis = RedisManager.getJedis(); String value = UUID.randomUUID().toString(); long end = System.currentTimeMillis() + timeout; while (System.currentTimeMillis() < end) { if (jedis.setnx(key, value) == 1) { // 鎖設置成功,redis操做成功 jedis.expire(key, timeout); return value; } if (jedis.ttl(key) == -1) { // 檢測過時時間,沒有設置則設置 jedis.expire(key, timeout); } Thread.sleep(1000); } } catch (Exception e) { e.printStackTrace(); } return null; } public boolean releaseLock(String key, String value) { try { Jedis jedis = RedisManager.getJedis(); while (true) { jedis.watch(key);// watch if (value.equals(jedis.get(key))) {// 判斷得到鎖的線程和當前redis中存的鎖是同一個 Transaction transaction = jedis.multi(); transaction.del(key); List<Object> list = transaction.exec(); if (list == null) { continue; } return true; } jedis.unwatch(); break; } } catch (Exception e) { e.printStackTrace(); } return false; } public static void main(String[] args) { String key = "aaa"; RedisLock redisLock = new RedisLock(); String lockId = redisLock.getLock(key, 10000); if (null != lockId) { System.out.println("得到鎖成功"); } else { System.out.println("得到鎖失敗"); } String lockId2 = redisLock.getLock(key, 10000); if (null != lockId2) { System.out.println("得到鎖成功"); } else { System.out.println("得到鎖失敗"); } boolean ret = redisLock.releaseLock(key, lockId); if (ret) { System.out.println("釋放鎖成功"); } else { System.out.println("釋放鎖失敗"); } String lockId3 = redisLock.getLock(key, 10000); if (null != lockId3) { System.out.println("得到鎖成功"); } else { System.out.println("得到鎖失敗"); } boolean ret2 = redisLock.releaseLock(key, lockId3); if (ret2) { System.out.println("釋放鎖成功"); } else { System.out.println("釋放鎖失敗"); } } }
LuaDemo.java 執行lua腳本
import java.util.ArrayList; import java.util.List; import redis.clients.jedis.Jedis; public class LuaDemo { public static void main(String[] args) throws Exception { Jedis jedis = RedisManager.getJedis(); String lua="local num=redis.call('incr',KEYS[1])\n"+ "if tonumber(num)==1 then\n"+ " redis.call('expire',KEYS[1],ARGV[1])\n"+ " return 1\n"+ "elseif tonumber(num)>tonumber(ARGV[2]) then\n"+ " return 0\n"+ "else\n"+ " return 1\n"+ "end"; List<String> keys=new ArrayList<>(); keys.add("ip:limit:127.0.0.1"); List<String> arggs=new ArrayList<>(); arggs.add("6000"); arggs.add("10"); Object obj=jedis.eval(lua,keys,arggs); System.out.println(obj); } }
LuaDemo2.java 經過sha摘要緩存lua腳本
import java.util.ArrayList; import java.util.List; import redis.clients.jedis.Jedis; public class LuaDemo2 { public static void main(String[] args) throws Exception { Jedis jedis = RedisManager.getJedis(); String lua="local num=redis.call('incr',KEYS[1])\n"+ "if tonumber(num)==1 then\n"+ " redis.call('expire',KEYS[1],ARGV[1])\n"+ " return 1\n"+ "elseif tonumber(num)>tonumber(ARGV[2]) then\n"+ " return 0\n"+ "else\n"+ " return 1\n"+ "end"; List<String> keys = new ArrayList<>(); keys.add("ip:limit:127.0.0.1"); List<String> arggs = new ArrayList<>(); arggs.add("6000"); arggs.add("10"); // 經過sha摘要緩存lua腳本,減小網絡傳輸,提升性能。(redis重啓緩存的sha摘要會丟失) String sha = jedis.scriptLoad(lua); System.out.println(sha); Object obj = jedis.evalsha(sha, keys, arggs); System.out.println(obj); } }
maven配置
<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.9.0</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> <version>2.4.3</version> </dependency>