Springboot2(46)集成redis(redisson)

源碼地址

springboot2教程系列

redis cluster安裝
Springboot2(32)集成redis(jedis)
Redis(1)常用操作命令

Redis(2)集羣redis-cluster & redis主從同步

Redis(3)內存回收原理,及內存過期淘汰策略詳解

Redis(4)阿里雲-開發規範

Redis(5)n種妙用,不僅僅是緩存

添加依賴

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.10.6</version>
</dependency>

添加redisson-config.yml

在項目的resources目錄下,添加redisson的配置文件(這裏使用yaml格式的配置文件redisson-config.yml,文件名可自己定, 文件的示例配置如下)

clusterServersConfig:
  idleConnectionTimeout: 10000
  pingTimeout: 1000
  connectTimeout: 10000
  timeout: 3000
  retryAttempts: 3
  retryInterval: 1500
  reconnectionTimeout: 3000
  failedAttempts: 3
  password: null
  subscriptionsPerConnection: 5
  clientName: null
  loadBalancer: !<org.redisson.connection.balancer.RoundRobinLoadBalancer> {}
  slaveSubscriptionConnectionMinimumIdleSize: 1
  slaveSubscriptionConnectionPoolSize: 50
  slaveConnectionMinimumIdleSize: 32
  slaveConnectionPoolSize: 64
  masterConnectionMinimumIdleSize: 32
  masterConnectionPoolSize: 64
  readMode: "SLAVE"
  nodeAddresses:
  - "redis://10.10.2.139:7001"
  - "redis://10.10.2.139:7012"
  - "redis://10.10.2.139:7000"
  - "redis://10.10.2.139:7003"
  - "redis://10.10.2.139:7004"
  - "redis://10.10.2.139:7005"
  scanInterval: 1000
threads: 0
nettyThreads: 0
codec: !<org.redisson.codec.JsonJacksonCodec> {}
transportMode: "NIO"

具體想加些什麼配置,可以查看Redisson提供的Config類裏支持的參數,不支持添加里面沒有的參數配置。除了yaml類型格式的配置,也支持json格式的配置文件,具體不在這裏展開,詳情可以參考Redisson配置方法

添加Redisson的配置參數讀取類RedissonConfig

@Configuration
public class RedissonConfig {
    @Bean
    public RedissonClient redisson() throws IOException {
        // 本例子使用的是yaml格式的配置文件,讀取使用Config.fromYAML,如果是Json文件,則使用Config.fromJSON
        Config config = Config.fromYAML(
            RedissonConfig.class.getClassLoader().getResource("redisson-config.yml"));
        return Redisson.create(config);
    }

}

常用功能

話題(訂閱分發)

監聽topic(“anyTopic”)的消息

@PostConstruct
public void topic(){
    RTopic topic = redissonClient.getTopic("anyTopic");
    topic.addListener(String.class, new MessageListener<String>() {
        @Override
        public void onMessage(CharSequence charSequence, String s) {
            System.out.println(s);
        }
    });
}

在Redis節點故障轉移(主從切換)或斷線重連以後,所有的話題監聽器將自動完成話題的重新訂閱。

發佈消息

RTopic topic = redisson.getTopic("anyTopic");

topic支持模糊匹配

// 訂閱所有滿足`topic1.*`表達式的話題
RPatternTopic topic1 = redisson.getPatternTopic("topic1.*");
int listenerId = topic1.addListener(Message.class, new PatternMessageListener<Message>() {
    @Override
    public void onMessage(String pattern, String channel, Message msg) {
         Assert.fail();
    }
});

在Redis節點故障轉移(主從切換)或斷線重連以後,所有的模糊話題監聽器將自動完成話題的重新訂閱。

布隆過濾器(Bloom Filter)

Redisson利用Redis實現了Java分佈式布隆過濾器(Bloom Filter)。所含最大比特數量爲2^32

RBloomFilter<Integer> bloomFilter = redissonClient.getBloomFilter("sample");
// 初始化布隆過濾器,預計統計元素數量爲55000000,期望誤差率爲0.03
bloomFilter.tryInit(55000000L, 0.003);
//添加元素
bloomFilter.add(1);
boolean b = bloomFilter.contains(1);
boolean b1 = bloomFilter.contains(2);

