0. 版本說明
- JDK 1.8.0_181
- SpringBoot 2.2.6.RELEASE
- JUnit 5.5
- Maven 3.5.4
- Redis redis:5.0.6
1. 環(huán)境準(zhǔn)備
1.1 Spring Boot項(xiàng)目建立
- pom.xml中依賴項(xiàng)配置
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.ys</groupId>
<artifactId>redis</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>redis</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- 引入Redis支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 引入Web項(xiàng)目支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 引入devtools支持熱部署, 讓修改立馬生效 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- 用來支持.properties和.xml配置文件解析 -->
<dependency>
<groupId> org.springframework.boot </groupId>
<artifactId> spring-boot-configuration-processor </artifactId>
<optional> true </optional>
</dependency>
<!-- 用來支持單元測(cè)試, 包含了JUnit5 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<!-- 用來為JUnit4過渡到JUnit5, 這里無需過渡 -->
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<!-- 表示使用Maven來執(zhí)行build -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
1.2 RedisConfig配置
- application.properties中配置
# Redis數(shù)據(jù)庫(kù)HOST
spring.redis.host=127.0.0.1
# Redis數(shù)據(jù)庫(kù)PORT
spring.redis.port=6379
# Redis數(shù)據(jù)庫(kù)索引
spring.redis.database=0
# Redis數(shù)據(jù)庫(kù)密碼
spring.redis.password=
# 連接池最大連接數(shù)(使用負(fù)值表示沒有限制)
spring.redis.jedis.pool.max-active=8
# 連接池最大阻塞等待時(shí)間(使用負(fù)值表示沒有限制)
spring.redis.jedis.pool.max-wait=-1
# 連接池中的最大空閑連接
spring.redis.jedis.pool.max-idle=8
# 連接池中的最小空閑連接
spring.redis.jedis.pool.min-idle=0
# 連接超時(shí)時(shí)間(毫秒)
spring.redis.timeout=300
- RedisConfig.java中的Bean創(chuàng)建
@Configuration
public class RedisConfig {
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
jackson2JsonRedisSerializer.setObjectMapper(om);
StringRedisSerializer keySerializer = new StringRedisSerializer();
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(keySerializer);
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashKeySerializer(keySerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
@Bean
@ConditionalOnMissingBean(StringRedisTemplate.class)
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
這樣, StringRedisTemplate和RedisTemplate就交由BeanFactory來創(chuàng)建, 可以保證全局唯一
1.3 SpringBootTest準(zhǔn)備
@SpringBootTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class RedisStringTest {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
@Order(1)
public void testSet() {
stringRedisTemplate.opsForValue().set("test-string-value", "Hello World");
}
}
重點(diǎn)是
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
和
@Order(1)
這兩個(gè)注解, 規(guī)定了@Test注解方法的執(zhí)行順序
2. 基本操作
2.1 字符串(string)
- 設(shè)置
@Test
@Order(1)
public void testSet() {
stringRedisTemplate.opsForValue().set("test-string-value", "Hello World");
}
- 獲取
@Test
@Order(2)
public void testGet() {
String result = stringRedisTemplate.opsForValue().get("test-string-value");
System.out.println(result);
}
打印結(jié)果
Hello World
- 設(shè)置值并帶有超時(shí)機(jī)制
@Test
@Order(3)
public void testSetTimeout() {
stringRedisTemplate.opsForValue().set("test-string-key-timeout", "Hello Again", 15, TimeUnit.SECONDS);
}
15秒后, 再在Redis命令行中查詢?cè)搆ey, 發(fā)現(xiàn)已不存在
- 刪除
@Test
@Order(4)
public void testDelete() {
stringRedisTemplate.delete("test-string-value");
}
2.2 列表(list)
- 從左側(cè)新增
@Test
@Order(1)
public void lpush() {
redisTemplate.opsForList().leftPush("TestList", "TestLeftPush");
}
- 從右側(cè)新增
@Test
@Order(2)
public void rpush() {
redisTemplate.opsForList().rightPush("TestList", "TestRightPush");
}
- 從左側(cè)彈出
@Test
@Order(3)
public void lpop() {
Object result = redisTemplate.opsForList().leftPop("TestList");
System.out.println("lpop的結(jié)果: " + result);
}
輸出結(jié)果
lpop的結(jié)果: TestLeftPush
- 從右側(cè)彈出
@Test
@Order(4)
public void rpop() {
Object result = redisTemplate.opsForList().rightPop("TestList");
System.out.println("rpop第1次的結(jié)果為: " + result);
result = redisTemplate.opsForList().rightPop("TestList");
System.out.println("rpop第2次的結(jié)果為: " + result);
}
輸出結(jié)果
rpop第1次的結(jié)果為: TestRightPush
rpop第2次的結(jié)果為: null
2.3 哈希(hash)
- 設(shè)置
@Test
@Order(1)
public void testPut() {
redisTemplate.opsForHash().put("TestHash", "FirstElement", "Hello, Redis hash.");
Assert.isTrue(redisTemplate.opsForHash().hasKey("TestHash", "FirstElement"),
"HashKey: key=TestHash, field=FirstElement不存在!");
}
- 獲取
@Test
@Order(2)
public void testGet() {
Object element = redisTemplate.opsForHash().get("TestHash", "FirstElement");
Assert.isTrue(element.equals("Hello, Redis hash."), "Hash value不匹配!");
}
- 刪除
@Test
@Order(3)
public void testDel() {
redisTemplate.opsForHash().delete("TestHash", "FirstElement");
Assert.isTrue(!redisTemplate.opsForHash().hasKey("TestHash", "FirstElement"),
"HashKey: key=TestHash, field=FirstElement依然存在!");
}
2.4 集合(set)
- 新增
@Test
@Order(1)
public void testAdd() {
redisTemplate.opsForSet().add("TestSet", "e1", "e2", "e3");
long size = redisTemplate.opsForSet().size("TestSet");
System.out.println("TestSet's size is: " + size);
}
輸出結(jié)果
TestSet's size is: 3
- 獲取
@Test
@Order(2)
public void testGet() {
Set<String> testSet = redisTemplate.opsForSet().members("TestSet");
System.out.println(testSet);
}
輸出結(jié)果
[e1, e2, e3]
- 刪除
@Test
@Order(3)
public void testDel() {
redisTemplate.opsForSet().remove("TestSet", "e1", "e2");
Set<String> testSet = redisTemplate.opsForSet().members("TestSet");
System.out.println("刪除操作后, 當(dāng)前集合元素為: " + testSet);
}
輸出結(jié)果
刪除操作后, 當(dāng)前集合元素為: [e3]
2.5 有序集合(zset)
- 新增(單個(gè) VS 多個(gè))
@Test
@Order(1)
public void testAdd() {
redisTemplate.opsForZSet().add("TestZset", "e1", 1);
Set<ZSetOperations.TypedTuple<String>> zset = new HashSet();
zset.add(new DefaultTypedTuple("e2", 20.0));
zset.add(new DefaultTypedTuple("e3", 30.0));
redisTemplate.opsForZSet().add("TestZset", zset);
}
- 獲取(按名次/分?jǐn)?shù)/倒序)
@Test
@Order(2)
public void testRange() {
Set<String> results = redisTemplate.opsForZSet().range("TestZset", 0, 1);
System.out.println("分?jǐn)?shù)最低的2個(gè)成員 range(0, 1): " + results);
results = redisTemplate.opsForZSet().rangeByScore("TestZset", 0.0, 100.0);
System.out.println("分?jǐn)?shù)處于指定區(qū)間的成員 rangeByScore(0.0, 100.0): " + results);
Set<ZSetOperations.TypedTuple<String>> zset = redisTemplate.opsForZSet().rangeByScoreWithScores("TestZset", 0.0, 100.0);
System.out.print("分?jǐn)?shù)處于指定區(qū)間的成員 rangeByScoreWithScores(0.0, 100.0): [");
zset.stream().forEach(x -> System.out.print("<value=" + x.getValue() + ",score=" + x.getScore() + ">,"));
System.out.println("]");
Set<ZSetOperations.TypedTuple<String>> topScorer = redisTemplate.opsForZSet().reverseRangeWithScores("TestZset", 0, 0);
System.out.print("分?jǐn)?shù)最高的1個(gè)成員 reverseRangeWithScores(0, 0): [");
topScorer.stream().forEach(x -> System.out.print("<value=" + x.getValue() + ",score=" + x.getScore() + ">,"));
System.out.println("]");
}
輸出結(jié)果
分?jǐn)?shù)最低的2個(gè)成員 range(0, 1): [e1, e2]
分?jǐn)?shù)處于指定區(qū)間的成員 rangeByScore(0.0, 100.0): [e1, e2, e3]
分?jǐn)?shù)處于指定區(qū)間的成員 rangeByScoreWithScores(0.0, 100.0): [<value=e1,score=1.0>,<value=e2,score=20.0>,<value=e3,score=30.0>,]
分?jǐn)?shù)最高的1個(gè)成員 reverseRangeWithScores(0, 0): [<value=e3,score=30.0>,]
- 獲取成員數(shù)量
@Test
@Order(3)
public void testSize() {
long size = redisTemplate.opsForZSet().size("TestZset");
System.out.println("key=TestZset的有序集合的成員數(shù): " + size);
}
輸出結(jié)果
key=TestZset的有序集合的成員數(shù): 3
- 根據(jù)值獲得分?jǐn)?shù)
@Test
@Order(4)
public void testScore() {
double score = redisTemplate.opsForZSet().score("TestZset", "e2");
System.out.println("成員e2的分?jǐn)?shù)為: " + score);
}
輸出結(jié)果
成員e2的分?jǐn)?shù)為: 20.0
- 根據(jù)值獲得名次
@Test
@Order(5)
public void testRank() {
Set<ZSetOperations.TypedTuple<String>> zset = redisTemplate.opsForZSet().rangeWithScores("TestZset", 0, -1);
zset.stream().forEach(x -> System.out.printf("成員%s的分?jǐn)?shù)為:%f, 名次為:%d\n",
x.getValue(),
x.getScore(),
redisTemplate.opsForZSet().rank("TestZset", x.getValue())));
}
輸出結(jié)果
成員e1的分?jǐn)?shù)為:1.000000, 名次為:0
成員e2的分?jǐn)?shù)為:20.000000, 名次為:1
成員e3的分?jǐn)?shù)為:30.000000, 名次為:2
- 修改分?jǐn)?shù)(覆蓋/加減)
@Test
@Order(6)
public void testChangeScore() {
redisTemplate.opsForZSet().add("TestZset", "e1", 50.0);
double score = redisTemplate.opsForZSet().score("TestZset", "e1");
System.out.println("通過zadd后, e1的分?jǐn)?shù)被覆蓋成: " + score);
score = redisTemplate.opsForZSet().incrementScore("TestZset", "e1", 10.0);
System.out.println("通過incrementScore(10.0)后, e1的分?jǐn)?shù)變成: " + score);
}
輸出結(jié)果
通過zadd后, e1的分?jǐn)?shù)被覆蓋成: 50.0
通過incrementScore(10.0)后, e1的分?jǐn)?shù)變成: 60.0
- 刪除
@Test
@Order(7)
public void testDel() {
redisTemplate.opsForZSet().remove("TestZset", "e1");
Set<String> zset = redisTemplate.opsForZSet().range("TestZset", 0, -1);
System.out.println("剩余成員為: " + zset);
}
輸出結(jié)果
剩余成員為: [e2, e3]
3. 坑
3.1 key值帶額外雙引號(hào)
情況:
- 利用
RedisTemplate
來向Redis
增加一個(gè)key
值為TestList
的列表, 然后通過lpush
和rpush
向該列表添加兩個(gè)值TestLeftPush
和TestRightPush
; - 在
Redis
命令行中手動(dòng)運(yùn)行lpush mylist l1
- 然后在
Redis
命令行中運(yùn)行keys *
, 發(fā)現(xiàn)key
值為
"\"TestList\""
"mylist"
為何會(huì)不一樣?
原因:
RedisConfig類中通過
redisTemplate.setKeySerializer(jackson2JsonRedisSerializer)
修改key
值解析方式為JSON格式了, 于是key
值就多了雙引號(hào)
解決辦法:
修改RedisConfig
類中代碼
StringRedisSerializer keySerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(keySerializer);
redisTemplate.setHashKeySerializer(keySerializer);
3.2 Redis命令超時(shí)
報(bào)錯(cuò):
Command timed out after no timeout
原因:
未設(shè)置redis超時(shí)時(shí)間
解決辦法:
application.properties修改配置項(xiàng)
# 連接超時(shí)時(shí)間(毫秒)
spring.redis.timeout=300
3.3 SpringBootTest中Test方法的執(zhí)行順序
情況:
在@SpringBootTest注解的類里, 四個(gè)@Test注解的方法, 執(zhí)行順序并不是按照定義順序, 且每次執(zhí)行都一樣; 那么如何控制這些@Test注解方法的執(zhí)行順序器躏?
原因:
使用的是JUnit5, 但未明確定義@Test執(zhí)行順序
解決辦法:
- 增加類注解
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
- 增加@Test方法注解
@Order(1)
4. 拓展思考
4.1 適用場(chǎng)景
分布式緩存:在分布式的系統(tǒng)架構(gòu)中,將緩存存儲(chǔ)在內(nèi)存中顯然不當(dāng)捐顷,因?yàn)榫彺嫘枰c其他機(jī)器共享炕檩,這時(shí) Redis 便挺身而出了换棚,緩存也是 Redis 使用最多的場(chǎng)景。
分布式鎖:在高并發(fā)的情況下米奸,我們需要一個(gè)鎖來防止并發(fā)帶來的臟數(shù)據(jù)悔叽,Java 自帶的鎖機(jī)制顯然對(duì)進(jìn)程間的并發(fā)并不好使构韵,此時(shí)可以利用 Redis 單線程的特性來實(shí)現(xiàn)我們的分布式鎖周蹭。
Session 存儲(chǔ)/共享:Redis 可以將 Session 持久化到存儲(chǔ)中,這樣可以避免由于機(jī)器宕機(jī)而丟失用戶會(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ù)載均衡和高可用性揍移。
限速次和,接口訪問頻率限制:比如發(fā)送短信驗(yàn)證碼的接口,通常為了防止別人惡意頻刷那伐,會(huì)限制用戶每分鐘獲取驗(yàn)證碼的頻率踏施,例如一分鐘不能超過 5 次石蔗。
4.2 緩存與數(shù)據(jù)庫(kù)一致性
- 先寫數(shù)據(jù)庫(kù), 再寫緩存
- 先寫緩存, 再寫數(shù)據(jù)庫(kù)
大部分情況下,我們的緩存理論上都是需要可以從數(shù)據(jù)庫(kù)恢復(fù)出來的畅形,所以基本上采取第一種順序都是不會(huì)有問題的养距。針對(duì)那些必須保證數(shù)據(jù)庫(kù)和緩存一致的情況,通常是不建議使用緩存的日熬,如果必須使用的話
4.3 緩存擊穿
用戶故意查詢數(shù)據(jù)庫(kù)中不存在(意味著緩存肯定也沒有)的內(nèi)容棍厌,導(dǎo)致每次查詢都會(huì)去庫(kù)里查一次
策略:
- 使用互斥鎖排隊(duì): 當(dāng)從緩存中獲取數(shù)據(jù)失敗時(shí),給當(dāng)前接口加上鎖竖席,從數(shù)據(jù)庫(kù)中加載完數(shù)據(jù)并寫入后再釋放鎖定铜。若其它線程獲取鎖失敗,則等待一段時(shí)間后重試怕敬。
- 使用布隆過濾器: 將所有可能存在的數(shù)據(jù)緩存放到布隆過濾器中揣炕,當(dāng)黑客訪問不存在的緩存時(shí)迅速返回避免緩存及 DB 掛掉。
4.4 緩存雪崩
緩存down了东跪,所有查詢都落到數(shù)據(jù)庫(kù)
策略: 讓緩存不會(huì)真正的down畸陡,具體來說
- 像解決緩存穿透一樣加鎖排隊(duì)。
- 建立備份緩存: 緩存A和緩存B虽填,A設(shè)置超時(shí)時(shí)間丁恭,B 不設(shè)值超時(shí)時(shí)間,先從 A 讀緩存斋日,A 沒有讀 B牲览,并且更新 A 緩存和 B 緩存。
- 計(jì)算數(shù)據(jù)緩存節(jié)點(diǎn)的時(shí)候采用一致性 hash: 這樣在節(jié)點(diǎn)數(shù)量發(fā)生改變時(shí)不會(huì)存在大量的緩存數(shù)據(jù)需要遷移的情況發(fā)生恶守。
4.5 緩存并發(fā)
這里的并發(fā)指的是多個(gè) Redis 的客戶端同時(shí) set 值引起的并發(fā)問題第献。比較有效的解決方案就是把 set 操作放在隊(duì)列中使其串行化,必須得一個(gè)一個(gè)執(zhí)行兔港。
5. 參考鏈接:
了解 Redis 并在 Spring Boot 項(xiàng)目中使用 Redis
SpringBoot高級(jí)篇Redis之ZSet數(shù)據(jù)結(jié)構(gòu)使用姿勢(shì)
解決redis redistemplate KEY為字符串是多雙引號(hào)的問題