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中的健都是以字符串進行存儲的。字符串可以是簡單字符串桩蓉、復(fù)雜字符串(JSON淋纲、XML)、數(shù)字(整形院究、浮點型)洽瞬、二進制(圖片、音頻业汰、視屏)伙窃,其最大值不能超過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中可以對列表的兩端進行插入和彈出(類似java中的Deque雙端列隊)执桌,也可以像數(shù)組一樣通過下標獲取對應(yīng)的值鄙皇。雙向鏈表的結(jié)構(gòu)使其既可以充當(dāng)棧也可以充當(dāng)列隊的角色。
可以使用其從左邊push右邊pop的特性實現(xiàn)消息列隊仰挣,比如注冊成功后的郵件通知使用Redis消息列隊相比于MQ中間件將更加輕量易于維護伴逸。
使用列表的有序性以及可以按下標和范圍查找的特性緩存數(shù)據(jù)庫中的需要分頁顯示的列表數(shù)據(jù)。
微信朋友圈的動態(tài)就可以使用list進行實現(xiàn)膘壶,每當(dāng)有好友發(fā)布動態(tài)時就向list中存儲動態(tài)的id错蝴。其有序性保證了時間軸的實現(xiàn)。
set
集合用來保存多個不同的字符串元素颓芭。和java中的set一樣顷锰,集合是無序的不能用下標進行訪問,集合的唯一性可以用來存儲標簽系統(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è)置分數(shù)(score)作為排序依據(jù)。有序集合增加了獲取指定分數(shù)的元素和元素范圍查找薪丁、計算成員排名功能遇西。
有序集合可以用在社交和游戲中的排行榜需求中。
如何存儲數(shù)據(jù)(內(nèi)部編碼)严嗜?
確定了支持的數(shù)據(jù)結(jié)構(gòu)后我們需要設(shè)計合理的編碼(存儲的大小和查詢的時間復(fù)雜度)方式將不同數(shù)據(jù)結(jié)構(gòu)的數(shù)據(jù)編碼成二進制數(shù)據(jù)存儲在內(nèi)存中粱檀。
在Redis中不同的數(shù)據(jù)結(jié)構(gòu)都有多種內(nèi)部編碼方式,在使用時需要根據(jù)實際的情況選擇合適的編碼以達到時間和空間的平衡漫玄。
如何操作數(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進行分割)給Redis服務(wù)器。
*3
$3
SET
$5
hello
$5
world
協(xié)議說明:
*3表示參數(shù)量為3個即本條命令有3個參數(shù)
5 $5表示參數(shù)的字節(jié)數(shù)虑稼。
上面的數(shù)據(jù)進行了格式化的顯示琳钉,實際傳輸?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對象然后進行相關(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、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默認集成了Lettuce(另一種redis java客戶端的實現(xiàn))倔监。
所以在自動裝配時會使用lettuce作為其連接底層耍攘,通過debug發(fā)現(xiàn)確實如此
再回到StringRedisTemplate的父類RedisTemplate<K, V>這個類目代,其中K帮匾、V是泛型實現(xiàn)參考springboot源碼中RedisAutoConfiguration默認自動配置實現(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é)為
- 獲取連接
- 執(zhí)行命令(使用裝配的具體Reids客戶端完成相關(guān)命令)
- 釋放連接
和前面Jedis連接池的使用流程基本一致。
總結(jié)
RedisTemplate對Redis命令進行了統(tǒng)一的封裝對外具有一致的api 和配置峡碉,內(nèi)部命令操作具體的實現(xiàn)由注入的redis客戶端完成
備注:springboot 1.5 Redis默認使用了jedis 客戶端
springboot 2.0 Redis默認使用了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