限流器(RateLimiter)

基於Redis的分佈式限流器(RateLimiter)可以用來在分佈式環境下現在請求方的調用頻率。既適用於不同Redisson實例下的多線程限流,也適用於相同Redisson實例下的多線程限流。該算法不保證公平性。

RRateLimiter rateLimiter = redisson.getRateLimiter("myRateLimiter");
// 初始化
// 最大流速 = 每1秒鐘產生10個令牌
rateLimiter.trySetRate(RateType.OVERALL, 10, 1, RateIntervalUnit.SECONDS);

CountDownLatch latch = new CountDownLatch(2);
limiter.acquire(3);
// ...

Thread t = new Thread(() -> {
    limiter.acquire(2);
    // ...        
});

分佈式集合(以map爲例)

基於Redis的Redisson的分佈式映射結構的RMap Java對象實現了java.util.concurrent.ConcurrentMap接口和java.util.Map接口。與HashMap不同的是,RMap保持了元素的插入順序。該對象的最大容量受Redis限制,最大元素數量是4 294 967 295個。

RMap<String, String> map = redissonClient.getMap("anyMap");
map.put("test","test");

元素淘汰功能(Eviction)

Redisson的分佈式的RMapCache Java對象在基於RMap的前提下實現了針對單個元素的淘汰機制。同時仍然保留了元素的插入順序。由於RMapCache是基於RMap實現的,使它同時繼承了java.util.concurrent.ConcurrentMap接口和java.util.Map接口。Redisson提供的Spring Cache整合以及JCache正是基於這樣的功能來實現的。

目前的Redis自身並不支持散列(Hash)當中的元素淘汰,因此所有過期元素都是通過org.redisson.EvictionScheduler實例來實現定期清理的。爲了保證資源的有效利用,每次運行最多清理300個過期元素。任務的啓動時間將根據上次實際清理數量自動調整,間隔時間趨於1秒到1小時之間。比如該次清理時刪除了300條元素,那麼下次執行清理的時間將在1秒以後(最小間隔時間)。一旦該次清理數量少於上次清理數量,時間間隔將增加1.5倍。

RMapCache<String, SomeObject> map = redisson.getMapCache("anyMap");
// 有效時間 ttl = 10分鐘
map.put("key1", new SomeObject(), 10, TimeUnit.MINUTES);
// 有效時間 ttl = 10分鐘, 最長閒置時間 maxIdleTime = 10秒鐘
map.put("key1", new SomeObject(), 10, TimeUnit.MINUTES, 10, TimeUnit.SECONDS);

// 有效時間 = 3 秒鐘
map.putIfAbsent("key2", new SomeObject(), 3, TimeUnit.SECONDS);
// 有效時間 ttl = 40秒鐘, 最長閒置時間 maxIdleTime = 10秒鐘
map.putIfAbsent("key2", new SomeObject(), 40, TimeUnit.SECONDS, 10, TimeUnit.SECONDS);

本地緩存功能(Local Cache)

在特定的場景下,映射(Map)上的高度頻繁的讀取操作,使網絡通信都被視爲瓶頸時,使用Redisson提供的帶有本地緩存功能的分佈式本地緩存映射RLocalCachedMapJava對象會是一個很好的選擇。它同時實現了java.util.concurrent.ConcurrentMapjava.util.Map兩個接口。本地緩存功能充分的利用了JVM的自身內存空間,對部分常用的元素實行就地緩存,這樣的設計讓讀取操作的性能較分佈式映射相比提高最多 45倍 。以下配置參數可以用來創建這個實例:

