Redis 簡介
Redis 是一個開源(BSD 許可)、內存存儲的數據結構服務器,可用作數據庫,高速緩存和消息隊列代理。它支持字符串、哈希表、列表、集合、有序集合等數據類型。內置復制、Lua 腳本、LRU 收回、事務以及不同級別磁盤持久化功能,同時通過 Redis Sentinel 提供高可用,通過 Redis Cluster 提供自動分區。
Redis 使用場景
微服務以及分布式被廣泛使用后,Redis 的使用場景就越來越多了,這里我羅列了主要的幾種場景。
- 分布式緩存:在分布式的系統架構中,將緩存存儲在內存中顯然不當,因為緩存需要與其他機器共享,這時 Redis 便挺身而出了,緩存也是 Redis 使用最多的場景。
- 分布式鎖:在高並發的情況下,我們需要一個鎖來防止並發帶來的臟數據,Java 自帶的鎖機制顯然對進程間的並發並不好使,此時可以利用 Redis 單線程的特性來實現我們的分布式鎖,如何實現,可以參考這篇文章。
- Session 存儲/共享:Redis 可以將 Session 持久化到存儲中,這樣可以避免由於機器宕機而丟失用戶會話信息。(個人認為更重要的是因為服務集群的出現,需要一個分布式Session來作為統一的會話)
- 發布/訂閱:Redis 還有一個發布/訂閱的功能,您可以設定對某一個 key 值進行消息發布及消息訂閱,當一個 key 值上進行了消息發布后,所有訂閱它的客戶端都會收到相應的消息。這一功能最明顯的用法就是用作實時消息系統。
- 任務隊列:Redis 的
lpush+brpop
命令組合即可實現阻塞隊列,生產者客戶端使用lrpush
從列表左側插入元素,多個消費者客戶端使用brpop
命令阻塞式的”搶”列表尾部的元素,多個客戶端保證了消費的負載均衡和高可用性。 - 限速,接口訪問頻率限制:比如發送短信驗證碼的接口,通常為了防止別人惡意頻刷,會限制用戶每分鍾獲取驗證碼的頻率,例如一分鍾不能超過 5 次。
當然 Redis 的使用場景並不僅僅只有這么多,還有很多未列出的場景,如計數、排行榜等。
Redis 數據類型
前面也提到過,Redis 支持字符串、哈希表、列表、集合、有序集合五種數據類型的存儲。
字符串(string)
- string類型是二進制安全的。意思是redis的string可以包含任何數據。比如jpg圖片或者序列化的對象 。
- string類型是Redis最基本的數據類型,一個鍵最大能存儲512MB。
string 這種數據結構應該是我們最為常用的。在 Redis 中 string 表示的是一個可變的字節數組,我們初始化字符串的內容、可以拿到字符串的長度,可以獲取 string 的子串,可以覆蓋 string 的子串內容,可以追加子串。
Redis 的 string 類型數據結構
如上圖所示,在 Redis 中我們初始化一個字符串時,會采用預分配冗余空間的方式來減少內存的頻繁分配,如圖 1 所示,實際分配的空間 capacity 一般要高於實際字符串長度 len。如果您看過 Java 的 ArrayList 的源碼相信會對此種模式很熟悉。
列表(list)
- redis列表是簡單的字符串列表,排序為插入的順序。列表的最大長度為2^32-1。
- redis的列表是使用鏈表實現的,這意味着,即使列表中有上百萬個元素,增加一個元素到列表的頭部或尾部的操作都是在常量的時間完成。
- 可以用列表獲取最新的內容(像帖子,微博等),用ltrim很容易就會獲取最新的內容,並移除舊的內容。
- 用列表可以實現生產者消費者模式,生產者調用lpush添加項到列表中,消費者調用rpop從列表中提取,如果沒有元素,則輪詢去獲取,或者使用brpop等待生產者添加項到列表中。
在 Redis 中列表 list 采用的存儲結構是雙向鏈表,由此可見其隨機定位性能較差,比較適合首位插入刪除。像 Java 中的數組一樣,Redis 中的列表支持通過下標訪問,不同的是 Redis 還為列表提供了一種負下標,-1
表示倒數一個元素,-2
表示倒數第二個數,依此類推。綜合列表首尾增刪性能優異的特點,通常我們使用 rpush/rpop/lpush/lpop
四條指令將列表作為隊列來使用。
List 類型數據結構
如上圖所示,在列表元素較少的情況下會使用一塊連續的內存存儲,這個結構是 ziplist,也即是壓縮列表。它將所有的元素緊挨着一起存儲,分配的是一塊連續的內存。當數據量比較多的時候才會改成 quicklist。因為普通的鏈表需要的附加指針空間太大,會比較浪費空間。比如這個列表里存的只是 int 類型的數據,結構上還需要兩個額外的指針 prev 和 next。所以 Redis 將鏈表和 ziplist 結合起來組成了 quicklist。也就是將多個 ziplist 使用雙向指針串起來使用。這樣既滿足了快速的插入刪除性能,又不會出現太大的空間冗余。
哈希表(hash)
- redis的哈希值是字符串字段和字符串之間的映射,是表示對象的完美數據類型。
- 哈希中的字段數量沒有限制,所以可以在你的應用程序以不同的方式來使用哈希。
hash 與 Java 中的 HashMap 差不多,實現上采用二維結構,第一維是數組,第二維是鏈表。hash 的 key 與 value 都存儲在鏈表中,而數組中存儲的則是各個鏈表的表頭。在檢索時,首先計算 key 的 hashcode,然后通過 hashcode 定位到鏈表的表頭,再遍歷鏈表得到 value 值。可能您比較好奇為啥要用鏈表來存儲 key 和 value,直接用 key 和 value 一對一存儲不就可以了嗎?其實是因為有些時候我們無法保證 hashcode 值的唯一,若兩個不同的 key 產生了相同的 hashcode,我們需要一個鏈表在存儲兩對鍵值對,這就是所謂的 hash 碰撞。
集合(set)
- redis集合是無序的字符串集合,集合中的值是唯一的,無序的。可以對集合執行很多操作,例如,測試元素是否存在,對多個集合執行交集、並集和差集等等。
- 我們通常可以用集合存儲一些無關順序的,表達對象間關系的數據,例如用戶的角色,可以用sismember很容易就判斷用戶是否擁有某個角色。
- 在一些用到隨機值的場合是非常適合的(抽獎),可以用 srandmember/spop 獲取/彈出一個隨機元素。
同時,使用@EnableCaching開啟聲明式緩存支持,這樣就可以使用基於注解的緩存技術。注解緩存是一個對緩存使用的抽象,通過在代碼中添加下面的一些注解,達到緩存的效果。- @Cacheable:在方法執行前Spring先查看緩存中是否有數據,如果有數據,則直接返回緩存數據;沒有則調用方法並將方法返回值放進緩存。(查)
- @CachePut:將方法的返回值放到緩存中。(增/改)
- @CacheEvict:刪除緩存中的數據。(刪)
熟悉 Java 的同學應該知道 HashSet 的內部實現使用的是 HashMap,只不過所有的 value 都指向同一個對象。Redis 的 Set 結構也是一樣,它的內部也使用 Hash 結構,所有的 value 都指向同一個內部值。
有序集合(sorted set)
- 有序集合由唯一的,不重復的字符串元素組成。有序集合中的每個元素都關聯了一個浮點值,稱為分數。可以把有序集合zset看成hash和集合的混合體,分數即為hash的key。
- 有序集合中的元素是按序存儲的,不是請求時才排序的。
有時也被稱作 ZSet,是 Redis 中一個比較特別的數據結構,在有序集合中我們會給每個元素賦予一個權重,其內部元素會按照權重進行排序,我們可以通過命令查詢某個范圍權重內的元素,這個特性在我們做一個排行榜的功能時可以說非常實用了。其底層的實現使用了兩個數據結構, hash 和跳躍列表,hash 的作用就是關聯元素 value 和權重 score,保障元素 value 的唯一性,可以通過元素 value 找到相應的 score 值。跳躍列表的目的在於給元素 value 排序,根據 score 的范圍獲取元素列表。
在 Spring Boot 項目中使用 Redis
spring-data-redis針對jedis提供了如下功能:
1. 連接池自動管理,提供了一個高度封裝的“RedisTemplate”類
2. 針對jedis客戶端中大量api進行了歸類封裝,將同一類型操作封裝為operation接口
ValueOperations:簡單K-V操作
SetOperations:set類型數據操作
ZSetOperations:zset類型數據操作
HashOperations:針對map類型的數據操作
ListOperations:針對list類型的數據操作
3. 提供了對key的“bound”(綁定)便捷化操作API,可以通過bound封裝指定的key,然后進行一系列的操作而無須“顯式”的再次指定Key,即BoundKeyOperations:
BoundValueOperations
BoundSetOperations
BoundListOperations
BoundSetOperations
BoundHashOperations
4. 將事務操作封裝,有容器控制。
5. 針對數據的“序列化/反序列化”,提供了多種可選擇策略(RedisSerializer)
JdkSerializationRedisSerializer:POJO對象的存取場景,使用JDK本身序列化機制,將pojo類通過ObjectInputStream/ObjectOutputStream進行序列化操作,最終redis-server中將存儲字節序列。是目前最常用的序列化策略。
StringRedisSerializer:Key或者value為字符串的場景,根據指定的charset對數據的字節序列編碼成string,是“new String(bytes, charset)”和“string.getBytes(charset)”的直接封裝。是最輕量級和高效的策略。
JacksonJsonRedisSerializer:jackson-json工具提供了javabean與json之間的轉換能力,可以將pojo實例序列化成json格式存儲在redis中,也可以將json格式的數據轉換成pojo實例。因為jackson工具在序列化和反序列化時,需要明確指定Class類型,因此此策略封裝起來稍微復雜。【需要jackson-mapper-asl工具支持】
OxmSerializer:提供了將javabean與xml之間的轉換能力,目前可用的三方支持包括jaxb,apache-xmlbeans;redis存儲的數據將是xml工具。不過使用此策略,編程將會有些難度,而且效率最低;不建議使用。【需要spring-oxm模塊的支持】
如果你的數據需要被第三方工具解析,那么數據應該使用StringRedisSerializer而不是JdkSerializationRedisSerializer。
如果使用的是默認的JdkSerializationRedisSerializer,注意一定要讓緩存的對象實現序列化接口用於序列化 。
關於key的設計
key的存活時間:
無論什么時候,只要有可能就利用key超時的優勢。
關系型數據庫的redis
1: 把表名轉換為key前綴 如, tag:
2: 第2段放置用於區分區key的字段--對應mysql中的主鍵的列名,如userid
3: 第3段放置主鍵值,如2,3,4...., a , b ,c
4: 第4段,寫要存儲的列名
例:user:userid:9:username
使用前配置
添加 Redis 依賴
<!-- 配置使用 redis 啟動器 -->
<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 數據庫索引(默認為 0)
spring.redis.database=0
# Redis 服務器連接端口
# Redis 服務器連接密碼(默認為空)
spring.redis.password=123456
#連接池最大連接數(使用負值表示沒有限制)
spring.redis.jedis.pool.max-active=8
# 連接池最大阻塞等待時間(使用負值表示沒有限制)
spring.redis.jedis.pool.max-wait=-1
# 連接池中的最大空閑連接
spring.redis.jedis.pool.max-idle=8
# 連接池中的最小空閑連接
spring.redis.jedis.pool.min-idle=0
# 連接超時時間(毫秒) 如果連接超時時間不設置,這要注釋掉配置而不能=0,否則會報連接超時錯誤:Command timed out after no timeout,,有超時時間最后設置為200以上
spring.redis.timeout=300
RedisTemplate 的配置
@Configuration
public class RedisConfig {
/**
* 默認是JDK的序列化策略,這里配置redisTemplate采用的是Jackson2JsonRedisSerializer的序列化策略
* @param redisConnectionFactory
* @return
*/
@Bean
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
//使用Jackson2JsonRedisSerializer來序列化和反序列化redis的value值(默認使用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等會拋出異常
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// 配置連接工廠
redisTemplate.setConnectionFactory(redisConnectionFactory);
//使用StringRedisSerializer來序列化和反序列化redis的key值
//redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setKeySerializer(jackson2JsonRedisSerializer);
// 值采用json序列化
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
/***
* stringRedisTemplate默認采用的是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 的相關操作提供了一個高度封裝的 RedisTemplate 類,而且對每種類型的數據結構都進行了歸類,將同一類型操作封裝為 operation 接口。RedisTemplate
對五種數據結構分別定義了操作,如下所示:
- 操作字符串:
redisTemplate.opsForValue()
- 操作 Hash:
redisTemplate.opsForHash()
- 操作 List:
redisTemplate.opsForList()
- 操作 Set:
redisTemplate.opsForSet()
- 操作 ZSet:
redisTemplate.opsForZSet()
但是對於 string 類型的數據,Spring Boot 還專門提供了 StringRedisTemplate
類,而且官方也建議使用該類來操作 String 類型的數據。那么它和 RedisTemplate
又有啥區別呢?
RedisTemplate
是一個泛型類,而StringRedisTemplate
不是,后者只能對鍵和值都為 String 類型的數據進行操作,而前者則可以操作任何類型。- 兩者的數據是不共通的,
StringRedisTemplate
只能管理StringRedisTemplate
里面的數據,RedisTemplate
只能管理RedisTemplate
中 的數據。
實踐
//用於指定junit運行環境,是junit提供給其他框架測試環境接口擴展,為了便於使用spring的依賴注入
@RunWith(SpringJUnit4ClassRunner.class)
//用於加載ApplicationContext,啟動spring容器
@SpringBootTest(classes = MyBootApplication.class)
public class RedisTemplateTest {
@Autowired
private RedisTemplate redisTemplate;
//因為spring自動注入管理了bean容器,直接用@Autowired取即可
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 操作字符串
*/
@Test
public void testString() {
//設置值
stringRedisTemplate.opsForValue().set("String", "Mao");
//獲取值
String string = stringRedisTemplate.opsForValue().get("String");
//設置值且設置超時時間
stringRedisTemplate.opsForValue().set("Middle", "Yu", 3, TimeUnit.MINUTES);
String middle = stringRedisTemplate.opsForValue().get("Middle");
System.out.println(middle);
//刪除數據
Boolean isDelete = stringRedisTemplate.delete("String");
System.out.println(isDelete ? "Yes" : "No");
}
/**
* 操作列表
*/
@Test
public void testList() {
ListOperations listOp = redisTemplate.opsForList();
//往 List 左側插入一個元素
listOp.leftPush("namelist", "mike");
listOp.leftPush("namelist", "kim");
//往 List 右側插入一個元素
listOp.rightPush("namelist", "jimmy");
listOp.rightPush("namelist", "chuck");
//List 大小
Long size = listOp.size("namelist");
//遍歷整個List
List namelist1 = listOp.range("namelist", 0, size);
//遍歷整個List,-1表示倒數第一個即最后一個
List namelist = listOp.range("namelist", 0, -1);
System.out.println(JSON.toJSONString(namelist));
//從 List 左側取出第一個元素,並移除
Object name1 = listOp.leftPop("namelist", 200, TimeUnit.MILLISECONDS);
System.out.println("is kim:" + name1.equals("kim"));
//從 List 右側取出第一個元素,並移除
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 對應的 Hash 中是否存在指定的 map 鍵
Assert.assertTrue(hashOp.hasKey("score", "Kim"));
//獲取指定 key 對應的 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"對應的hash表Map
Map<String, Integer> map = hashOp.entries("score");
System.out.println(JSON.toJSONString(map));
//刪除指定 key 對應 Hash 中指定鍵的鍵值對
hashOp.delete("score", "Mike");
//如果要刪除整個hash表,要用redisTemplate.delete("score")方法,否則報錯:Fields must not be empty
//hashOp.delete("score");
//刪除整個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));
//移除集合中的元素,可以一個或多個
setOp.remove("city", "paris");
//判斷是否是集合中的元素
Boolean isMember = setOp.isMember("city", "paris");
System.out.println("paris is in city:" + isMember);
//移除並返回集合中的一個隨機元素
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);
//獲取變量指定區間的元素。0, -1表示全部
Set<String> zcity = zSetOp.range("zcity", 0, -1);
System.out.println(JSON.toJSONString(zcity));
//通過分數返回有序集合指定區間內的成員,其中有序集成員按分數值遞增(從小到大)順序排列
Set<String> byScore = zSetOp.rangeByScore("zcity", 85, 100);
System.out.println(JSON.toJSONString(byScore));
//獲取有序集合的成員數
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 的排序取值。這個排序只有在有相同分數的情況下才能使用,如果有不同的分數則返回值不確定。
//rangeByLex應用在數值上比較
Set<Integer> set = zSetOp1.rangeByLex("board", range);
System.out.println(JSON.toJSONString(set));
//用於獲取滿足非 score 的設置下標開始的長度排序取值。
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));
}
}
報錯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:這是因為服務器沒有設置密碼,客戶端設置了密碼去連接,導致連接失敗。我們在啟動服務器時通過命令(config set requirepass "123456")手動設置了密碼,一旦Redis服務器關閉后,重新啟動加載了Redis啟動的配置,因為啟動配置菜單(redis-server.exe redis.windows.conf)中沒有配置密碼,所以重新啟動的redis是沒有設置密碼的,這樣就會報錯。
解決方法:如果要設置密碼則要在Redis啟動的配置文件中修改添加;或者如果不設置密碼則客戶端連接時application.properties不要設置密碼連接
# Redis 服務器連接密碼(默認為空)
#spring.redis.password=123456
報錯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:這是因為客戶端連接時application.properties中設置了連接超時時間為0導致的.
解決方法:
# 連接超時時間(毫秒) 如果連接超時時間不設置,這要注釋掉配置而不能=0,否則會報連接超時錯誤:Command timed out after no timeout,有超時時間最后設置為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:如果要刪除整個hash表,要用redisTemplate.delete("score")方法,否則報錯:Fields must not be empty
解決方法:使用redisTemplate方法,HashOperations的delete是用於刪除hash表字段用
HashOperations<String,String,Integer> hashOp = redisTemplate.opsForHash();
//hashOp.delete("score");
redisTemplate.delete("score");
關於 Redis 的幾個經典問題
緩存與數據庫一致性問題
對於既有數據庫操作又有緩存操作的接口,一般分為兩種執行順序。
- 先操作數據庫,再操作緩存。這種情況下如果數據庫操作成功,緩存操作失敗就會導致緩存和數據庫不一致。
- 第二種情況就是先操作緩存再操作數據庫,這種情況下如果緩存操作成功,數據庫操作失敗也會導致數據庫和緩存不一致。
大部分情況下,我們的緩存理論上都是需要可以從數據庫恢復出來的,所以基本上采取第一種順序都是不會有問題的。針對那些必須保證數據庫和緩存一致的情況,通常是不建議使用緩存的。
緩存穿透問題
緩存擊穿表示惡意用戶頻繁的模擬請求緩存中不存在的數據,以致這些請求短時間內直接落在了數據庫上,導致數據庫性能急劇下降,最終影響服務整體的性能。這個在實際項目很容易遇到,如搶購活動、秒殺活動的接口 API 被大量的惡意用戶刷,導致短時間內數據庫宕機。對於緩存擊穿的問題,有以下幾種解決方案,這里只做簡要說明。
- 使用互斥鎖排隊。當從緩存中獲取數據失敗時,給當前接口加上鎖,從數據庫中加載完數據並寫入后再釋放鎖。若其它線程獲取鎖失敗,則等待一段時間后重試。(數據庫取數據時加鎖)
- 使用布隆過濾器。將所有可能存在的數據緩存放到布隆過濾器中,當黑客訪問不存在的緩存時迅速返回避免緩存及 DB 掛掉。
緩存雪崩問題
在短時間內有大量緩存失效,如果這期間有大量的請求發生同樣也有可能導致數據庫發生宕機。在 Redis 機群的數據分布算法上如果使用的是傳統的 hash 取模算法,在增加或者移除 Redis 節點的時候就會出現大量的緩存臨時失效的情形。
- 像解決緩存穿透一樣加鎖排隊。
- 建立備份緩存,緩存 A 和緩存 B,A 設置超時時間,B 不設值超時時間,先從 A 讀緩存,A 沒有讀 B,並且更新 A 緩存和 B 緩存。
- 計算數據緩存節點的時候采用一致性 hash 算法,這樣在節點數量發生改變時不會存在大量的緩存數據需要遷移的情況發生。
描述
brpop : block right pop
BRPOP key1 [key2 ] timeout
移出並獲取列表的最后一個元素, 如果列表沒有元素會阻塞列表直到等待超時或發現可彈出元素為止。
問題
Q:布隆過濾器是什么?有什么用?怎么使用?
Q:我們在編寫RedisConfig配置類時是否繼承於CachingConfigurerSupport類有什么區別?為什么有的繼承了有的不選擇繼承,是繼承了的話可以結合Springboot的@EnableCaching的注解開啟緩存么?以便使用如下注解自動開啟緩存么?那在項目中是使用好還是不使用好?
開啟緩存后注解使用參考:
注解緩存的使用
@Cacheable:在方法執行前Spring先查看緩存中是否有數據,如果有數據,則直接返回緩存數據;沒有則調用方法並將方法返回值放進緩存。
@CachePut:將方法的返回值放到緩存中。
@CacheEvict:刪除緩存中的數據。
Q:Spring Boot中混合使用StringRedisTemplate和RedisTemplate的坑——存儲到Redis的數據取不到值。
A:因為他同時使用了StringRedisTemplate和RedisTemplate在Redis中存儲和讀取數據。它們最重要的一個區別就是默認采用的序列化方式不同。StringRedisTemplate采用的是RedisSerializer.string()來序列化Redis中存儲數據的Key ;RedisTemplate使用的序列化類為defaultSerializer,默認情況下為JdkSerializationRedisSerializer。如果未指定Key的序列化類,keySerializer與defaultSerializer采用相同的序列化類。
解決方法:需要指定統一的Key的序列化處理類,比如在RedisTemplate序列化時指定與StringRedisTemplate相同的類。 也就是對他們的序列化都采用StringRedisSerializer。
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
Q:Redis的brpop命令對應RedisTemplate中的什么方法?
其實可以寫個死循環調用rightPop(K key)
方法,當獲取到數據時才跳出循環即可。當然要注意接口超時的情況。所以直接使用超時方法就是阻塞調用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:單元測試時@RunWith(SpringJUnit4ClassRunner.class)@SpringBootTest(classes = MyBootApplication.class)兩個注解的作用?
A:如下。
1)@RunWith:用於指定junit運行環境,是junit提供給其他框架測試環境接口擴展,為了便於使用spring的依賴注入,spring提供了org.springframework.test.context.junit4.SpringJUnit4ClassRunner作為Junit測試環境。
2)@ContextConfiguration({"classes=Congfig.clsss",classpath:applicationContext.xml"}) 這里可以用classes來直接導入同包下寫的配置類。或者導入配置文件。
3)@SpringBootTest替代了spring-test中的@ContextConfiguration注解,目的是加載ApplicationContext,啟動spring容器。因為@SpringBootTest包含了讀取配置文件的@ContextConfiguration注解。
springboot使用單元測試步驟:
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、編寫測試類,注意加注解@RunWith(SpringJUnit4ClassRunner.class)@SpringBootTest(classes = MyBootApplication.class),模板如下:
//用於指定junit運行環境,是junit提供給其他框架測試環境接口擴展,為了便於使用spring的依賴注入
@RunWith(SpringJUnit4ClassRunner.class)
//用於加載ApplicationContext,啟動spring容器
@SpringBootTest(classes = MyBootApplication.class)
public class RedisTemplateTest {
@Autowired
private RedisTemplate redisTemplate;
//因為spring自動注入管理了bean容器,直接用@Autowired取即可
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
public void testString(){
//設置值
stringRedisTemplate.opsForValue().set("String","Mao");
}
}
Q:緩存並發問題,這里的並發指的是多個 Redis 的客戶端同時 set 值引起的並發問題。比較有效的解決方案就是把 set 操作放在隊列中使其串行化,必須得一個一個執行。我的疑問是,redis本身就是單線程串行執行的,怎么會有並發問題呢?
A:我們說的緩存並發指的是多個Redis客戶端同時SET Key時會引起並發問題。我們知道,Redis是單線程的,在多個Client並發操作時,秉承“先發起先執行”的原則,其它的處於阻塞狀態。
緩存並發問題其實主要指的是讀取數據庫數據的並發操作問題。
常見緩存並發有兩種場景:
- 緩存過期后會從后端數據庫查詢數據然后存入Redis緩存,但在高並發情況下可能在還沒來得及將庫中查出來的數據存入Redis時,其它Client又從數據庫里查詢數據再存入Redis了。這樣一來會造成多個請求並發的從數據庫獲取數據,對后端數據庫會造成壓力。
- 在高並發場景下,某個Key正在更新時,可能有很多Client在獲取此Key的數據,這樣會導致“臟數據”。
如何解決緩存並發問題呢?
1、加鎖。我們常借助“鎖”來實現,具體實現邏輯為:
在更新緩存或者讀取過期緩存的情況下,我們先獲取“鎖”,當完成了緩存數據的更新后再釋放鎖,這樣一來,其它的請求需要在釋放鎖之后執行,會犧牲一點時間。
2、異步隊列串行執行。把 set 操作放在隊列中使其串行化,必須得一個一個執行。如通過消息中間件異步執行。
3、使用類似SQL的樂觀鎖機制 。解決途徑是在並發寫入Redis緩存時,用要寫入數據的版本號和時間戳與Redis中的數據進行對比,如果寫入的數據時間戳或者版本號 比Redis 高,則寫入Redis中;否則就不寫入。