Redis API & Java RedisTemplate深入分析

袁志健發表於2018-10-25

Redis API

Redis是一種基於鍵值對的NoSQL資料庫。

在展開Redis API之前作為開發者的我們無論在用什麼樣的程式語言,開發什麼樣的專案都會有使用到將資料快取在記憶體中的場景。

如果讓我們自己開設計並開發一款基於鍵值對的快取資料庫我們該如何實現?

支援哪些資料結構?

  • 作為java coder的筆者就經常遇到需要將配置資訊、熱點高頻資料、統計資料、高效能需求資料快取到String、List、Map等資料結構的需求。

在快取資料時需要根據需求選擇合適的資料結構,Redis中提供了5種基本的資料結構。

  • string
  • hash
  • list
  • set
  • zset

string

字串是Redis中最基本的資料結構。Redis中的健都是以字串進行儲存的。字串可以是簡單字串、複雜字串(JSON、XML)、數字(整形、浮點型)、二進位制(圖片、音訊、視屏),其最大值不能超過512MB。

字串的使用場景很多可以將物件轉換成json字串儲存在Redis中。在分散式web伺服器中也可以使用字串儲存使用者session,保證請求在路由到新機器時能夠識別使用者身份無需二次登入。

當字串儲存數字時可以實現高效能分散式執行緒安全的的快速計數(類似Java中的AtomicLong#incrementAndGet 需要使用CAS實現執行緒安全,而Redis天生的單執行緒模型使其簡單高效地實現了計數功能),實現很多統計功能。

hash

field value
id 1
name 小明
age 19
birthday 1999-09-09

雜湊型別可以看做java中的map型別。在Redis中的雜湊型別的對映關係為field-value不是健對應的值,需要注意value不同的上下文。

雜湊可以儲存關係型資料庫表中的欄位和值,如可以將使用者資訊儲存在hash

list

列表型別用於儲存多個有序的字串。在Redis中可以對列表的兩端進行插入和彈出(類似java中的Deque雙端列隊),也可以像陣列一樣通過下標獲取對應的值。雙向連結串列的結構使其既可以充當棧也可以充當列隊的角色。

可以使用其從左邊push右邊pop的特性實現訊息列隊,比如註冊成功後的郵件通知使用Redis訊息列隊相比於MQ中介軟體將更加輕量易於維護。

使用列表的有序性以及可以按下標和範圍查詢的特性快取資料庫中的需要分頁顯示的列表資料。

微信朋友圈的動態就可以使用list進行實現,每當有好友釋出動態時就向list中儲存動態的id。其有序性保證了時間軸的實現。

set

集合用來儲存多個不同的字串元素。和java中的set一樣,集合是無序的不能用下標進行訪問,集合的唯一性可以用來儲存標籤系統中的tag如使用者的興趣愛好或是新聞系統中使用者關注的欄目等。

Redis中的集合型別NB的地方在於除了基本的增刪改查操作外還支援集合間的交集、並集、差集運算,這種特性將非常方便地解決了社交網路應用中的很多需求,如共同關注、共同喜好、二度好友等功能。

此外Redis提供隨機獲取集合中元素的api可以用於生成隨機數的業務中如抽獎系統等。

zset

有序集合是在集合的基礎上為每個元素設定分數(score)作為排序依據。有序集合增加了獲取指定分數的元素和元素範圍查詢、計算成員排名功能。

有序集合可以用在社交和遊戲中的排行榜需求中。

如何儲存資料(內部編碼)?

確定了支援的資料結構後我們需要設計合理的編碼(儲存的大小和查詢的時間複雜度)方式將不同資料結構的資料編碼成二進位制資料儲存在記憶體中。

在Redis中不同的資料結構都有多種內部編碼方式,在使用時需要根據實際的情況選擇合適的編碼以達到時間和空間的平衡。

內部編碼.png

如何運算元據(命令、協議)?

我們還需要設計對外開放的api 供外部系統訪問資料。

redis-cli

Redis提供了redis-cli 可以在命令列運算元據 Redis是一種基於鍵值對的NoSQL資料庫,它的5種資料結構都是健值對中的值。對於健來說有一些通用命令。

命令 描述
keys * 檢視所有健
dbsize 健總數
exists key 檢查健是否存在
del key [key ...] 刪除鍵
expire key seconds 鍵過期
type key 鍵的資料結構型別
object encoding key 值的內部編碼

後端開發的同學們在接觸Redis之前肯定學過至少一種關係型資料庫,下面以關係型資料庫的增、刪、改、查來總結Redis中不同資料結構的操作命令

string set key value [ex seconds] [px milliseconds] [nx|xx] ex seconds:為健設定秒級過期時間 px milliseconds:為健設定毫秒級過期時間 nx : 健必須不存在才能成功,用於新增 xx:健必須存在才能成功,用於更新 del key 同增 get key
hash hset key field value hdel key field 同增 hget key field
list rpush key value [value ...] lpush key value [value ...] linsert key before|after piovt value lpop key rpop key lrem count value ltrim key start end lset key index value lrange key start end lindex key index lllen key
set sadd key element [element ...] srem key element [element ...] 同增 scard key sismember key element srandmember key [count] spop key smenbers key
zset zadd key score member [score member ...] zrem key member 同增 zcard key zscore key member zrank key member zrevrank key member

redis 的命令有很多無需把每個命令都背下來只需要牢記5種資料結構的特性再根據實際的需求去尋找需要的命令就OK了。 更多命令見Redis命令參考

客戶端通訊協議RESP

Redis 定製了RESP協議實現客戶端與服務端的正常互動。這是一種基於TCP協議之上簡單高效的協議。

客戶端請求 如需要在Redis 中儲存鍵值對為hello-world 的資料,需要在客戶端傳送如下格式的資料(每行使用/r/n進行分割)給Redis伺服器。

*3
$3
SET
$5
hello
$5
world
複製程式碼

協議說明: *3表示引數量為3個即本條命令有3個引數 $3 $5 $5表示引數的位元組數。 上面的資料進行了格式化的顯示,實際傳輸的格式為如下程式碼

*3/r/n$3/r/nSET/r/n$5/r/nhello/r/n$5/r/nworld/r/n
複製程式碼

Redis服務端響應 Redis伺服器收到指令正確解析後返回如下資料

+OK
複製程式碼

協議說明: 狀態回覆:+ 錯誤回覆:- 整數回覆:: 字串回覆:$ 多條字串回覆:*

Redis客戶端

Jedis

jedis實現了RESP協議。

獲取jedis

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
    <type>jar</type>
    <scope>compile</scope>
</dependency>
複製程式碼

Jedis基本使用

// 建立連線
Jedis jedis = new Jedis("localhost", 6379);
// 儲存資料
jedis.set("foo", "bar");
// 獲取資料
String value = jedis.get("foo");
複製程式碼

Jedis連線池的使用

JedisPool pool = new JedisPool(new JedisPoolConfig(), "localhost", 6379);
/// Jedis implements Closeable. Hence, the jedis instance will be auto-closed after the last statement.
try (Jedis jedis = pool.getResource()) {
    /// ... do stuff here ... for example
    jedis.set("foo", "bar");
    String foobar = jedis.get("foo");
    jedis.zadd("sose", 0, "car"); jedis.zadd("sose", 0, "bike");
    Set<String> sose = jedis.zrange("sose", 0, -1);
    System.out.print(sose);
}
複製程式碼

Spring RedisTemplate

RedisTemplate基本使用

在理解RedisTemplate背後的原理前我們先看看其是如何操作Redis的。

...

@Autowired
private StringRedisTemplate stringRedisTemplate;

@Test
public void test() throws Exception {
    // 儲存字串
    stringRedisTemplate.opsForValue().set("aaa", "111");
    Assert.assertEquals("111", stringRedisTemplate.opsForValue().get("aaa"));
}

...
複製程式碼

我們需要向健aaa設定value為111需要先獲取ValueOperations物件然後進行相關命令操作。

RedisTemplate原始碼分析

針對Redis的支援的資料結構,從RedisTemplate原始碼中可知使用如下類封裝了相關資料結構的命令

  • ValueOperations (string)
  • ListOperations (list)
  • SetOperations (set)
  • ZSetOperations (zset)
  • GeoOperations (GEO)
  • HyperLogLogOperations (HyperLogLog)
public class RedisTemplate<K, V> extends RedisAccessor implements RedisOperations<K, V>, BeanClassLoaderAware {

   ...
   
   private @Nullable ValueOperations<K, V> valueOps;
   private @Nullable ListOperations<K, V> listOps;
   private @Nullable SetOperations<K, V> setOps;
   private @Nullable ZSetOperations<K, V> zSetOps;
   private @Nullable GeoOperations<K, V> geoOps;
   private @Nullable HyperLogLogOperations<K, V> hllOps;
   
   ...
 
 }
複製程式碼

在"RedisTemplate基本使用"所示的例子中通過spring的注入註解獲取了StringRedisTemplate物件的引用。

@Autowired
private StringRedisTemplate stringRedisTemplate;
複製程式碼

StringRedisTemplate又是如何被初始化的呢? 我們找到springboot 原始碼中的RedisAutoConfiguration如下所示

@Configuration
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {

   @Bean
   @ConditionalOnMissingBean(name = "redisTemplate")
   public RedisTemplate<Object, Object> redisTemplate(
         RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
      RedisTemplate<Object, Object> template = new RedisTemplate<>();
      template.setConnectionFactory(redisConnectionFactory);
      return template;
   }

   @Bean
   @ConditionalOnMissingBean
   public StringRedisTemplate stringRedisTemplate(
         RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
      StringRedisTemplate template = new StringRedisTemplate();
      template.setConnectionFactory(redisConnectionFactory);
      return template;
   }

}
複製程式碼

在RedisAutoConfiguration了初始化了RedisTemplate<Object, Object> 和 StringRedisTemplate物件,他們都依賴的一個引數 redisConnectionFactory,

redisConnectionFactory又是如何建立的呢?

通過IDE(intelliJ IDEA是真好用^_^)可以看到RedisConnectionFactory有兩個實現

redisConnectionFactory實現
檢視RedisConnectionFactory、JedisConnectionFactory、LettuceConnectionFactory類可知這邊使用抽象工廠模式

基於springboot是插拔式開箱即用特性我猜測這邊肯定有地方注入了連線工廠。 在RedisAutoConfiguration類所在的包下找到了JedisConnectionConfiguration、LettuceConnectionConfiguration

@Configuration
@ConditionalOnClass({ GenericObjectPool.class, JedisConnection.class, Jedis.class })
class JedisConnectionConfiguration extends RedisConnectionConfiguration {

...

@Bean
@ConditionalOnMissingBean(RedisConnectionFactory.class)
public JedisConnectionFactory redisConnectionFactory() throws UnknownHostException {
   return createJedisConnectionFactory();
}

...
}
複製程式碼
@Configuration
@ConditionalOnClass(RedisClient.class)
class LettuceConnectionConfiguration extends RedisConnectionConfiguration {

...
@Bean
@ConditionalOnMissingBean(RedisConnectionFactory.class)
public LettuceConnectionFactory redisConnectionFactory(
      ClientResources clientResources) throws UnknownHostException {
   LettuceClientConfiguration clientConfig = getLettuceClientConfiguration(
         clientResources, this.properties.getLettuce().getPool());
   return createLettuceConnectionFactory(clientConfig);
}
...

}
複製程式碼

根據spring的java 註解自動配置@ConditionalOnClass 發現在最新的springboot2.0中已經移除了jdeis預設整合了Lettuce(另一種redis java客戶端的實現)。 所以在自動裝配時會使用lettuce作為其連線底層,通過debug發現確實如此

springboot2.0預設裝配的Redis連線工廠
再回到StringRedisTemplate的父類RedisTemplate<K, V>這個類,其中K、V是泛型實現參考springboot原始碼中RedisAutoConfiguration預設自動配置實現,我們可以擴充套件自己需要的型別如key儲存string,物件轉成成json string 在redis中儲存配置如下程式碼所示:

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisJsonTemplate(RedisConnectionFactory jedisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(jedisConnectionFactory);

        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        @SuppressWarnings("all")
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);

        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        redisTemplate.setKeySerializer(stringRedisSerializer);
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);

        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

}
複製程式碼

