0. 版本说明
- JDK 1.8.0_181
- SpringBoot 2.2.6.RELEASE
- JUnit 5.5
- Maven 3.5.4
- Redis redis:5.0.6
1. 环境准备
1.1 Spring Boot项目建立
- pom.xml中依赖项配置
<?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项目支持 -->
<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>
<!-- 用来支持单元测试, 包含了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来执行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数据库HOST
spring.redis.host=127.0.0.1
# Redis数据库PORT
spring.redis.port=6379
# Redis数据库索引
spring.redis.database=0
# Redis数据库密码
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制)
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
# 连接超时时间(毫秒)
spring.redis.timeout=300
- RedisConfig.java中的Bean创建
@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来创建, 可以保证全局唯一
1.3 SpringBootTest准备
@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");
}
}
重点是
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
和
@Order(1)
这两个注解, 规定了@Test注解方法的执行顺序
2. 基本操作
2.1 字符串(string)
- 设置
@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);
}
打印结果
Hello World
- 设置值并带有超时机制
@Test
@Order(3)
public void testSetTimeout() {
stringRedisTemplate.opsForValue().set("test-string-key-timeout", "Hello Again", 15, TimeUnit.SECONDS);
}
15秒后, 再在Redis命令行中查询该key, 发现已不存在
- 删除
@Test
@Order(4)
public void testDelete() {
stringRedisTemplate.delete("test-string-value");
}
2.2 列表(list)
- 从左侧新增
@Test
@Order(1)
public void lpush() {
redisTemplate.opsForList().leftPush("TestList", "TestLeftPush");
}
- 从右侧新增
@Test
@Order(2)
public void rpush() {
redisTemplate.opsForList().rightPush("TestList", "TestRightPush");
}
- 从左侧弹出
@Test
@Order(3)
public void lpop() {
Object result = redisTemplate.opsForList().leftPop("TestList");
System.out.println("lpop的结果: " + result);
}
输出结果
lpop的结果: TestLeftPush
- 从右侧弹出
@Test
@Order(4)
public void rpop() {
Object result = redisTemplate.opsForList().rightPop("TestList");
System.out.println("rpop第1次的结果为: " + result);
result = redisTemplate.opsForList().rightPop("TestList");
System.out.println("rpop第2次的结果为: " + result);
}
输出结果
rpop第1次的结果为: TestRightPush
rpop第2次的结果为: null
2.3 哈希(hash)
- 设置
@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);
}
输出结果
TestSet's size is: 3
- 获取
@Test
@Order(2)
public void testGet() {
Set<String> testSet = redisTemplate.opsForSet().members("TestSet");
System.out.println(testSet);
}
输出结果
[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("删除操作后, 当前集合元素为: " + testSet);
}
输出结果
删除操作后, 当前集合元素为: [e3]
2.5 有序集合(zset)
- 新增(单个 VS 多个)
@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);
}
- 获取(按名次/分数/倒序)
@Test
@Order(2)
public void testRange() {
Set<String> results = redisTemplate.opsForZSet().range("TestZset", 0, 1);
System.out.println("分数最低的2个成员 range(0, 1): " + results);
results = redisTemplate.opsForZSet().rangeByScore("TestZset", 0.0, 100.0);
System.out.println("分数处于指定区间的成员 rangeByScore(0.0, 100.0): " + results);
Set<ZSetOperations.TypedTuple<String>> zset = redisTemplate.opsForZSet().rangeByScoreWithScores("TestZset", 0.0, 100.0);
System.out.print("分数处于指定区间的成员 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("分数最高的1个成员 reverseRangeWithScores(0, 0): [");
topScorer.stream().forEach(x -> System.out.print("<value=" + x.getValue() + ",score=" + x.getScore() + ">,"));
System.out.println("]");
}
输出结果
分数最低的2个成员 range(0, 1): [e1, e2]
分数处于指定区间的成员 rangeByScore(0.0, 100.0): [e1, e2, e3]
分数处于指定区间的成员 rangeByScoreWithScores(0.0, 100.0): [<value=e1,score=1.0>,<value=e2,score=20.0>,<value=e3,score=30.0>,]
分数最高的1个成员 reverseRangeWithScores(0, 0): [<value=e3,score=30.0>,]
- 获取成员数量
@Test
@Order(3)
public void testSize() {
long size = redisTemplate.opsForZSet().size("TestZset");
System.out.println("key=TestZset的有序集合的成员数: " + size);
}
输出结果
key=TestZset的有序集合的成员数: 3
- 根据值获得分数
@Test
@Order(4)
public void testScore() {
double score = redisTemplate.opsForZSet().score("TestZset", "e2");
System.out.println("成员e2的分数为: " + score);
}
输出结果
成员e2的分数为: 20.0
- 根据值获得名次
@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的分数为:%f, 名次为:%d\n",
x.getValue(),
x.getScore(),
redisTemplate.opsForZSet().rank("TestZset", x.getValue())));
}
输出结果
成员e1的分数为:1.000000, 名次为:0
成员e2的分数为:20.000000, 名次为:1
成员e3的分数为:30.000000, 名次为:2
- 修改分数(覆盖/加减)
@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的分数被覆盖成: " + score);
score = redisTemplate.opsForZSet().incrementScore("TestZset", "e1", 10.0);
System.out.println("通过incrementScore(10.0)后, e1的分数变成: " + score);
}
输出结果
通过zadd后, e1的分数被覆盖成: 50.0
通过incrementScore(10.0)后, e1的分数变成: 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);
}
输出结果
剩余成员为: [e2, e3]
3. 坑
3.1 key值带额外双引号
情况:
- 利用
RedisTemplate
来向Redis
增加一个key
值为TestList
的列表, 然后通过lpush
和rpush
向该列表添加两个值TestLeftPush
和TestRightPush
; - 在
Redis
命令行中手动运行lpush mylist l1
- 然后在
Redis
命令行中运行keys *
, 发现key
值为
"\"TestList\""
"mylist"
为何会不一样?
原因:
RedisConfig类中通过
redisTemplate.setKeySerializer(jackson2JsonRedisSerializer)
修改key
值解析方式为JSON格式了, 于是key
值就多了双引号
解决办法:
修改RedisConfig
类中代码
StringRedisSerializer keySerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(keySerializer);
redisTemplate.setHashKeySerializer(keySerializer);
3.2 Redis命令超时
报错:
Command timed out after no timeout
原因:
未设置redis超时时间
解决办法:
application.properties修改配置项
# 连接超时时间(毫秒)
spring.redis.timeout=300
3.3 SpringBootTest中Test方法的执行顺序
情况:
在@SpringBootTest注解的类里, 四个@Test注解的方法, 执行顺序并不是按照定义顺序, 且每次执行都一样; 那么如何控制这些@Test注解方法的执行顺序?
原因:
使用的是JUnit5, 但未明确定义@Test执行顺序
解决办法:
- 增加类注解
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
- 增加@Test方法注解
@Order(1)
4. 拓展思考
4.1 适用场景
分布式缓存:在分布式的系统架构中,将缓存存储在内存中显然不当,因为缓存需要与其他机器共享,这时 Redis 便挺身而出了,缓存也是 Redis 使用最多的场景。
分布式锁:在高并发的情况下,我们需要一个锁来防止并发带来的脏数据,Java 自带的锁机制显然对进程间的并发并不好使,此时可以利用 Redis 单线程的特性来实现我们的分布式锁。
Session 存储/共享:Redis 可以将 Session 持久化到存储中,这样可以避免由于机器宕机而丢失用户会话信息。
发布/订阅:Redis 还有一个发布/订阅的功能,您可以设定对某一个 key 值进行消息发布及消息订阅,当一个 key 值上进行了消息发布后,所有订阅它的客户端都会收到相应的消息。这一功能最明显的用法就是用作实时消息系统。
任务队列:Redis 的 lpush+brpop 命令组合即可实现阻塞队列,生产者客户端使用 lrpush 从列表左侧插入元素,多个消费者客户端使用 brpop 命令阻塞式的"抢"列表尾部的元素,多个客户端保证了消费的负载均衡和高可用性。
限速,接口访问频率限制:比如发送短信验证码的接口,通常为了防止别人恶意频刷,会限制用户每分钟获取验证码的频率,例如一分钟不能超过 5 次。
4.2 缓存与数据库一致性
- 先写数据库, 再写缓存
- 先写缓存, 再写数据库
大部分情况下,我们的缓存理论上都是需要可以从数据库恢复出来的,所以基本上采取第一种顺序都是不会有问题的。针对那些必须保证数据库和缓存一致的情况,通常是不建议使用缓存的,如果必须使用的话
4.3 缓存击穿
用户故意查询数据库中不存在(意味着缓存肯定也没有)的内容,导致每次查询都会去库里查一次
策略:
- 使用互斥锁排队: 当从缓存中获取数据失败时,给当前接口加上锁,从数据库中加载完数据并写入后再释放锁。若其它线程获取锁失败,则等待一段时间后重试。
- 使用布隆过滤器: 将所有可能存在的数据缓存放到布隆过滤器中,当黑客访问不存在的缓存时迅速返回避免缓存及 DB 挂掉。
4.4 缓存雪崩
缓存down了,所有查询都落到数据库
策略: 让缓存不会真正的down,具体来说
- 像解决缓存穿透一样加锁排队。
- 建立备份缓存: 缓存A和缓存B,A设置超时时间,B 不设值超时时间,先从 A 读缓存,A 没有读 B,并且更新 A 缓存和 B 缓存。
- 计算数据缓存节点的时候采用一致性 hash: 这样在节点数量发生改变时不会存在大量的缓存数据需要迁移的情况发生。
4.5 缓存并发
这里的并发指的是多个 Redis 的客户端同时 set 值引起的并发问题。比较有效的解决方案就是把 set 操作放在队列中使其串行化,必须得一个一个执行。
5. 参考链接:
了解 Redis 并在 Spring Boot 项目中使用 Redis
SpringBoot高级篇Redis之ZSet数据结构使用姿势
解决redis redistemplate KEY为字符串是多双引号的问题
网友评论