LocalCachedMapOptions options = LocalCachedMapOptions.defaults()
      // 用於淘汰清除本地緩存內的元素
      // 共有以下幾種選擇:
      // LFU - 統計元素的使用頻率,淘汰用得最少(最不常用)的。
      // LRU - 按元素使用時間排序比較,淘汰最早(最久遠)的。
      // SOFT - 元素用Java的WeakReference來保存,緩存元素通過GC過程清除。
      // WEAK - 元素用Java的SoftReference來保存, 緩存元素通過GC過程清除。
      // NONE - 永不淘汰清除緩存元素。
     .evictionPolicy(EvictionPolicy.NONE)
     // 如果緩存容量值爲0表示不限制本地緩存容量大小
     .cacheSize(1000)
      // 以下選項適用於斷線原因造成了未收到本地緩存更新消息的情況。
      // 斷線重連的策略有以下幾種:
      // CLEAR - 如果斷線一段時間以後則在重新建立連接以後清空本地緩存
      // LOAD - 在服務端保存一份10分鐘的作廢日誌
      //        如果10分鐘內重新建立連接,則按照作廢日誌內的記錄清空本地緩存的元素
      //        如果斷線時間超過了這個時間,則將清空本地緩存中所有的內容
      // NONE - 默認值。斷線重連時不做處理。
     .reconnectionStrategy(ReconnectionStrategy.NONE)
      // 以下選項適用於不同本地緩存之間相互保持同步的情況
      // 緩存同步策略有以下幾種:
      // INVALIDATE - 默認值。當本地緩存映射的某條元素髮生變動時,同時驅逐所有相同本地緩存映射內的該元素
      // UPDATE - 當本地緩存映射的某條元素髮生變動時,同時更新所有相同本地緩存映射內的該元素
      // NONE - 不做任何同步處理
     .syncStrategy(SyncStrategy.INVALIDATE)
      // 每個Map本地緩存裏元素的有效時間,默認毫秒爲單位
     .timeToLive(10000)
      // 或者
     .timeToLive(10, TimeUnit.SECONDS)
      // 每個Map本地緩存裏元素的最長閒置時間,默認毫秒爲單位
     .maxIdle(10000)
      // 或者
     .maxIdle(10, TimeUnit.SECONDS);
RLocalCachedMap<String, Integer> map = redisson.getLocalCachedMap("test", options);

String prevObject = map.put("123", 1);
String currentObject = map.putIfAbsent("323", 2);
String obj = map.remove("123");

// 在不需要舊值的情況下可以使用fast爲前綴的類似方法
map.fastPut("a", 1);
map.fastPutIfAbsent("d", 32);
map.fastRemove("b");

RFuture<String> putAsyncFuture = map.putAsync("321");
RFuture<Void> fastPutAsyncFuture = map.fastPutAsync("321");

map.fastPutAsync("321", new SomeObject());
map.fastRemoveAsync("321");

當不再使用Map本地緩存對象的時候應該手動銷燬,如果Redisson對象被關閉(shutdown)了,則不用手動銷燬。

RLocalCachedMap<String, Integer> map = ...
map.destroy();

如何通過加載數據的方式來降低過期淘汰事件發佈信息對網絡的影響

代碼範例:

public void loadData(String cacheName, Map<String, String> data) {
    RLocalCachedMap<String, String> clearMap = redisson.getLocalCachedMap(cacheName, 
            LocalCachedMapOptions.defaults().cacheSize(1).syncStrategy(SyncStrategy.INVALIDATE));
    RLocalCachedMap<String, String> loadMap = redisson.getLocalCachedMap(cacheName, 
            LocalCachedMapOptions.defaults().cacheSize(1).syncStrategy(SyncStrategy.NONE));
    
    loadMap.putAll(data);
    clearMap.clearLocalCache();
}

映射監聽器(Map Listener)

Redisson爲所有實現了RMapCacheRLocalCachedMapCache接口的對象提供了監聽以下事件的監聽器:

事件 | 監聽器 元素 添加 事件 | org.redisson.api.map.event.EntryCreatedListener
元素 過期 事件 | org.redisson.api.map.event.EntryExpiredListener
元素 刪除 事件 | org.redisson.api.map.event.EntryRemovedListener
元素 更新 事件 | org.redisson.api.map.event.EntryUpdatedListener

RMapCache<String, Integer> map = redisson.getMapCache("myMap");
// 或
RLocalCachedMapCache<String, Integer> map = redisson.getLocalCachedMapCache("myMap", options);

int updateListener = map.addListener(new EntryUpdatedListener<Integer, Integer>() {
     @Override
     public void onUpdated(EntryEvent<Integer, Integer> event) {
          event.getKey(); // 字段名
          event.getValue() // 新值
          event.getOldValue() // 舊值
          // ...
     }
});

int createListener = map.addListener(new EntryCreatedListener<Integer, Integer>() {
     @Override
     public void onCreated(EntryEvent<Integer, Integer> event) {
          event.getKey(); // 字段名
          event.getValue() // 值
          // ...
     }
});