ValueOperations、ListOperations等是如何呼叫jedis或者lettuce 的api的?

跟蹤stringRedisTemplate.opsForValue().set("aaa", "111");中的set方法

@Nullable
<T> T execute(RedisCallback<T> callback, boolean b) {
   return template.execute(callback, b);
}

@Override
public void set(K key, V value) {

   byte[] rawValue = rawValue(value);
   execute(new ValueDeserializingRedisCallback(key) {

      @Override
      protected byte[] inRedis(byte[] rawKey, RedisConnection connection) {
         connection.set(rawKey, rawValue);
         return null;
      }
   }, true);
}
複製程式碼

會統一走execute(命令模式)方法完成操作,注入匿名內部類建立的回撥物件用於獲取連線後執行具體的指令。 檢視execute實現可知最終回到RedisTemplate類中的execute執行相關操作。

RedisTemplate類中execute方法如下:

@Nullable
public <T> T execute(RedisCallback<T> action, boolean exposeConnection, boolean pipeline) {

   Assert.isTrue(initialized, "template not initialized; call afterPropertiesSet() before using it");
   Assert.notNull(action, "Callback object must not be null");
   // 獲取連線工廠
   RedisConnectionFactory factory = getRequiredConnectionFactory();
   RedisConnection conn = null;
   try {
      // 獲取連線  
      if (enableTransactionSupport) {
         // only bind resources in case of potential transaction synchronization
         conn = RedisConnectionUtils.bindConnection(factory, enableTransactionSupport);
      } else {
         conn = RedisConnectionUtils.getConnection(factory);
      }

      boolean existingConnection = TransactionSynchronizationManager.hasResource(factory);

      RedisConnection connToUse = preProcessConnection(conn, existingConnection);

      boolean pipelineStatus = connToUse.isPipelined();
      if (pipeline && !pipelineStatus) {
         connToUse.openPipeline();
      }

      RedisConnection connToExpose = (exposeConnection ? connToUse : createRedisConnectionProxy(connToUse));
      // 使用傳遞的回撥物件執行具體的命令
      T result = action.doInRedis(connToExpose);

      // close pipeline
      if (pipeline && !pipelineStatus) {
         connToUse.closePipeline();
      }

      // TODO: any other connection processing?
      return postProcessResult(result, connToUse, existingConnection);
   } finally {
      // 釋放連線
      RedisConnectionUtils.releaseConnection(conn, factory);
   }
}
複製程式碼

其流程可以總結為

  1. 獲取連線
  2. 執行命令(使用裝配的具體Reids客戶端完成相關命令)
  3. 釋放連線

和前面Jedis連線池的使用流程基本一致。

總結

RedisTemplate對Redis命令進行了統一的封裝對外具有一致的api 和配置,內部命令操作具體的實現由注入的redis客戶端完成 備註:springboot 1.5 Redis預設使用了jedis 客戶端 springboot 2.0 Redis預設使用了lettuce客戶端 ,增加了響應式api 的支援,有同學可能對lettuce不瞭解這裡引用一下lettuce專案github 的wiki 來說明一下

Lettuce is a scalable thread-safe Redis client for synchronous, asynchronous and reactive usage. Multiple threads may share one connection if they avoid blocking and transactional operations such as BLPOP and  MULTI/EXEC. Lettuce is built with netty. Supports advanced Redis features such as Sentinel, Cluster, Pipelining, Auto-Reconnect and Redis data models.

感覺非常強大的樣子? 最後附上RedisTemplate 操作Redis的demo github.com/yuanzj/Spri…

相關文章