Redis API & Java RedisTemplate深入分析

Redis API

Redis是一種基于鍵值對的NoSQL數(shù)據(jù)庫。

在展開Redis API之前作為開發(fā)者的我們無論在用什么樣的編程語言,開發(fā)什么樣的項目都會有使用到將數(shù)據(jù)緩存在內(nèi)存中的場景。

如果讓我們自己開設(shè)計并開發(fā)一款基于鍵值對的緩存數(shù)據(jù)庫我們該如何實現(xiàn)?

支持哪些數(shù)據(jù)結(jié)構(gòu)?

  • 作為java coder的筆者就經(jīng)常遇到需要將配置信息、熱點高頻數(shù)據(jù)、統(tǒng)計數(shù)據(jù)、高性能需求數(shù)據(jù)緩存到String、List、Map等數(shù)據(jù)結(jié)構(gòu)的需求。

在緩存數(shù)據(jù)時需要根據(jù)需求選擇合適的數(shù)據(jù)結(jié)構(gòu),Redis中提供了5種基本的數(shù)據(jù)結(jié)構(gòu)。

  • string
  • hash
  • list
  • set
  • zset

string

字符串是Redis中最基本的數(shù)據(jù)結(jié)構(gòu)。Redis中的健都是以字符串進(jìn)行存儲的。字符串可以是簡單字符串、復(fù)雜字符串(JSON、XML)、數(shù)字(整形、浮點型)、二進(jìn)制(圖片、音頻、視屏),其最大值不能超過512MB。

字符串的使用場景很多可以將對象轉(zhuǎn)換成json字符串存儲在Redis中。在分布式web服務(wù)器中也可以使用字符串存儲用戶session,保證請求在路由到新機器時能夠識別用戶身份無需二次登錄。

當(dāng)字符串存儲數(shù)字時可以實現(xiàn)高性能分布式線程安全的的快速計數(shù)(類似Java中的AtomicLong#incrementAndGet 需要使用CAS實現(xiàn)線程安全,而Redis天生的單線程模型使其簡單高效地實現(xiàn)了計數(shù)功能),實現(xiàn)很多統(tǒng)計功能。

hash

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

哈希類型可以看做java中的map類型。在Redis中的哈希類型的映射關(guān)系為field-value不是健對應(yīng)的值,需要注意value不同的上下文。

哈希可以存儲關(guān)系型數(shù)據(jù)庫表中的字段和值,如可以將用戶信息存儲在hash

list

列表類型用于存儲多個有序的字符串。在Redis中可以對列表的兩端進(jìn)行插入和彈出(類似java中的Deque雙端列隊),也可以像數(shù)組一樣通過下標(biāo)獲取對應(yīng)的值。雙向鏈表的結(jié)構(gòu)使其既可以充當(dāng)棧也可以充當(dāng)列隊的角色。

可以使用其從左邊push右邊pop的特性實現(xiàn)消息列隊,比如注冊成功后的郵件通知使用Redis消息列隊相比于MQ中間件將更加輕量易于維護(hù)。

使用列表的有序性以及可以按下標(biāo)和范圍查找的特性緩存數(shù)據(jù)庫中的需要分頁顯示的列表數(shù)據(jù)。

微信朋友圈的動態(tài)就可以使用list進(jìn)行實現(xiàn),每當(dāng)有好友發(fā)布動態(tài)時就向list中存儲動態(tài)的id。其有序性保證了時間軸的實現(xiàn)。

set

集合用來保存多個不同的字符串元素。和java中的set一樣,集合是無序的不能用下標(biāo)進(jìn)行訪問,集合的唯一性可以用來存儲標(biāo)簽系統(tǒng)中的tag如用戶的興趣愛好或是新聞系統(tǒng)中用戶關(guān)注的欄目等。

Redis中的集合類型NB的地方在于除了基本的增刪改查操作外還支持集合間的交集、并集、差集運算,這種特性將非常方便地解決了社交網(wǎng)絡(luò)應(yīng)用中的很多需求,如共同關(guān)注、共同喜好、二度好友等功能。

此外Redis提供隨機獲取集合中元素的api可以用于生成隨機數(shù)的業(yè)務(wù)中如抽獎系統(tǒng)等。

zset

有序集合是在集合的基礎(chǔ)上為每個元素設(shè)置分?jǐn)?shù)(score)作為排序依據(jù)。有序集合增加了獲取指定分?jǐn)?shù)的元素和元素范圍查找、計算成員排名功能。

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

如何存儲數(shù)據(jù)(內(nèi)部編碼)?

確定了支持的數(shù)據(jù)結(jié)構(gòu)后我們需要設(shè)計合理的編碼(存儲的大小和查詢的時間復(fù)雜度)方式將不同數(shù)據(jù)結(jié)構(gòu)的數(shù)據(jù)編碼成二進(jìn)制數(shù)據(jù)存儲在內(nèi)存中。

在Redis中不同的數(shù)據(jù)結(jié)構(gòu)都有多種內(nèi)部編碼方式,在使用時需要根據(jù)實際的情況選擇合適的編碼以達(dá)到時間和空間的平衡。

內(nèi)部編碼.png

如何操作數(shù)據(jù)(命令、協(xié)議)?

我們還需要設(shè)計對外開放的api 供外部系統(tǒng)訪問數(shù)據(jù)。