int expireListener = map.addListener(new EntryExpiredListener<Integer, Integer>() {
     @Override
     public void onExpired(EntryEvent<Integer, Integer> event) {
          event.getKey(); // 字段名
          event.getValue() // 值
          // ...
     }
});

int removeListener = map.addListener(new EntryRemovedListener<Integer, Integer>() {
     @Override
     public void onRemoved(EntryEvent<Integer, Integer> event) {
          event.getKey(); // 字段名
          event.getValue() // 值
          // ...
     }
});

map.removeListener(updateListener);
map.removeListener(createListener);
map.removeListener(expireListener);
map.removeListener(removeListener);

命令的批量執行

多個連續命令可以通過RBatch對象在一次網絡會話請求裏合併發送,這樣省去了產生多個請求消耗的時間和資源。這在Redis中叫做管道

用戶可以通過以下方式調整通過管道方式發送命令的方式:

BatchOptions options = BatchOptions.defaults()
// 指定執行模式
//
// ExecutionMode.REDIS_READ_ATOMIC - 所有命令緩存在Redis節點中,以原子性事務的方式執行。
//
// ExecutionMode.REDIS_WRITE_ATOMIC - 所有命令緩存在Redis節點中,以原子性事務的方式執行。
//
// ExecutionMode.IN_MEMORY - 所有命令緩存在Redisson本機內存中統一發送,但逐一執行(非事務)。默認模式。
//
// ExecutionMode.IN_MEMORY_ATOMIC - 所有命令緩存在Redisson本機內存中統一發送,並以原子性事務的方式執行。
//
.executionMode(ExecutionMode.IN_MEMORY)

// 告知Redis不用返回結果(可以減少網絡用量)
.skipResult()

// 將寫入操作同步到從節點
// 同步到2個從節點,等待時間爲1秒鐘
.syncSlaves(2, 1, TimeUnit.SECONDS)

// 處理結果超時爲2秒鐘
.responseTimeout(2, TimeUnit.SECONDS)

// 命令重試等待間隔時間爲2秒鐘
.retryInterval(2, TimeUnit.SECONDS);

// 命令重試次數。僅適用於未發送成功的命令
.retryAttempts(4);

使用方式如下:

RBatch batch = redisson.createBatch();
batch.getMap("test").fastPutAsync("1", "2");
batch.getMap("test").fastPutAsync("2", "3");
batch.getMap("test").putAsync("2", "5");
batch.getAtomicLongAsync("counter").incrementAndGetAsync();
batch.getAtomicLongAsync("counter").incrementAndGetAsync();

BatchResult res = batch.execute();
// 或者
Future<BatchResult> asyncRes = batch.executeAsync();
List<?> response = res.getResponses();
res.getSyncedSlaves();

在集羣模式下,所有的命令會按各個槽所在的節點,篩選分配到各個節點並同時發送。每個節點返回的結果將會彙總到最終的結果列表裏。

腳本執行

redisson.getBucket("foo").set("bar");
String r = redisson.getScript().eval(Mode.READ_ONLY,
   "return redis.call('get', 'foo')", RScript.ReturnType.VALUE);

// 通過預存的腳本進行同樣的操作
RScript s = redisson.getScript();
// 首先將腳本保存到所有的Redis主節點
String res = s.scriptLoad("return redis.call('get', 'foo')");
// 返回值 res == 282297a0228f48cd3fc6a55de6316f31422f5d17

// 再通過SHA值調用腳本
Future<Object> r1 = redisson.getScript().evalShaAsync(Mode.READ_ONLY,
   "282297a0228f48cd3fc6a55de6316f31422f5d17",
   RScript.ReturnType.VALUE, Collections.emptyList());

分佈式調度任務服務(Scheduler Service)

分佈式調度任務服務概述

Redisson的分佈式調度任務服務實現了java.util.concurrent.ScheduledExecutorService接口,支持在不同的獨立節點裏執行基於java.util.concurrent.Callable接口或java.lang.Runnable接口的任務。Redisson獨立節點按順序運行Redis列隊裏的任務。調度任務是一種需要在未來某個指定時間運行一次或多次的特殊任務。

設定任務計劃

