目錄
Jedis是Redis的Java客戶端實現,支持Redis的全部特性,如:事務、管道、發佈/訂閱、集羣等,還支持連接池等特性。
1 基本使用
首先需要引入依賴,目前最新的3.1.0-m1版本已經提供了對Redis 5 streams的支持:
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.1.0-m1</version>
</dependency>
Jedis支持Redis的全部操作,下面是一個使用set、get命令的單連接示例:
public static void main(String[] args) {
Jedis jedis=new Jedis("localhost",6379);
System.out.println(jedis.set("hello","world"));
System.out.println(jedis.get("hello"));
jedis.close();
}
運行後輸出“OK”和“world”。 Jedis的構造方法還支持配置連接超時時間和讀寫超時時間。set方法和get方法支持byte[]類型的參數,意味着可以將對象序列化後傳入Redis。
2 高級特性
由於還沒有介紹哨兵和集羣機制,所以這裏僅介紹事務、管道、發佈/訂閱機制等已經介紹了的特性,腳本機制比較簡單就不做介紹了。
2.1 連接池JedisPool
連接池相比於單連接,具有開銷少、易控制的優點,Jedis也提供了連接池實現:JedisPool。下面將上一節的例子改編爲連接池實現:
public static void main(String[] args) {
JedisPool jedisPool=new JedisPool("localhost",6379);
try(Jedis jedis=jedisPool.getResource()) {
System.out.println(jedis.set("hello", "world"));
System.out.println(jedis.get("hello"));
}
jedisPool.close();
}
輸出不變。這裏使用了 try-with-resource ,會在try塊執行完畢後自動關閉Jedis連接,其close方法如下:
public void close() {
if (this.dataSource != null) {
JedisPoolAbstract pool = this.dataSource;
this.dataSource = null;
if (this.client.isBroken()) {
pool.returnBrokenResource(this);
} else {
pool.returnResource(this);
}
} else {
super.close();
}
}
可見如果是通過連接池方式獲取連接實例,其close方法會歸還連接。
JedisPool的構造方法可以傳入GenericObjectPoolConfig對象,對連接池進行配置,比較常用的屬性有:
參數 | 含義 | 默認值 |
---|---|---|
maxTotal | 最大連接數 | 8 |
maxIdle | 最大空閒連接數 | 8 |
minIdle | 最小空閒連接數 | 0 |
maxWaitMillis | 連接池資源耗盡後,請求最大等待時間 | -1,代表一直等待 |
jmxEnabled | 是否啓用jmx監控 | true |
jmxNameBase | 在jmx中的名稱 | null |
jmxNamePrefix | 在jmx中的名稱前綴 | pool |
minEvictableIdleTimeMillis | 連接的最小空閒時間,達到此值後會被移除 | 1800000(30分鐘) |
numTestsPerEvictionRun | 空閒連接檢測時的取樣數 | 3 |
testOnBorrow | 從連接池取出連接時是否檢測有效性(發送ping命令檢測),失效連接會被移除,下同 | false |
testOnCreate | 連接池創建連接時是否檢測有效性, | false |
testOnReturn | 歸還連接時是否檢測有效性 | false |
testWhileIdle | 是否在連接空閒時檢測有效性 | false |
timeBetweenEvictionRunsMillis | 空閒連接的檢測週期 | -1,即不檢測 |
blockWhenExhausted | 連接池資源耗盡時,請求是否等待,該值爲true時,maxWaitMillis纔有意義 | true |
evictionPolicy | 連接移除策略 | DefaultEvictionPolicy |
fairness | 連接池內部存放空閒連接的的阻塞隊列是否是公平的 | false |
2.2 管道
Redis原生的管道一大缺陷就是用戶必須將命令編寫爲RESP格式,非常冗長,還容易出錯,Jedis的Pipeline實現提供了類似Jedis類的操作,非常友好:
public static void main(String[] args) {
JedisPool jedisPool=new JedisPool("localhost",6379);
try(Jedis jedis=jedisPool.getResource()) {
Pipeline pipeline=jedis.pipelined();
pipeline.set("hello","world");
Response<String> response=pipeline.get("hello");
pipeline.sync();
System.out.println(response.get());
pipeline.close();
}
jedisPool.close();
}
可以看到,Jedis的Pipeline使用和Jedis類使用具有良好的統一性,完全不需要了解RESP消息格式。Jedis實現管道的原理是,每次執行操作時,就講命令寫入RedisOutputStream,當調用sync方法時,再調用flush將數據一同發送出去,之後讀取RedisInputStream,將數據寫入Response隊列即可。
Response類似於Future,僅表示未來的數據,如果這裏將resposne.get()移動到pipeline.sync()前面,則會報出如下異常:
Exception in thread "main" redis.clients.jedis.exceptions.JedisDataException: Please close pipeline or multi block before calling this method.
at redis.clients.jedis.Response.get(Response.java:33)
at JedisTest.main(JedisTest.java:16)
2.3 事務
Jedis的事務機制和管道機制有類似之處:
public static void main(String[] args) {
JedisPool jedisPool=new JedisPool("localhost",6379);
try(Jedis jedis=jedisPool.getResource()) {
Transaction transaction=jedis.multi();
transaction.set("hello","world");
Response<String> response=transaction.get("hello");
transaction.exec();
System.out.println(response.get());
}
jedisPool.close();
}
都使用到了Response。實際上,如果看源碼的話就能發現,Jedis的Transaction類正是繼承了PipelineBase,和管道機制師出同門。
同樣地,Jedis也支持watch:
public static void main(String[] args) {
JedisPool jedisPool=new JedisPool("localhost",6379);
try(Jedis jedis=jedisPool.getResource()) {
jedis.watch("hello");
Transaction transaction=jedis.multi();
Thread.sleep(10000L);
transaction.set("hello","world");
Response<String> response=transaction.get("hello");
transaction.exec();
System.out.println(response.get());
} catch (InterruptedException e) {
e.printStackTrace();
}
jedisPool.close();
}
在線程睡眠的10秒鐘內,修改hello對應的值的話,會有如下輸出,表明事務沒有成功提交:
Exception in thread "main" redis.clients.jedis.exceptions.JedisDataException: Please close pipeline or multi block before calling this method.
at redis.clients.jedis.Response.get(Response.java:33)
at JedisTest.main(JedisTest.java:15)
2.4 發佈/訂閱
發佈比較簡單,只要調用publish方法即可,重點是訂閱機制。無論是subscribe,還是psubscribe,其參數列表中都有一個JedisPubSub對象:
void subscribe(JedisPubSub jedisPubSub, String... channels);
可以將它理解爲一個事件監聽器, 它是一個抽象類,沒有默認實現,需要我們編寫事件處理邏輯,下面是其可以重寫的方法列表:
我們以onMessage方法爲例:
public static void main(String[] args) {
JedisPool jedisPool=new JedisPool("localhost",6379);
try(Jedis jedis=jedisPool.getResource()) {
jedis.subscribe(new TestListener(),"test");
}
jedisPool.close();
}
static class TestListener extends JedisPubSub{
@Override
public void onMessage(String channel, String message) {
System.out.println("Received Message:"+message+" from "+channel);
}
}
然後在命令行客戶端輸入 publish test helloworld,從而得到如下響應:
Received Message:helloworld from test
需要注意的是,subscribe會阻塞調用線程,一直等待消息,最好是在子線程裏進行訂閱,這樣可以在主線程調用unsubscribe來退訂。
2.5 streams支持
Jedis 3.1.0最大的更新就是支持了Redis 5 的streams數據類型,以下是一個例子:
public static void main(String[] args) {
JedisPool jedisPool=new JedisPool("localhost",6379);
try(Jedis jedis=jedisPool.getResource()) {
Map<String,String> hash=new HashMap<>();
hash.put("name","zhangsan");
hash.put("age","10");
hash.put("sexual","male");
jedis.xadd("myteststream",StreamEntryID.NEW_ENTRY,hash);
System.out.println(jedis.xlen("myteststream"));
}
jedisPool.close();
}
hash就是field-string對,最終輸出的結果是1,代表成功向myteststream流中寫入了1個條目。 而StreamEntryID.NEW_ENTRY就是“*”,除此之外還有代表最後一個Entry的“$”(LAST_ENTRY)和未接收Entry的“>”(UNRECEIVED_ENTRY):
public static final StreamEntryID NEW_ENTRY = new StreamEntryID() {
private static final long serialVersionUID = 1L;
public String toString() {
return "*";
}
};
public static final StreamEntryID LAST_ENTRY = new StreamEntryID() {
private static final long serialVersionUID = 1L;
public String toString() {
return "$";
}
};
public static final StreamEntryID UNRECEIVED_ENTRY = new StreamEntryID() {
private static final long serialVersionUID = 1L;
public String toString() {
return ">";
}
};
3 利用Jedis實現一個簡單的分佈式鎖
基於Redis的分佈式鎖框架有很多,官網列出的有:
- Redlock-rb(Ruby實現)。
- Redlock-py(Python實現)。
- Aioredlock(Asyncio Python實現)。
- Redlock-php(PHP實現)。
- PHPRedisMutex(進一步的PHP實現)
- cheprasov / php-redis-lock(用於鎖的PHP庫)
- Redsync.go(Go實現)。
- Redisson(Java實現)。
- Redis :: DistLock(Perl實現)。
- Redlock-cpp(C ++實現)。
- Redlock-cs(C#/ .NET實現)。
- RedLock.net(C#/ .NET實現)
- ScarletLock(帶可配置數據存儲區的C#.NET實現)
- node-redlock(NodeJS實現)。包括對鎖定擴展的支持。
這裏使用Jedis實現一個互斥鎖。該鎖有兩個方法:lock和unlock。
3.1 構造方法
由於需要藉助Jedis實例來加解鎖,因此該類需要有一個Jedis對象,同時需要有相應的構造方法:
private Jedis jedis;
public JedisLock(Jedis jedis) {
this.jedis = jedis;
}
同時還需要考慮無參的情況,於是我們再添加一個JedisPool成員變量:
private JedisPool pool;
public JedisLock() {
pool=new JedisPool(PropertiesUtil.get("lock.host"),Integer.parseInt(PropertiesUtil.get("lock.port")));
}
PropertiesUtil是自己實現的類,用來讀取classpath下的lock.properties文件中的配置。
3.2 lock方法
互斥鎖獲取時,要求該鎖必須空閒,獲取後,該鎖必須處於佔用狀態。這一性質使用setnx就可以實現。
另一個需要考慮的問題是,等待獲取鎖的超時時間,和鎖本身的存活時間,前者可以通過傳入參數解決,後者可以通過expire/pexpire命令解決
這裏結合以上兩個問題,使用set nx px來獲取鎖。
現在開始實現,首先判斷jedis是否爲null,是的話需要從連接池取出一個連接,flag的作用是在收尾時判斷是否需要重新將連接設爲null:
boolean flag=false;
if(jedis==null) {
jedis = pool.getResource();
flag = true;
}
接下來,根據wait和expire參數是否大於0,選擇合適的分支進行獲取鎖,獲取成功則返回true,這裏wait單位是秒,expire單位是毫秒:
if(expire>0){
if(wait>0){
while(wait>0){
if(jedis.get(key)==null){
String result=jedis.set(key,"1",new SetParams().px(expire).nx());
if(result!=null&&result.equals("OK")){
jedis.set(id,"1");
close(flag,jedis);
return true;
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
wait--;
}
}else{
if(jedis.get(key)==null){
String result=jedis.set(key,"1",new SetParams().px(expire).nx());
if(result!=null&&result.equals("OK")){
jedis.set(id,"1");
close(flag,jedis);
return true;
}
}
}
}else{
if(wait>0){
while(wait>0){
if(jedis.get(key)==null) {
String result = jedis.set(key, "1", new SetParams().nx());
if (result != null && result.equals("OK")) {
jedis.set(id, "1");
close(flag, jedis);
return true;
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
wait--;
}
}else{
if(jedis.get(key)==null){
String result=jedis.set(key,"1",new SetParams().nx());
if(result!=null&&result.equals("OK")){
jedis.set(id,"1");
close(flag,jedis);
return true;
}
}
}
}
key是鎖的名稱,全局一致,id則是全局唯一ID。如果未能成功獲取鎖,則返回false:
close(flag,jedis);
return false;
close的邏輯就是如果flag爲true,則關閉jedis並設置jedis爲null:
private void close(boolean flag, Jedis jedis) {
if(flag){
jedis.close();
jedis=null;
}
}
3.3 unlock方法
unclock的思路比較簡單,只要驗證一下自己的id是否存在於Redis即可,是則刪除key,否則說明自己不是鎖的所有者,沒有資格解鎖:
public void unlock(String key) throws IllegalAccessException {
boolean flag=false;
if(jedis==null) {
jedis = pool.getResource();
flag = true;
}
if(jedis.get(id)==null){
throw new IllegalAccessException("Permission Denied.");
}else{
jedis.del(key);
jedis.del(id);
}
close(flag,jedis);
}
3.4 測試
我們編寫一個長度爲10的循環,創建10個子線程,每個子線程都嘗試獲取鎖,獲取到鎖後,等待2000毫秒再釋放鎖:
public static void main(String[] args) {
for(int i=0;i<10;i++){
new Thread(()->{
JedisLock lock=new JedisLock();
if(lock.lock(Thread.currentThread().getName(),10,0)){
System.out.println("線程"+Thread.currentThread().getName()+"獲得鎖啦");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
lock.unlock(Thread.currentThread().getName());
System.out.println("線程"+Thread.currentThread().getName()+"釋放鎖啦");
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
lock.close();
}).start();
}
}
這裏就用線程名作爲id字段,理論上應該有10/2=5個線程獲得鎖,輸出如下:
線程Thread-9獲得鎖啦
線程Thread-9釋放鎖啦
線程Thread-7獲得鎖啦
線程Thread-7釋放鎖啦
線程Thread-1獲得鎖啦
線程Thread-1釋放鎖啦
線程Thread-0獲得鎖啦
線程Thread-0釋放鎖啦
線程Thread-3獲得鎖啦
線程Thread-3釋放鎖啦
符合預期。以上代碼已上傳Github:https://github.com/Yanghanchen/jedistest