Redis 簡(jiǎn)介
Redis 是一個(gè)開(kāi)源(BSD 許可)、內(nèi)存存儲(chǔ)的數(shù)據(jù)結(jié)構(gòu)服務(wù)器,可用作數(shù)據(jù)庫(kù),高速緩存和消息隊(duì)列代理。它支持字符串、哈希表、列表、集合、有序集合等數(shù)據(jù)類型。內(nèi)置復(fù)制、Lua 腳本、LRU 收回、事務(wù)以及不同級(jí)別磁盤持久化功能,同時(shí)通過(guò) Redis Sentinel 提供高可用,通過(guò) Redis Cluster 提供自動(dòng)分區(qū)。
Redis 使用場(chǎng)景
微服務(wù)以及分布式被廣泛使用后,Redis 的使用場(chǎng)景就越來(lái)越多了,這里我羅列了主要的幾種場(chǎng)景。
- 分布式緩存:在分布式的系統(tǒng)架構(gòu)中,將緩存存儲(chǔ)在內(nèi)存中顯然不當(dāng),因?yàn)榫彺嫘枰c其他機(jī)器共享,這時(shí) Redis 便挺身而出了,緩存也是 Redis 使用最多的場(chǎng)景。
- 分布式鎖:在高并發(fā)的情況下,我們需要一個(gè)鎖來(lái)防止并發(fā)帶來(lái)的臟數(shù)據(jù),Java 自帶的鎖機(jī)制顯然對(duì)進(jìn)程間的并發(fā)并不好使,此時(shí)可以利用 Redis 單線程的特性來(lái)實(shí)現(xiàn)我們的分布式鎖,如何實(shí)現(xiàn),可以參考這篇文章。
- Session 存儲(chǔ)/共享:Redis 可以將 Session 持久化到存儲(chǔ)中,這樣可以避免由于機(jī)器宕機(jī)而丟失用戶會(huì)話信息。(個(gè)人認(rèn)為更重要的是因?yàn)榉?wù)集群的出現(xiàn),需要一個(gè)分布式Session來(lái)作為統(tǒng)一的會(huì)話)
- 發(fā)布/訂閱:Redis 還有一個(gè)發(fā)布/訂閱的功能,您可以設(shè)定對(duì)某一個(gè) key 值進(jìn)行消息發(fā)布及消息訂閱,當(dāng)一個(gè) key 值上進(jìn)行了消息發(fā)布后,所有訂閱它的客戶端都會(huì)收到相應(yīng)的消息。這一功能最明顯的用法就是用作實(shí)時(shí)消息系統(tǒng)。
-
任務(wù)隊(duì)列:Redis 的
lpush+brpop命令組合即可實(shí)現(xiàn)阻塞隊(duì)列,生產(chǎn)者客戶端使用lrpush從列表左側(cè)插入元素,多個(gè)消費(fèi)者客戶端使用brpop命令阻塞式的”搶”列表尾部的元素,多個(gè)客戶端保證了消費(fèi)的負(fù)載均衡和高可用性。 - 限速,接口訪問(wèn)頻率限制:比如發(fā)送短信驗(yàn)證碼的接口,通常為了防止別人惡意頻刷,會(huì)限制用戶每分鐘獲取驗(yàn)證碼的頻率,例如一分鐘不能超過(guò) 5 次。
當(dāng)然 Redis 的使用場(chǎng)景并不僅僅只有這么多,還有很多未列出的場(chǎng)景,如計(jì)數(shù)、排行榜等。
Redis 數(shù)據(jù)類型
前面也提到過(guò),Redis 支持字符串、哈希表、列表、集合、有序集合五種數(shù)據(jù)類型的存儲(chǔ)。
字符串(string)
- string類型是二進(jìn)制安全的。意思是redis的string可以包含任何數(shù)據(jù)。比如jpg圖片或者序列化的對(duì)象 。
- string類型是Redis最基本的數(shù)據(jù)類型,一個(gè)鍵最大能存儲(chǔ)512MB。
string 這種數(shù)據(jù)結(jié)構(gòu)應(yīng)該是我們最為常用的。在 Redis 中 string 表示的是一個(gè)可變的字節(jié)數(shù)組,我們初始化字符串的內(nèi)容、可以拿到字符串的長(zhǎng)度,可以獲取 string 的子串,可以覆蓋 string 的子串內(nèi)容,可以追加子串。
Redis 的 string 類型數(shù)據(jù)結(jié)構(gòu)
如上圖所示,在 Redis 中我們初始化一個(gè)字符串時(shí),會(huì)采用預(yù)分配冗余空間的方式來(lái)減少內(nèi)存的頻繁分配,如圖 1 所示,實(shí)際分配的空間 capacity 一般要高于實(shí)際字符串長(zhǎng)度 len。如果您看過(guò) Java 的 ArrayList 的源碼相信會(huì)對(duì)此種模式很熟悉。
列表(list)
- redis列表是簡(jiǎn)單的字符串列表,排序?yàn)椴迦氲捻樞颉A斜淼淖畲箝L(zhǎng)度為2^32-1。
- redis的列表是使用鏈表實(shí)現(xiàn)的,這意味著,即使列表中有上百萬(wàn)個(gè)元素,增加一個(gè)元素到列表的頭部或尾部的操作都是在常量的時(shí)間完成。
- 可以用列表獲取最新的內(nèi)容(像帖子,微博等),用ltrim很容易就會(huì)獲取最新的內(nèi)容,并移除舊的內(nèi)容。
- 用列表可以實(shí)現(xiàn)生產(chǎn)者消費(fèi)者模式,生產(chǎn)者調(diào)用lpush添加項(xiàng)到列表中,消費(fèi)者調(diào)用rpop從列表中提取,如果沒(méi)有元素,則輪詢?nèi)カ@取,或者使用brpop等待生產(chǎn)者添加項(xiàng)到列表中。
在 Redis 中列表 list 采用的存儲(chǔ)結(jié)構(gòu)是雙向鏈表,由此可見(jiàn)其隨機(jī)定位性能較差,比較適合首位插入刪除。像 Java 中的數(shù)組一樣,Redis 中的列表支持通過(guò)下標(biāo)訪問(wèn),不同的是 Redis 還為列表提供了一種負(fù)下標(biāo),-1 表示倒數(shù)一個(gè)元素,-2 表示倒數(shù)第二個(gè)數(shù),依此類推。綜合列表首尾增刪性能優(yōu)異的特點(diǎn),通常我們使用 rpush/rpop/lpush/lpop 四條指令將列表作為隊(duì)列來(lái)使用。
List 類型數(shù)據(jù)結(jié)構(gòu)
如上圖所示,在列表元素較少的情況下會(huì)使用一塊連續(xù)的內(nèi)存存儲(chǔ),這個(gè)結(jié)構(gòu)是 ziplist,也即是壓縮列表。它將所有的元素緊挨著一起存儲(chǔ),分配的是一塊連續(xù)的內(nèi)存。當(dāng)數(shù)據(jù)量比較多的時(shí)候才會(huì)改成 quicklist。因?yàn)?strong>普通的鏈表需要的附加指針空間太大,會(huì)比較浪費(fèi)空間。比如這個(gè)列表里存的只是 int 類型的數(shù)據(jù),結(jié)構(gòu)上還需要兩個(gè)額外的指針 prev 和 next。所以 Redis 將鏈表和 ziplist 結(jié)合起來(lái)組成了 quicklist。也就是將多個(gè) ziplist 使用雙向指針串起來(lái)使用。這樣既滿足了快速的插入刪除性能,又不會(huì)出現(xiàn)太大的空間冗余。
哈希表(hash)
- redis的哈希值是字符串字段和字符串之間的映射,是表示對(duì)象的完美數(shù)據(jù)類型。
- 哈希中的字段數(shù)量沒(méi)有限制,所以可以在你的應(yīng)用程序以不同的方式來(lái)使用哈希。
hash 與 Java 中的 HashMap 差不多,實(shí)現(xiàn)上采用二維結(jié)構(gòu),第一維是數(shù)組,第二維是鏈表。hash 的 key 與 value 都存儲(chǔ)在鏈表中,而數(shù)組中存儲(chǔ)的則是各個(gè)鏈表的表頭。在檢索時(shí),首先計(jì)算 key 的 hashcode,然后通過(guò) hashcode 定位到鏈表的表頭,再遍歷鏈表得到 value 值。可能您比較好奇為啥要用鏈表來(lái)存儲(chǔ) key 和 value,直接用 key 和 value 一對(duì)一存儲(chǔ)不就可以了嗎?其實(shí)是因?yàn)橛行r(shí)候我們無(wú)法保證 hashcode 值的唯一,若兩個(gè)不同的 key 產(chǎn)生了相同的 hashcode,我們需要一個(gè)鏈表在存儲(chǔ)兩對(duì)鍵值對(duì),這就是所謂的 hash 碰撞。
集合(set)
- redis集合是無(wú)序的字符串集合,集合中的值是唯一的,無(wú)序的。可以對(duì)集合執(zhí)行很多操作,例如,測(cè)試元素是否存在,對(duì)多個(gè)集合執(zhí)行交集、并集和差集等等。
- 我們通常可以用集合存儲(chǔ)一些無(wú)關(guān)順序的,表達(dá)對(duì)象間關(guān)系的數(shù)據(jù),例如用戶的角色,可以用sismember很容易就判斷用戶是否擁有某個(gè)角色。
- 在一些用到隨機(jī)值的場(chǎng)合是非常適合的(抽獎(jiǎng)),可以用 srandmember/spop 獲取/彈出一個(gè)隨機(jī)元素。
同時(shí),使用@EnableCaching開(kāi)啟聲明式緩存支持,這樣就可以使用基于注解的緩存技術(shù)。注解緩存是一個(gè)對(duì)緩存使用的抽象,通過(guò)在代碼中添加下面的一些注解,達(dá)到緩存的效果。- @Cacheable:在方法執(zhí)行前Spring先查看緩存中是否有數(shù)據(jù),如果有數(shù)據(jù),則直接返回緩存數(shù)據(jù);沒(méi)有則調(diào)用方法并將方法返回值放進(jìn)緩存。(查)
- @CachePut:將方法的返回值放到緩存中。(增/改)
- @CacheEvict:刪除緩存中的數(shù)據(jù)。(刪)
熟悉 Java 的同學(xué)應(yīng)該知道 HashSet 的內(nèi)部實(shí)現(xiàn)使用的是 HashMap,只不過(guò)所有的 value 都指向同一個(gè)對(duì)象。Redis 的 Set 結(jié)構(gòu)也是一樣,它的內(nèi)部也使用 Hash 結(jié)構(gòu),所有的 value 都指向同一個(gè)內(nèi)部值。
有序集合(sorted set)
- 有序集合由唯一的,不重復(fù)的字符串元素組成。有序集合中的每個(gè)元素都關(guān)聯(lián)了一個(gè)浮點(diǎn)值,稱為分?jǐn)?shù)。可以把有序集合zset看成hash和集合的混合體,分?jǐn)?shù)即為hash的key。
- 有序集合中的元素是按序存儲(chǔ)的,不是請(qǐng)求時(shí)才排序的。
有時(shí)也被稱作 ZSet,是 Redis 中一個(gè)比較特別的數(shù)據(jù)結(jié)構(gòu),在有序集合中我們會(huì)給每個(gè)元素賦予一個(gè)權(quán)重,其內(nèi)部元素會(huì)按照權(quán)重進(jìn)行排序,我們可以通過(guò)命令查詢某個(gè)范圍權(quán)重內(nèi)的元素,這個(gè)特性在我們做一個(gè)排行榜的功能時(shí)可以說(shuō)非常實(shí)用了。其底層的實(shí)現(xiàn)使用了兩個(gè)數(shù)據(jù)結(jié)構(gòu), hash 和跳躍列表,hash 的作用就是關(guān)聯(lián)元素 value 和權(quán)重 score,保障元素 value 的唯一性,可以通過(guò)元素 value 找到相應(yīng)的 score 值。跳躍列表的目的在于給元素 value 排序,根據(jù) score 的范圍獲取元素列表。
在 Spring Boot 項(xiàng)目中使用 Redis
spring-data-redis針對(duì)jedis提供了如下功能:
1. 連接池自動(dòng)管理,提供了一個(gè)高度封裝的“RedisTemplate”類
2. 針對(duì)jedis客戶端中大量api進(jìn)行了歸類封裝,將同一類型操作封裝為operation接口
ValueOperations:簡(jiǎn)單K-V操作
SetOperations:set類型數(shù)據(jù)操作
ZSetOperations:zset類型數(shù)據(jù)操作
HashOperations:針對(duì)map類型的數(shù)據(jù)操作
ListOperations:針對(duì)list類型的數(shù)據(jù)操作
3. 提供了對(duì)key的“bound”(綁定)便捷化操作API,可以通過(guò)bound封裝指定的key,然后進(jìn)行一系列的操作而無(wú)須“顯式”的再次指定Key,即BoundKeyOperations:
BoundValueOperations
BoundSetOperations
BoundListOperations
BoundSetOperations
BoundHashOperations
4. 將事務(wù)操作封裝,有容器控制。
5. 針對(duì)數(shù)據(jù)的“序列化/反序列化”,提供了多種可選擇策略(RedisSerializer)
JdkSerializationRedisSerializer:POJO對(duì)象的存取場(chǎng)景,使用JDK本身序列化機(jī)制,將pojo類通過(guò)ObjectInputStream/ObjectOutputStream進(jìn)行序列化操作,最終redis-server中將存儲(chǔ)字節(jié)序列。是目前最常用的序列化策略。
StringRedisSerializer:Key或者value為字符串的場(chǎng)景,根據(jù)指定的charset對(duì)數(shù)據(jù)的字節(jié)序列編碼成string,是“new String(bytes, charset)”和“string.getBytes(charset)”的直接封裝。是最輕量級(jí)和高效的策略。
JacksonJsonRedisSerializer:jackson-json工具提供了javabean與json之間的轉(zhuǎn)換能力,可以將pojo實(shí)例序列化成json格式存儲(chǔ)在redis中,也可以將json格式的數(shù)據(jù)轉(zhuǎn)換成pojo實(shí)例。因?yàn)閖ackson工具在序列化和反序列化時(shí),需要明確指定Class類型,因此此策略封裝起來(lái)稍微復(fù)雜。【需要jackson-mapper-asl工具支持】
OxmSerializer:提供了將javabean與xml之間的轉(zhuǎn)換能力,目前可用的三方支持包括jaxb,apache-xmlbeans;redis存儲(chǔ)的數(shù)據(jù)將是xml工具。不過(guò)使用此策略,編程將會(huì)有些難度,而且效率最低;不建議使用?!拘枰猻pring-oxm模塊的支持】
如果你的數(shù)據(jù)需要被第三方工具解析,那么數(shù)據(jù)應(yīng)該使用StringRedisSerializer而不是JdkSerializationRedisSerializer。
如果使用的是默認(rèn)的JdkSerializationRedisSerializer,注意一定要讓緩存的對(duì)象實(shí)現(xiàn)序列化接口用于序列化 。
關(guān)于key的設(shè)計(jì)
key的存活時(shí)間:
無(wú)論什么時(shí)候,只要有可能就利用key超時(shí)的優(yōu)勢(shì)。
關(guān)系型數(shù)據(jù)庫(kù)的redis
1: 把表名轉(zhuǎn)換為key前綴 如, tag:
2: 第2段放置用于區(qū)分區(qū)key的字段--對(duì)應(yīng)mysql中的主鍵的列名,如userid
3: 第3段放置主鍵值,如2,3,4...., a , b ,c
4: 第4段,寫要存儲(chǔ)的列名
例:user:userid:9:username
使用前配置
添加 Redis 依賴
<!-- 配置使用 redis 啟動(dòng)器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--springboot 集成 junit 起步依賴-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.1.6.RELEASE</version>
<scope>test</scope>
</dependency>
Spring Boot 中配置 Redis: application.properties
spring.redis.host=127.0.0.1
spring.redis.port=6379
# Redis 數(shù)據(jù)庫(kù)索引(默認(rèn)為 0)
spring.redis.database=0
# Redis 服務(wù)器連接端口
# Redis 服務(wù)器連接密碼(默認(rèn)為空)
spring.redis.password=123456
#連接池最大連接數(shù)(使用負(fù)值表示沒(méi)有限制)
spring.redis.jedis.pool.max-active=8
# 連接池最大阻塞等待時(shí)間(使用負(fù)值表示沒(méi)有限制)
spring.redis.jedis.pool.max-wait=-1
# 連接池中的最大空閑連接
spring.redis.jedis.pool.max-idle=8
# 連接池中的最小空閑連接
spring.redis.jedis.pool.min-idle=0
# 連接超時(shí)時(shí)間(毫秒) 如果連接超時(shí)時(shí)間不設(shè)置,這要注釋掉配置而不能=0,否則會(huì)報(bào)連接超時(shí)錯(cuò)誤:Command timed out after no timeout,,有超時(shí)時(shí)間最后設(shè)置為200以上
spring.redis.timeout=300
RedisTemplate 的配置
@Configuration
public class RedisConfig {
/**
* 默認(rèn)是JDK的序列化策略,這里配置redisTemplate采用的是Jackson2JsonRedisSerializer的序列化策略
* @param redisConnectionFactory
* @return
*/
@Bean
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
//使用Jackson2JsonRedisSerializer來(lái)序列化和反序列化redis的value值(默認(rèn)使用JDK的序列化方式)
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
// 指定要序列化的域,field,get和set,以及修飾符范圍,ANY是都有包括private和public
om.setVisibility(PropertyAccessor.ALL,JsonAutoDetect.Visibility.ANY);
// 指定序列化輸入的類型,類必須是非final修飾的,final修飾的類,比如String,Integer等會(huì)拋出異常
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// 配置連接工廠
redisTemplate.setConnectionFactory(redisConnectionFactory);
//使用StringRedisSerializer來(lái)序列化和反序列化redis的key值
//redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setKeySerializer(jackson2JsonRedisSerializer);
// 值采用json序列化
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
/***
* stringRedisTemplate默認(rèn)采用的是String的序列化策略
* @param redisConnectionFactory
* @return
*/
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory){
StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
stringRedisTemplate.setConnectionFactory(redisConnectionFactory);
return stringRedisTemplate;
}
}
另外一直版本:key采用String序列化,value使用jackson。
@Configuration
public class RedisConfig {
@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
template.setConnectionFactory(factory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
RedisTemplate
Spring Boot 的 spring-boot-starter-data-redis 為 Redis 的相關(guān)操作提供了一個(gè)高度封裝的 RedisTemplate 類,而且對(duì)每種類型的數(shù)據(jù)結(jié)構(gòu)都進(jìn)行了歸類,將同一類型操作封裝為 operation 接口。RedisTemplate 對(duì)五種數(shù)據(jù)結(jié)構(gòu)分別定義了操作,如下所示:
- 操作字符串:
redisTemplate.opsForValue() - 操作 Hash:
redisTemplate.opsForHash() - 操作 List:
redisTemplate.opsForList() - 操作 Set:
redisTemplate.opsForSet() - 操作 ZSet:
redisTemplate.opsForZSet()
但是對(duì)于 string 類型的數(shù)據(jù),Spring Boot 還專門提供了 StringRedisTemplate 類,而且官方也建議使用該類來(lái)操作 String 類型的數(shù)據(jù)。那么它和 RedisTemplate 又有啥區(qū)別呢?
-
RedisTemplate是一個(gè)泛型類,而StringRedisTemplate不是,后者只能對(duì)鍵和值都為 String 類型的數(shù)據(jù)進(jìn)行操作,而前者則可以操作任何類型。 - 兩者的數(shù)據(jù)是不共通的,
StringRedisTemplate只能管理StringRedisTemplate里面的數(shù)據(jù),RedisTemplate只能管理RedisTemplate中 的數(shù)據(jù)。
實(shí)踐
//用于指定junit運(yùn)行環(huán)境,是junit提供給其他框架測(cè)試環(huán)境接口擴(kuò)展,為了便于使用spring的依賴注入
@RunWith(SpringJUnit4ClassRunner.class)
//用于加載ApplicationContext,啟動(dòng)spring容器
@SpringBootTest(classes = MyBootApplication.class)
public class RedisTemplateTest {
@Autowired
private RedisTemplate redisTemplate;
//因?yàn)閟pring自動(dòng)注入管理了bean容器,直接用@Autowired取即可
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 操作字符串
*/
@Test
public void testString() {
//設(shè)置值
stringRedisTemplate.opsForValue().set("String", "Mao");
//獲取值
String string = stringRedisTemplate.opsForValue().get("String");
//設(shè)置值且設(shè)置超時(shí)時(shí)間
stringRedisTemplate.opsForValue().set("Middle", "Yu", 3, TimeUnit.MINUTES);
String middle = stringRedisTemplate.opsForValue().get("Middle");
System.out.println(middle);
//刪除數(shù)據(jù)
Boolean isDelete = stringRedisTemplate.delete("String");
System.out.println(isDelete ? "Yes" : "No");
}
/**
* 操作列表
*/
@Test
public void testList() {
ListOperations listOp = redisTemplate.opsForList();
//往 List 左側(cè)插入一個(gè)元素
listOp.leftPush("namelist", "mike");
listOp.leftPush("namelist", "kim");
//往 List 右側(cè)插入一個(gè)元素
listOp.rightPush("namelist", "jimmy");
listOp.rightPush("namelist", "chuck");
//List 大小
Long size = listOp.size("namelist");
//遍歷整個(gè)List
List namelist1 = listOp.range("namelist", 0, size);
//遍歷整個(gè)List,-1表示倒數(shù)第一個(gè)即最后一個(gè)
List namelist = listOp.range("namelist", 0, -1);
System.out.println(JSON.toJSONString(namelist));
//從 List 左側(cè)取出第一個(gè)元素,并移除
Object name1 = listOp.leftPop("namelist", 200, TimeUnit.MILLISECONDS);
System.out.println("is kim:" + name1.equals("kim"));
//從 List 右側(cè)取出第一個(gè)元素,并移除
Object name2 = listOp.rightPop("namelist");
System.out.println("is chuck:" + name2.equals("chuck"));
}
/**
* 操作 Hash
*/
@Test
public void testHash() {
//添加泛型方便操作和返回想要的具體類型
HashOperations<String, String, Integer> hashOp = redisTemplate.opsForHash();
//Hash 中新增元素。
hashOp.put("score", "Mike", 10);
hashOp.put("score", "Jimmy", 9);
hashOp.put("score", "Kim", 8);
//判斷指定 key 對(duì)應(yīng)的 Hash 中是否存在指定的 map 鍵
Assert.assertTrue(hashOp.hasKey("score", "Kim"));
//獲取指定 key 對(duì)應(yīng)的 Hash 中指定鍵的值
Integer kim = hashOp.get("score", "Kim");
System.out.println("kim score:" + kim);
//獲取hash表所有的key集合
Set<String> name = hashOp.keys("score");
System.out.println(JSON.toJSONString(name));
//獲取hash表所有的values集合
List<Integer> score = hashOp.values("score");
System.out.println(JSON.toJSONString(score));
//獲取"score"對(duì)應(yīng)的hash表Map
Map<String, Integer> map = hashOp.entries("score");
System.out.println(JSON.toJSONString(map));
//刪除指定 key 對(duì)應(yīng) Hash 中指定鍵的鍵值對(duì)
hashOp.delete("score", "Mike");
//如果要?jiǎng)h除整個(gè)hash表,要用redisTemplate.delete("score")方法,否則報(bào)錯(cuò):Fields must not be empty
//hashOp.delete("score");
//刪除整個(gè)hash表
redisTemplate.delete("score");
Map<String, Integer> map1 = hashOp.entries("score");
System.out.println(JSON.toJSONString(map1));
}
/**
* 操作集合
*/
@Test
public void testSet() {
SetOperations<String, String> setOp = redisTemplate.opsForSet();
//向集合中添加元素,set元素具有唯一性
setOp.add("city", "quanzhou", "newyork", "paris", "hongkong", "hongkong");
Long size = setOp.size("city");
System.out.println("city size:" + size);
//獲取集合中的元素
Set city = setOp.members("city");
System.out.println(JSON.toJSONString(city));
//移除集合中的元素,可以一個(gè)或多個(gè)
setOp.remove("city", "paris");
//判斷是否是集合中的元素
Boolean isMember = setOp.isMember("city", "paris");
System.out.println("paris is in city:" + isMember);
//移除并返回集合中的一個(gè)隨機(jī)元素
String city1 = setOp.pop("city");
System.out.println(city1);
}
/**
* 操作有序集合
*/
@Test
public void testZSet() {
ZSetOperations<String, String> zSetOp = redisTemplate.opsForZSet();
zSetOp.add("zcity", "beijing", 100);
zSetOp.add("zcity", "shanghai", 95);
zSetOp.add("zcity", "guangzhou", 75);
zSetOp.add("zcity", "shenzhen", 85);
//獲取變量指定區(qū)間的元素。0, -1表示全部
Set<String> zcity = zSetOp.range("zcity", 0, -1);
System.out.println(JSON.toJSONString(zcity));
//通過(guò)分?jǐn)?shù)返回有序集合指定區(qū)間內(nèi)的成員,其中有序集成員按分?jǐn)?shù)值遞增(從小到大)順序排列
Set<String> byScore = zSetOp.rangeByScore("zcity", 85, 100);
System.out.println(JSON.toJSONString(byScore));
//獲取有序集合的成員數(shù)
Long aLong = zSetOp.zCard("zcity");
System.out.println("zcity size: " + aLong);
ZSetOperations<String, Integer> zSetOp1 = redisTemplate.opsForZSet();
zSetOp1.add("board", 1, 100);
zSetOp1.add("board", 2, 100);
zSetOp1.add("board", 3, 100);
zSetOp1.add("board", 4, 100);
Set<Integer> board = zSetOp1.range("board", 0, -1);
System.out.println(JSON.toJSONString(board));
RedisZSetCommands.Range range = new RedisZSetCommands.Range();
//less than
range.lt("3");
RedisZSetCommands.Limit limit = new RedisZSetCommands.Limit();
limit.count(1);
limit.offset(1);
//用于獲取滿足非 score 的排序取值。這個(gè)排序只有在有相同分?jǐn)?shù)的情況下才能使用,如果有不同的分?jǐn)?shù)則返回值不確定。
//rangeByLex應(yīng)用在數(shù)值上比較
Set<Integer> set = zSetOp1.rangeByLex("board", range);
System.out.println(JSON.toJSONString(set));
//用于獲取滿足非 score 的設(shè)置下標(biāo)開(kāi)始的長(zhǎng)度排序取值。
Set<Integer> setlmt = zSetOp1.rangeByLex("board", range, limit);
System.out.println(JSON.toJSONString(setlmt));
}
/**
* 分布式鎖
*/
@Test
public void testLock() {
String value = UUID.randomUUID().toString();
Boolean lock = lock("buy", value,120L, TimeUnit.SECONDS);
if (!lock) {
System.out.println("can't lock buy");
}
try {
Thread.sleep(6);
} catch (InterruptedException e) {
e.printStackTrace();
}
Boolean unLock = unLock("buy",value);
if (!unLock) {
System.out.println("can't unlock buy");
}
}
public Boolean lock(String key,String value, Long timeout, TimeUnit timeUnit) {
Boolean lockStat = stringRedisTemplate.execute((RedisCallback<Boolean>) connection ->
connection.set(key.getBytes(Charset.forName("UTF-8")), value.getBytes(Charset.forName("UTF-8")),
Expiration.from(timeout, timeUnit), RedisStringCommands.SetOption.SET_IF_ABSENT));
return lockStat;
}
public Boolean unLock(String key,String value) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
boolean unLockStat = stringRedisTemplate.execute((RedisCallback<Boolean>) connection ->
connection.eval(script.getBytes(), ReturnType.BOOLEAN, 1,
key.getBytes(Charset.forName("UTF-8")), value.getBytes(Charset.forName("UTF-8"))));
return unLockStat;
}
@Test
public void boundTest(){
BoundListOperations bound = redisTemplate.boundListOps("bound");
bound.leftPush("haha");
bound.rightPush("hehe");
List list = bound.range(0, -1);
System.out.println(JSON.toJSONString(list));
}
}
報(bào)錯(cuò)1:
Caused by: io.lettuce.core.RedisCommandExecutionException: ERR Client sent AUTH, but no password is set
at io.lettuce.core.ExceptionFactory.createExecutionException(ExceptionFactory.java:135)
A:這是因?yàn)榉?wù)器沒(méi)有設(shè)置密碼,客戶端設(shè)置了密碼去連接,導(dǎo)致連接失敗。我們?cè)趩?dòng)服務(wù)器時(shí)通過(guò)命令(config set requirepass "123456")手動(dòng)設(shè)置了密碼,一旦Redis服務(wù)器關(guān)閉后,重新啟動(dòng)加載了Redis啟動(dòng)的配置,因?yàn)閱?dòng)配置菜單(redis-server.exe redis.windows.conf)中沒(méi)有配置密碼,所以重新啟動(dòng)的redis是沒(méi)有設(shè)置密碼的,這樣就會(huì)報(bào)錯(cuò)。
解決方法:如果要設(shè)置密碼則要在Redis啟動(dòng)的配置文件中修改添加;或者如果不設(shè)置密碼則客戶端連接時(shí)application.properties不要設(shè)置密碼連接
# Redis 服務(wù)器連接密碼(默認(rèn)為空)
#spring.redis.password=123456
報(bào)錯(cuò)2:
org.springframework.dao.QueryTimeoutException: Redis command timed out; nested exception is io.lettuce.core.RedisCommandTimeoutException: Command timed out after no timeout at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:70)
A:這是因?yàn)榭蛻舳诉B接時(shí)application.properties中設(shè)置了連接超時(shí)時(shí)間為0導(dǎo)致的.
解決方法:
# 連接超時(shí)時(shí)間(毫秒) 如果連接超時(shí)時(shí)間不設(shè)置,這要注釋掉配置而不能=0,否則會(huì)報(bào)連接超時(shí)錯(cuò)誤:Command timed out after no timeout,有超時(shí)時(shí)間最后設(shè)置為200以上
spring.redis.timeout=300
org.springframework.data.redis.RedisSystemException: Unknown redis exception; nested exception is java.lang.IllegalArgumentException: Fields must not be empty at org.springframework.data.redis.FallbackExceptionTranslationStrategy.getFallback(FallbackExceptionTranslationStrategy.java:53)
A:如果要?jiǎng)h除整個(gè)hash表,要用redisTemplate.delete("score")方法,否則報(bào)錯(cuò):Fields must not be empty
解決方法:使用redisTemplate方法,HashOperations的delete是用于刪除hash表字段用
HashOperations<String,String,Integer> hashOp = redisTemplate.opsForHash();
//hashOp.delete("score");
redisTemplate.delete("score");
關(guān)于 Redis 的幾個(gè)經(jīng)典問(wèn)題
緩存與數(shù)據(jù)庫(kù)一致性問(wèn)題
對(duì)于既有數(shù)據(jù)庫(kù)操作又有緩存操作的接口,一般分為兩種執(zhí)行順序。
- 先操作數(shù)據(jù)庫(kù),再操作緩存。這種情況下如果數(shù)據(jù)庫(kù)操作成功,緩存操作失敗就會(huì)導(dǎo)致緩存和數(shù)據(jù)庫(kù)不一致。
- 第二種情況就是先操作緩存再操作數(shù)據(jù)庫(kù),這種情況下如果緩存操作成功,數(shù)據(jù)庫(kù)操作失敗也會(huì)導(dǎo)致數(shù)據(jù)庫(kù)和緩存不一致。
大部分情況下,我們的緩存理論上都是需要可以從數(shù)據(jù)庫(kù)恢復(fù)出來(lái)的,所以基本上采取第一種順序都是不會(huì)有問(wèn)題的。針對(duì)那些必須保證數(shù)據(jù)庫(kù)和緩存一致的情況,通常是不建議使用緩存的。
緩存穿透問(wèn)題
緩存擊穿表示惡意用戶頻繁的模擬請(qǐng)求緩存中不存在的數(shù)據(jù),以致這些請(qǐng)求短時(shí)間內(nèi)直接落在了數(shù)據(jù)庫(kù)上,導(dǎo)致數(shù)據(jù)庫(kù)性能急劇下降,最終影響服務(wù)整體的性能。這個(gè)在實(shí)際項(xiàng)目很容易遇到,如搶購(gòu)活動(dòng)、秒殺活動(dòng)的接口 API 被大量的惡意用戶刷,導(dǎo)致短時(shí)間內(nèi)數(shù)據(jù)庫(kù)宕機(jī)。對(duì)于緩存擊穿的問(wèn)題,有以下幾種解決方案,這里只做簡(jiǎn)要說(shuō)明。
- 使用互斥鎖排隊(duì)。當(dāng)從緩存中獲取數(shù)據(jù)失敗時(shí),給當(dāng)前接口加上鎖,從數(shù)據(jù)庫(kù)中加載完數(shù)據(jù)并寫入后再釋放鎖。若其它線程獲取鎖失敗,則等待一段時(shí)間后重試。(數(shù)據(jù)庫(kù)取數(shù)據(jù)時(shí)加鎖)
- 使用布隆過(guò)濾器。將所有可能存在的數(shù)據(jù)緩存放到布隆過(guò)濾器中,當(dāng)黑客訪問(wèn)不存在的緩存時(shí)迅速返回避免緩存及 DB 掛掉。
緩存雪崩問(wèn)題
在短時(shí)間內(nèi)有大量緩存失效,如果這期間有大量的請(qǐng)求發(fā)生同樣也有可能導(dǎo)致數(shù)據(jù)庫(kù)發(fā)生宕機(jī)。在 Redis 機(jī)群的數(shù)據(jù)分布算法上如果使用的是傳統(tǒng)的 hash 取模算法,在增加或者移除 Redis 節(jié)點(diǎn)的時(shí)候就會(huì)出現(xiàn)大量的緩存臨時(shí)失效的情形。
- 像解決緩存穿透一樣加鎖排隊(duì)。
- 建立備份緩存,緩存 A 和緩存 B,A 設(shè)置超時(shí)時(shí)間,B 不設(shè)值超時(shí)時(shí)間,先從 A 讀緩存,A 沒(méi)有讀 B,并且更新 A 緩存和 B 緩存。
- 計(jì)算數(shù)據(jù)緩存節(jié)點(diǎn)的時(shí)候采用一致性 hash 算法,這樣在節(jié)點(diǎn)數(shù)量發(fā)生改變時(shí)不會(huì)存在大量的緩存數(shù)據(jù)需要遷移的情況發(fā)生。
描述
brpop : block right pop
BRPOP key1 [key2 ] timeout
移出并獲取列表的最后一個(gè)元素, 如果列表沒(méi)有元素會(huì)阻塞列表直到等待超時(shí)或發(fā)現(xiàn)可彈出元素為止。
問(wèn)題
Q:布隆過(guò)濾器是什么?有什么用?怎么使用?
Q:我們?cè)诰帉慠edisConfig配置類時(shí)是否繼承于CachingConfigurerSupport類有什么區(qū)別?為什么有的繼承了有的不選擇繼承,是繼承了的話可以結(jié)合Springboot的@EnableCaching的注解開(kāi)啟緩存么?以便使用如下注解自動(dòng)開(kāi)啟緩存么?那在項(xiàng)目中是使用好還是不使用好?
開(kāi)啟緩存后注解使用參考:
注解緩存的使用
@Cacheable:在方法執(zhí)行前Spring先查看緩存中是否有數(shù)據(jù),如果有數(shù)據(jù),則直接返回緩存數(shù)據(jù);沒(méi)有則調(diào)用方法并將方法返回值放進(jìn)緩存。
@CachePut:將方法的返回值放到緩存中。
@CacheEvict:刪除緩存中的數(shù)據(jù)。
Q:Spring Boot中混合使用StringRedisTemplate和RedisTemplate的坑——存儲(chǔ)到Redis的數(shù)據(jù)取不到值。
A:因?yàn)樗瑫r(shí)使用了StringRedisTemplate和RedisTemplate在Redis中存儲(chǔ)和讀取數(shù)據(jù)。它們最重要的一個(gè)區(qū)別就是默認(rèn)采用的序列化方式不同。StringRedisTemplate采用的是RedisSerializer.string()來(lái)序列化Redis中存儲(chǔ)數(shù)據(jù)的Key ;RedisTemplate使用的序列化類為defaultSerializer,默認(rèn)情況下為JdkSerializationRedisSerializer。如果未指定Key的序列化類,keySerializer與defaultSerializer采用相同的序列化類。
解決方法:需要指定統(tǒng)一的Key的序列化處理類,比如在RedisTemplate序列化時(shí)指定與StringRedisTemplate相同的類。 也就是對(duì)他們的序列化都采用StringRedisSerializer。
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
Q:Redis的brpop命令對(duì)應(yīng)RedisTemplate中的什么方法?
其實(shí)可以寫個(gè)死循環(huán)調(diào)用rightPop(K key)方法,當(dāng)獲取到數(shù)據(jù)時(shí)才跳出循環(huán)即可。當(dāng)然要注意接口超時(shí)的情況。所以直接使用超時(shí)方法就是阻塞調(diào)用bRPop。
@Override
public V rightPop(K key, long timeout, TimeUnit unit) {
int tm = (int) TimeoutUtils.toSeconds(timeout, unit);
return execute(new ValueDeserializingRedisCallback(key) {
@Override
protected byte[] inRedis(byte[] rawKey, RedisConnection connection) {
List<byte[]> bRPop = connection.bRPop(tm, rawKey);
return (CollectionUtils.isEmpty(bRPop) ? null : bRPop.get(1));
}
}, true);
}
Q:單元測(cè)試時(shí)@RunWith(SpringJUnit4ClassRunner.class)@SpringBootTest(classes = MyBootApplication.class)兩個(gè)注解的作用?
A:如下。
1)@RunWith:用于指定junit運(yùn)行環(huán)境,是junit提供給其他框架測(cè)試環(huán)境接口擴(kuò)展,為了便于使用spring的依賴注入,spring提供了org.springframework.test.context.junit4.SpringJUnit4ClassRunner作為Junit測(cè)試環(huán)境。
2)@ContextConfiguration({"classes=Congfig.clsss",classpath:applicationContext.xml"}) 這里可以用classes來(lái)直接導(dǎo)入同包下寫的配置類。或者導(dǎo)入配置文件。
3)@SpringBootTest替代了spring-test中的@ContextConfiguration注解,目的是加載ApplicationContext,啟動(dòng)spring容器。因?yàn)锧SpringBootTest包含了讀取配置文件的@ContextConfiguration注解。
springboot使用單元測(cè)試步驟:
1、引入依賴
<!--springboot 集成 junit 起步依賴-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.1.6.RELEASE</version>
<scope>test</scope>
</dependency>
2、編寫測(cè)試類,注意加注解@RunWith(SpringJUnit4ClassRunner.class)@SpringBootTest(classes = MyBootApplication.class),模板如下:
//用于指定junit運(yùn)行環(huán)境,是junit提供給其他框架測(cè)試環(huán)境接口擴(kuò)展,為了便于使用spring的依賴注入
@RunWith(SpringJUnit4ClassRunner.class)
//用于加載ApplicationContext,啟動(dòng)spring容器
@SpringBootTest(classes = MyBootApplication.class)
public class RedisTemplateTest {
@Autowired
private RedisTemplate redisTemplate;
//因?yàn)閟pring自動(dòng)注入管理了bean容器,直接用@Autowired取即可
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
public void testString(){
//設(shè)置值
stringRedisTemplate.opsForValue().set("String","Mao");
}
}
Q:緩存并發(fā)問(wèn)題,這里的并發(fā)指的是多個(gè) Redis 的客戶端同時(shí) set 值引起的并發(fā)問(wèn)題。比較有效的解決方案就是把 set 操作放在隊(duì)列中使其串行化,必須得一個(gè)一個(gè)執(zhí)行。我的疑問(wèn)是,redis本身就是單線程串行執(zhí)行的,怎么會(huì)有并發(fā)問(wèn)題呢?
A:我們說(shuō)的緩存并發(fā)指的是多個(gè)Redis客戶端同時(shí)SET Key時(shí)會(huì)引起并發(fā)問(wèn)題。我們知道,Redis是單線程的,在多個(gè)Client并發(fā)操作時(shí),秉承“先發(fā)起先執(zhí)行”的原則,其它的處于阻塞狀態(tài)。
緩存并發(fā)問(wèn)題其實(shí)主要指的是讀取數(shù)據(jù)庫(kù)數(shù)據(jù)的并發(fā)操作問(wèn)題。
常見(jiàn)緩存并發(fā)有兩種場(chǎng)景:
- 緩存過(guò)期后會(huì)從后端數(shù)據(jù)庫(kù)查詢數(shù)據(jù)然后存入Redis緩存,但在高并發(fā)情況下可能在還沒(méi)來(lái)得及將庫(kù)中查出來(lái)的數(shù)據(jù)存入Redis時(shí),其它Client又從數(shù)據(jù)庫(kù)里查詢數(shù)據(jù)再存入Redis了。這樣一來(lái)會(huì)造成多個(gè)請(qǐng)求并發(fā)的從數(shù)據(jù)庫(kù)獲取數(shù)據(jù),對(duì)后端數(shù)據(jù)庫(kù)會(huì)造成壓力。
- 在高并發(fā)場(chǎng)景下,某個(gè)Key正在更新時(shí),可能有很多Client在獲取此Key的數(shù)據(jù),這樣會(huì)導(dǎo)致“臟數(shù)據(jù)”。
如何解決緩存并發(fā)問(wèn)題呢?
1、加鎖。我們常借助“鎖”來(lái)實(shí)現(xiàn),具體實(shí)現(xiàn)邏輯為:
在更新緩存或者讀取過(guò)期緩存的情況下,我們先獲取“鎖”,當(dāng)完成了緩存數(shù)據(jù)的更新后再釋放鎖,這樣一來(lái),其它的請(qǐng)求需要在釋放鎖之后執(zhí)行,會(huì)犧牲一點(diǎn)時(shí)間。
2、異步隊(duì)列串行執(zhí)行。把 set 操作放在隊(duì)列中使其串行化,必須得一個(gè)一個(gè)執(zhí)行。如通過(guò)消息中間件異步執(zhí)行。
3、使用類似SQL的樂(lè)觀鎖機(jī)制 。解決途徑是在并發(fā)寫入Redis緩存時(shí),用要寫入數(shù)據(jù)的版本號(hào)和時(shí)間戳與Redis中的數(shù)據(jù)進(jìn)行對(duì)比,如果寫入的數(shù)據(jù)時(shí)間戳或者版本號(hào) 比Redis 高,則寫入Redis中;否則就不寫入。