Redisson獨立節點不要求任務的類在類路徑裏。他們會自動被Redisson獨立節點的ClassLoader加載。因此每次執行一個新任務時,不需要重啓Redisson獨立節點。

採用Callable任務的範例:

public class CallableTask implements Callable<Long> {

    @RInject
    private RedissonClient redissonClient;

    @Override
    public Long call() throws Exception {
        RMap<String, Integer> map = redissonClient.getMap("myMap");
        Long result = 0;
        for (Integer value : map.values()) {
            result += value;
        }
        return result;
    }

}

在創建ExecutorService時可以配置以下參數:

ExecutorOptions options = ExecutorOptions.defaults()

// 指定重新嘗試執行任務的時間間隔。
// ExecutorService的工作節點將等待10分鐘後重新嘗試執行任務
//
// 設定爲0則不進行重試
//
// 默認值爲5分鐘
options.taskRetryInterval(10, TimeUnit.MINUTES);
RScheduledExecutorService executorService = redisson.getExecutorService("myExecutor");
ScheduledFuture<Long> future = executorService.schedule(new CallableTask(), 10, TimeUnit.MINUTES);
Long result = future.get();

使用Lambda任務的範例:

RExecutorService executorService = redisson.getExecutorService("myExecutor", options);
ScheduledFuture<Long> future = executorService.schedule((Callable & Serializable)() -> {
      System.out.println("task has been executed!");
}, 10, TimeUnit.MINUTES);
Long result = future.get();

採用Runnable任務的範例:

public class RunnableTask implements Runnable {

    @RInject
    private RedissonClient redissonClient;

    private long param;

    public RunnableTask() {
    }

    public RunnableTask(long param) {
        this.param= param;
    }

    @Override
    public void run() {
        RAtomicLong atomic = redissonClient.getAtomicLong("myAtomic");
        atomic.addAndGet(param);
    }

}

RScheduledExecutorService executorService = redisson.getExecutorService("myExecutor");
ScheduledFuture<?> future1 = executorService.schedule(new RunnableTask(123), 10, TimeUnit.HOURS);
// ...
ScheduledFuture<?> future2 = executorService.scheduleAtFixedRate(new RunnableTask(123), 10, 25, TimeUnit.HOURS);
// ...
ScheduledFuture<?> future3 = executorService.scheduleWithFixedDelay(new RunnableTask(123), 5, 10, TimeUnit.HOURS);

通過CRON表達式設定任務計劃

在分佈式調度任務中,可以通過CRON表達式來爲任務設定一個更復雜的計劃。表達式與Quartz的CRON格式完全兼容。

例如:

RScheduledExecutorService executorService = redisson.getExecutorService("myExecutor");
executorService.schedule(new RunnableTask(), CronSchedule.of("10 0/5 * * * ?"));
// ...
executorService.schedule(new RunnableTask(), CronSchedule.dailyAtHourAndMinute(10, 5));
// ...
executorService.schedule(new RunnableTask(), CronSchedule.weeklyOnDayAndHourAndMinute(12, 4, Calendar.MONDAY, Calendar.FRIDAY));

取消計劃任務

分佈式調度任務服務提供了兩張取消任務的方式:通過調用ScheduledFuture.cancel()方法或調用RScheduledExecutorService.cancelScheduledTask方法。通過對Thread.currentThread().isInterrupted()方法的調用可以在已經處於運行狀態的任務裏實現任務中斷:

public class RunnableTask implements Callable<Long> {

    @RInject
    private RedissonClient redissonClient;

    @Override
    public Long call() throws Exception {
        RMap<String, Integer> map = redissonClient.getMap("myMap");
        Long result = 0;
        // map裏包含了許多的元素
        for (Integer value : map.values()) {
           if (Thread.currentThread().isInterrupted()) {
                // 任務被取消了
                return null;
           }
           result += value;
        }
        return result;
    }

}

RScheduledExecutorService executorService = redisson.getExecutorService("myExecutor");
RScheduledFuture<Long> future = executorService.scheduleAsync(new RunnableTask(), CronSchedule.dailyAtHourAndMinute(10, 5));
// ...
future.cancel(true);
// 或
String taskId = future.getTaskId();
// ...
executorService.cancelScheduledTask(taskId);
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章