redis-cli

Redis提供了redis-cli 可以在命令行操作數(shù)據(jù)
Redis是一種基于鍵值對的NoSQL數(shù)據(jù)庫,它的5種數(shù)據(jù)結(jié)構(gòu)都是健值對中的值。對于健來說有一些通用命令。

命令 描述
keys * 查看所有健
dbsize 健總數(shù)
exists key 檢查健是否存在
del key [key ...] 刪除鍵
expire key seconds 鍵過期
type key 鍵的數(shù)據(jù)結(jié)構(gòu)類型
object encoding key 值的內(nèi)部編碼

后端開發(fā)的同學(xué)們在接觸Redis之前肯定學(xué)過至少一種關(guān)系型數(shù)據(jù)庫,下面以關(guān)系型數(shù)據(jù)庫的增、刪、改、查來總結(jié)Redis中不同數(shù)據(jù)結(jié)構(gòu)的操作命令

string set key value [ex seconds] [px milliseconds] [nx|xx] ex seconds:為健設(shè)置秒級過期時間 px milliseconds:為健設(shè)置毫秒級過期時間 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種數(shù)據(jù)結(jié)構(gòu)的特性再根據(jù)實際的需求去尋找需要的命令就OK了。
更多命令見Redis命令參考

客戶端通信協(xié)議RESP

Redis 定制了RESP協(xié)議實現(xiàn)客戶端與服務(wù)端的正常交互。這是一種基于TCP協(xié)議之上簡單高效的協(xié)議。

客戶端請求
如需要在Redis 中存儲鍵值對為hello-world 的數(shù)據(jù),需要在客戶端發(fā)送如下格式的數(shù)據(jù)(每行使用/r/n進(jìn)行分割)給Redis服務(wù)器。

*3
$3
SET
$5
hello
$5
world

協(xié)議說明:
*3表示參數(shù)量為3個即本條命令有3個參數(shù)
35 $5表示參數(shù)的字節(jié)數(shù)。
上面的數(shù)據(jù)進(jìn)行了格式化的顯示,實際傳輸?shù)母袷綖槿缦麓a

*3/r/n$3/r/nSET/r/n$5/r/nhello/r/n$5/r/nworld/r/n

Redis服務(wù)端響應(yīng)
Redis服務(wù)器收到指令正確解析后返回如下數(shù)據(jù)

+OK

協(xié)議說明:
狀態(tài)回復(fù):+
錯誤回復(fù):-
整數(shù)回復(fù)::
字符串回復(fù):$
多條字符串回復(fù):*

Redis客戶端

Jedis

jedis實現(xiàn)了RESP協(xié)議。

獲取jedis

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
    <type>jar</type>
    <scope>compile</scope>
</dependency>

Jedis基本使用

// 創(chuàng)建連接
Jedis jedis = new Jedis("localhost", 6379);
// 存儲數(shù)據(jù)
jedis.set("foo", "bar");
// 獲取數(shù)據(jù)
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設(shè)置value為111需要先獲取ValueOperations對象然后進(jìn)行相關(guān)命令操作。

RedisTemplate源碼分析

針對Redis的支持的數(shù)據(jù)結(jié)構(gòu),從RedisTemplate源碼中可知使用如下類封裝了相關(guān)數(shù)據(jù)結(jié)構(gòu)的命令

  • 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對象,他們都依賴的一個參數(shù) redisConnectionFactory,

redisConnectionFactory又是如何創(chuàng)建的呢?

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

redisConnectionFactory實現(xiàn)

查看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);
}
...

}

根據(jù)spring的java 注解自動配置@ConditionalOnClass 發(fā)現(xiàn)在最新的springboot2.0中已經(jīng)移除了jdeis默認(rèn)集成了Lettuce(另一種redis java客戶端的實現(xiàn))。
所以在自動裝配時會使用lettuce作為其連接底層,通過debug發(fā)現(xiàn)確實如此

springboot2.0默認(rèn)裝配的Redis連接工廠

再回到StringRedisTemplate的父類RedisTemplate<K, V>這個類,其中K、V是泛型實現(xiàn)參考springboot源碼中RedisAutoConfiguration默認(rèn)自動配置實現(xiàn),我們可以擴展自己需要的類型如key存儲string,對象轉(zhuǎn)成成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等是如何調(diào)用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);
}

會統(tǒng)一走execute(命令模式)方法完成操作,注入匿名內(nèi)部類創(chuàng)建的回調(diào)對象用于獲取連接后執(zhí)行具體的指令
查看execute實現(xiàn)可知最終回到RedisTemplate類中的execute執(zhí)行相關(guān)操作。

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));
      // 使用傳遞的回調(diào)對象執(zhí)行具體的命令
      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);
   }
}

其流程可以總結(jié)為

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

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

總結(jié)

RedisTemplate對Redis命令進(jìn)行了統(tǒng)一的封裝對外具有一致的api 和配置,內(nèi)部命令操作具體的實現(xiàn)由注入的redis客戶端完成
備注:springboot 1.5 Redis默認(rèn)使用了jedis 客戶端
springboot 2.0 Redis默認(rèn)使用了lettuce客戶端 ,增加了響應(yīng)式api 的支持,有同學(xué)可能對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
https://github.com/yuanzj/SpringBootRedisDemo

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。