redis筆記2

分佈式鎖的實現

鎖是用來解決什麼問題的;java

  1. 一個進程中的多個線程,多個線程併發訪問同一個資源的時候,如何解決線程安全問題。
  2. 一個分佈式架構系統中的兩個模塊同時去訪問一個文件對文件進行讀寫操做
  3. 多個應用對同一條數據作修改的時候,如何保證數據的安全性

在單進程中,咱們能夠用到synchronized、lock之類的同步操做去解決,可是對於分佈式架構下多進程的狀況下,如何作到跨進程的鎖。就須要藉助一些第三方手段來完成linux

設計一個分佈式所須要解決的問題

分佈式鎖的解決方案redis

  1. 怎麼去獲取鎖

數據庫,經過惟一約束

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=’’;

存在的須要思考的問題

  1. 鎖沒有失效時間,一旦解鎖操做失敗,就會致使鎖記錄一直在數據庫中,其餘線程沒法再得到到鎖
  2. 鎖是非阻塞的,數據的insert操做,一旦插入失敗就會直接報錯。沒有得到鎖的線程並不會進入排隊隊列,要想再次得到鎖就要再次觸發得到鎖操做
  3. 鎖是非重入的,同一個線程在沒有釋放鎖以前沒法再次得到該鎖

zookeeper實現分佈式鎖

利用zookeeper的惟一節點特性或者有序臨時節點特性得到最小節點做爲鎖. zookeeper 的實現相對簡單,經過curator客戶端,已經對鎖的操做進行了封裝,原理以下

 

 

zookeeper的優點

1.  可靠性高、實現簡單

2.  zookeeper由於臨時節點的特性,若是由於其餘客戶端由於異常和zookeeper鏈接中斷了,那麼節點會被刪除,意味着鎖會被自動釋放

3.  zookeeper自己提供了一套很好的集羣方案,比較穩定

4.  釋放鎖操做,會有watch通知機制,也就是服務器端會主動發送消息給客戶端這個鎖已經被釋放了

基於緩存的分佈式鎖實現

redis中有一個setNx命令,這個命令只有在key不存在的狀況下爲key設置值。因此能夠利用這個特性來實現分佈式鎖的操做

具體實現代碼

  1. 添加依賴包

 

  1. 編寫redis鏈接的代碼

 

釋放鎖的代碼

 

  1. 分佈式鎖的具體實現

 

  1. 怎麼釋放鎖

 

redis多路複用機制

linux的內核會把全部外部設備都看做一個文件來操做,對一個文件的讀寫操做會調用內核提供的系統命令,返回一個 file descriptor(文件描述符)。對於一個socket的讀寫也會有響應的描述符,稱爲socketfd(socket 描述符)。而IO多路複用是指內核一旦發現進程指定的一個或者多個文件描述符IO條件準備好之後就通知該進程

IO多路複用又稱爲事件驅動,操做系統提供了一個功能,當某個socket可讀或者可寫的時候,它會給一個通知。當配合非阻塞socket使用時,只有當系統通知我哪一個描述符可讀了,我纔去執行read操做,能夠保證每次read都能讀到有效數據。操做系統的功能經過select/pool/epoll/kqueue之類的系統調用函數來使用,這些函數能夠同時監視多個描述符的讀寫就緒狀況,這樣多個描述符的I/O操做都能在一個線程內併發交替完成,這就叫I/O多路複用,這裏的複用指的是同一個線程

多路複用的優點在於用戶能夠在一個線程內同時處理多個socket的 io請求。達到同一個線程同時處理多個IO請求的目的。而在同步阻塞模型中,必須經過多線程的方式才能達到目的

 

redis中使用lua腳本

lua腳本

Lua是一個高效的輕量級腳本語言,用標準C語言編寫並以源代碼形式開放, 其設計目的是爲了嵌入應用程序中,從而爲應用程序提供靈活的擴展和定製功能

使用腳本的好處

  1. 減小網絡開銷,在Lua腳本中能夠把多個命令放在同一個腳本中運行
  2. 原子操做,redis會將整個腳本做爲一個總體執行,中間不會被其餘命令插入。換句話說,編寫腳本的過程當中無需擔憂會出現競態條件
  3. 複用性,客戶端發送的腳本會永遠存儲在redis中,這意味着其餘客戶端能夠複用這一腳原本完成一樣的邏輯

Lua在linux中的安裝

到官網下載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與Lua

在Lua腳本中調用Redis命令,可使用redis.call函數調用。好比咱們調用string類型的命令

redis.call(‘set’,’hello’,’world’)

redis.call 函數的返回值就是redis命令的執行結果。前面咱們介紹過redis的5中類型的數據返回的值的類型也都不同。redis.call函數會將這5種類型的返回值轉化對應的Lua的數據類型

從Lua腳本中得到返回值

在不少狀況下咱們都須要腳本能夠有返回值,在腳本中可使用return 語句將值返回給redis客戶端,經過return語句來執行,若是沒有執行return,默認返回爲nil。

如何在redis中執行lua腳本

Redis提供了EVAL命令可使開發者像調用其餘Redis內置命令同樣調用腳本。

[EVAL]  [腳本內容] [key參數的數量]  [key …] [arg …]

能夠經過key和arg這兩個參數向腳本中傳遞數據,他們的值能夠在腳本中分別使用KEYSARGV 這兩個類型的全局變量訪問。好比咱們經過腳本實現一個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

EVALSHA命令

考慮到咱們經過eval執行lua腳本,腳本比較長的狀況下,每次調用腳本都須要把整個腳本傳給redis,比較佔用帶寬。爲了解決這個問題,redis提供了EVALSHA命令容許開發者經過腳本內容的SHA1摘要來執行腳本。該命令的用法和EVAL同樣,只不過是將腳本內容替換成腳本內容的SHA1摘要

 

  1. Redis在執行EVAL命令時會計算腳本的SHA1摘要並記錄在腳本緩存中
  2. 執行EVALSHA命令時Redis會根據提供的摘要從腳本緩存中查找對應的腳本內容,若是找到了就執行腳本,不然返回「NOSCRIPT No matching script,Please use EVAL」

 

經過如下案例來演示EVALSHA命令的效果

script load "return redis.call('get','hello')"          將腳本加入緩存並生成sha1命令

evalsha "a5a402e90df3eaeca2ff03d56d99982e05cf6574" 0

咱們在調用eval命令以前,先執行evalsha命令,若是提示腳本不存在,則再調用eval命令

lua腳本實戰

實現一個針對某個手機號的訪問頻次, 如下是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>
相關文章
相關標籤/搜索