緩存學習(八):Redis的Java客戶端

目錄

1 基本使用

2 高級特性

2.1 連接池JedisPool

2.2 管道

2.3 事務

2.4 發佈/訂閱

2.5 streams支持

3 利用Jedis實現一個簡單的分佈式鎖

3.1 構造方法

3.2 lock方法

3.3 unlock方法

3.4 測試


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的分佈式鎖框架有很多,官網列出的有:

這裏使用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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章