Redis

Redis

初识 Redis

认识 NoSQL

SQL NoSQL
数据结构 结构化(Structured) 非结构化
数据关联 关联的(Relational) 无关联的
查询方式 SQL 查询 非 SQL
事务特性 ACID BASE
存储方式 磁盘 内存
扩展性 垂直 水平
使用场景 1)数据结构固定
2)相关业务对数据安全性、一致性要求较高
1)数据结构不固定
2)对一致性、安全性要求不高
3)对性能要求

认识 Redis

特征:

  • 键值(key-value)型,value 支持多种不同数据结构,功能丰富
  • 单线程,每个命令具备原子性
  • 低延迟,速度快(基于内存、IO 多路复用、良好的编码)
  • 支持数据持久化
  • 支持主从集群、分片集群
  • 支持多语言客户端

Redis 命令

数据结构介绍

Redis 是一个 key-value 的数据库,key 一般是 String 类型,不过 value 的类型多种多样

官网(https://redis.io/commands)

key value
String hello world
Hash {name: “Jack”, age: 21}
List [A -> B -> C-> C]
Set {A, B, C}
SortedSet {A: 1, B: 2, C: 3}
GEO {A: (120.3, 30.6)}
BitMap 0110110101110101011
HyperLog 0110110101110101011

通用命令

通用命令是不分数据类型的,都可以使用的命令

  • KEYS:查看符合模板的所有 key,不建议在生产环境设备上使用
  • DEL:删除一个指定的 key
  • EXISTS:判断 key 是否存在
  • EXPIRE:给一个 key 设置有效期,有效期到期时该 key 会被自动删除

通过help [command] 可以查看一个命令的具体用法,例如:

1
2
3
4
5
6
7
# 查看keys命令的帮助信息:
127.0.0.1:6379> help keys

KEYS pattern
summary: Find all keys matching the given pattern
since: 1.0.0
group: generic

String 类型

String 类型,也就是字符串类型,是 Redis 中最简单的存储类型

其 value 是字符串,不过根据字符串的格式不同,又可以分为3类:

  • string:普通字符串
  • int:整数类型,可以做自增、自减操作
  • float:浮点类型,可以做自增、自减操作

不管是哪种格式,底层都是字节数组形式存储,只不过是编码方式不同。字符串类型的最大空间不能超过512m

KEY VALUE
msg hello world
num 10
score 92.5

String 常见命令

String的常见命令有:

  • SET:添加或者修改已经存在的一个 String 类型的键值对
  • GET:根据key获取 String 类型的 value
  • MSET:批量添加多个 String 类型的键值对
  • MGET:根据多个 key 获取多个 String 类型的 value
  • INCR:让一个整型的 key 自增1
  • INCRBY:让一个整型的 key 自增并指定步长,例如:incrby num 2 让 num 值自增2
  • INCRBYFLOAT:让一个浮点类型的数字自增并指定步长
  • SETNX:添加一个 String 类型的键值对,前提是这个 key 不存在,否则不执行
  • SETEX:添加一个 String 类型的键值对,并且指定有效期

Key 结构

Redis 没有类似 MySQL中 的 Table 的概念,该如何区分不同类型的 key 呢?

例如,需要存储用户、商品信息到 redis,有一个用户 id 是1,有一个商品 id 恰好也是1,此时如果使用 id 作为 key,那就会冲突了,该怎么办?

答:可以通过给 key 添加前缀加以区分,不过这个前缀不是随便加的,有一定的规范:

Redis 的 key 允许有多个单词形成层级结构,多个单词之间用 ‘:’ 隔开,格式如下:

1
[项目名]:[业务名]:[类型]:[id]

这个格式并非固定,也可以根据自己的需求来删除或添加词条。这样以来,我们就可以把不同类型的数据区分开了。从而避免了key的冲突问题。

例如项目名称叫 hanyang,有 user 和 product 两种不同类型的数据,可以这样定义 key:

  • user相关的key:hanyang:user:1

  • product相关的key:hanyang:product:1

Hash 类型

Hash 类型,也叫散列,其 value 是一个无序字典,类似于 Java 中的 HashMap 结构

Hash 的常见命令有:

  • HSET key field value:添加或者修改 hash 类型 key 的 field 的值

  • HGET key field:获取一个 hash 类型 key 的 field 的值

  • HMSET:批量添加多个 hash 类型 key 的 field 的值

  • HMGET:批量获取多个 hash 类型 key 的 field 的值

  • HGETALL:获取一个 hash 类型的 key 中的所有的 field 和 value

  • HKEYS:获取一个 hash 类型的 key 中的所有的 field

  • HINCRBY:让一个 hash 类型 key 的字段值自增并指定步长

  • HSETNX:添加一个 hash 类型的 key 的 field 值,前提是这个 field 不存在,否则不执行

List 类型

Redis 中的 List 类型与 Java 中的 LinkedList 类似,可以看做是一个双向链表结构。既可以支持正向检索和也可以支持反向检索

特征也与 LinkedList 类似:

  • 有序
  • 元素可以重复
  • 插入和删除快
  • 查询速度一般

常用来存储一个有序数据,例如:朋友圈点赞列表,评论列表等。

List的常见命令有:

  • LPUSH key element … :向列表左侧插入一个或多个元素
  • LPOP key:移除并返回列表左侧的第一个元素,没有则返回 nil
  • RPUSH key element … :向列表右侧插入一个或多个元素
  • RPOP key:移除并返回列表右侧的第一个元素
  • LRANGE key star end:返回一段角标范围内的所有元素
  • BLPOP和BRPOP:与 LPOP 和 RPOP 类似,只不过在没有元素时等待指定时间,而不是直接返回 nil

如何利用 List 结构模拟一个栈?

  • 入口和出口在同一边

如何利用 List 结构模拟一个队列?

  • 入口和出口在不同边

如何利用 List 结构模拟一个阻塞队列?

  • 入口和出口在不同边
  • 出队时采用 BLPOP 或 BRPOP

Set 类型

Redis 的 Set 结构与 Java 中的 HashSet 类似,可以看做是一个 value 为 null 的 HashMap。因为也是一个 hash 表,因此具备与 HashSet 类似的特征:

  • 无序

  • 元素不可重复

  • 查找快

  • 支持交集、并集、差集等功能

Set的常见命令有:

  • SADD key member … :向 set 中添加一个或多个元素
  • SREM key member … : 移除 set 中的指定元素
  • SCARD key: 返回 set 中元素的个数
  • SISMEMBER key member:判断一个元素是否存在于 set 中
  • SMEMBERS:获取 set 中的所有元素
  • SINTER key1 key2 … :求 key1 与 key2 的交集

SortedSet 类型

Redis 的 SortedSet 是一个可排序的 set 集合,与 Java 中的 TreeSet 有些类似,但底层数据结构却差别很大。SortedSet 中的每一个元素都带有一个 score 属性,可以基于 score 属性对元素排序,底层的实现是一个跳表(SkipList)加 hash 表

SortedSet 具备下列特性:

  • 可排序
  • 元素不重复
  • 查询速度快

因为 SortedSet 的可排序特性,经常被用来实现排行榜这样的功能。

SortedSet 的常见命令有:

  • ZADD key score member:添加一个或多个元素到 sorted set ,如果已经存在则更新其 score 值
  • ZREM key member:删除 sorted set 中的一个指定元素
  • ZSCORE key member : 获取 sorted set 中的指定元素的score值
  • ZRANK key member:获取 sorted set 中的指定元素的排名
  • ZCARD key:获取 sorted set 中的元素个数
  • ZCOUNT key min max:统计 score 值在给定范围内的所有元素的个数
  • ZINCRBY key increment member:让 sorted set 中的指定元素自增,步长为指定的 increment 值
  • ZRANGE key min max:按照 score 排序后,获取指定排名范围内的元素
  • ZRANGEBYSCORE key min max:按照score排序后,获取指定 score 范围内的元素
  • ZDIFF、ZINTER、ZUNION:求差集、交集、并集

注意:所有的排名默认都是升序,如果要降序则在命令的Z后面添加 REV 即可,例如:

  • 升序获取 sorted set 中的指定元素的排名:ZRANK key member

  • 降序获取 sorted set 中的指定元素的排名:ZREVRANK key memeber

Redis 的 Java 客户端

官网(https://redis.io/docs/clients/)

Jedis 客户端

官网( https://github.com/redis/jedis)

快速上手

1)引入依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!--jedis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.7.0</version>
</dependency>
<!--单元测试-->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.7.0</version>
<scope>test</scope>
</dependency>

2)建立连接

新建一个单元测试类,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
private Jedis jedis;

@BeforeEach
void setUp() {
// 1.建立连接
// jedis = new Jedis("192.168.150.101", 6379);
jedis = JedisConnectionFactory.getJedis();
// 2.设置密码
jedis.auth("123321");
// 3.选择库
jedis.select(0);
}

3)测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
void testString() {
// 存入数据
String result = jedis.set("name", "虎哥");
System.out.println("result = " + result);
// 获取数据
String name = jedis.get("name");
System.out.println("name = " + name);
}

@Test
void testHash() {
// 插入hash数据
jedis.hset("user:1", "name", "Jack");
jedis.hset("user:1", "age", "21");

// 获取
Map<String, String> map = jedis.hgetAll("user:1");
System.out.println(map);
}

4)释放资源

1
2
3
4
5
6
@AfterEach
void tearDown() {
if (jedis != null) {
jedis.close();
}
}

连接池

Jedis 本身是线程不安全的,并且频繁的创建和销毁连接会有性能损耗,因此推荐使用 Jedis 连接池代替 Jedis 的直连方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.hanyang.jedis.util;

import redis.clients.jedis.*;

public class JedisConnectionFactory {

private static JedisPool jedisPool;

static {
// 配置连接池
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(8);
poolConfig.setMaxIdle(8);
poolConfig.setMinIdle(0);
poolConfig.setMaxWaitMillis(1000);
// 创建连接池对象,参数:连接池配置、服务端ip、服务端端口、超时时间、密码
jedisPool = new JedisPool(poolConfig, "192.168.150.101", 6379, 1000, "123321");
}

public static Jedis getJedis(){
return jedisPool.getResource();
}
}

SpringDataRedis 客户端

SpringData 是 Spring 中数据操作的模块,包含对各种数据库的集成,其中对 Redis 的集成模块就叫做 SpringDataRedis,官网地址(https://spring.io/projects/spring-data-redis)

  • 提供了对不同 Redis 客户端的整合(Lettuce 和 Jedis)
  • 提供了 RedisTemplate 统一 API 来操作 Redis
  • 支持 Redis 的发布订阅模型
  • 支持 Redis 哨兵和 Redis 集群
  • 支持基于 Lettuce 的响应式编程
  • 支持基于 JDK、JSON、字符串、Spring 对象的数据序列化及反序列化
  • 支持基于 Redis 的 JDKCollection 实现

SpringDataRedis 中提供了 RedisTemplate 工具类,其中封装了各种对Redis的操作。并且将不同数据类型的操作 API 封装到了不同的类型中

API 返回值类型 说明
redisTemplate.opsForValue() ValueOpeartions 操作 String 类型数据
redisTemplate.opsForHash() HashOperations 操作 Hash 类型数据
redisTemplate.opsForList() ListOperations 操作 List 类型数据
redisTemplate.opsForSet() SetOperations 操作 Set 类型数据
redisTemplate.opsForZSet() ZSetOperations 操作 SortedSet 类型数据
redisTemplate 通用的命令

快速上手

1)引入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<?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.5.7</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.hanyang</groupId>
<artifactId>redis-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>redis-demo</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>
<!--common-pool-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!--Jackson依赖-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>

</project>

2)配置 Redis

1
2
3
4
5
6
7
8
9
10
11
spring:
redis:
host: 192.168.150.101
port: 6379
password: 123321
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
max-wait: 100ms

3)注入 RedisTemplate

因为有了 SpringBoot 的自动装配,可以拿来就用:

1
2
3
4
5
6
@SpringBootTest
class RedisStringTests {

@Autowired
private RedisTemplate redisTemplate;
}

4)编写测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@SpringBootTest
class RedisStringTests {

@Autowired
private RedisTemplate edisTemplate;

@Test
void testString() {
// 写入一条String数据
redisTemplate.opsForValue().set("name", "renkelin");
// 获取string数据
Object name = stringRedisTemplate.opsForValue().get("name");
System.out.println("name = " + name);
}
}

自定义序列化

可以自定义 RedisTemplate 的序列化方式,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Configuration
public class RedisConfig {

@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory){
// 创建RedisTemplate对象
RedisTemplate<String, Object> template = new RedisTemplate<>();
// 设置连接工厂
template.setConnectionFactory(connectionFactory);
// 创建JSON序列化工具
GenericJackson2JsonRedisSerializer jsonRedisSerializer =
new GenericJackson2JsonRedisSerializer();
// 设置Key的序列化
template.setKeySerializer(RedisSerializer.string());
template.setHashKeySerializer(RedisSerializer.string());
// 设置Value的序列化
template.setValueSerializer(jsonRedisSerializer);
template.setHashValueSerializer(jsonRedisSerializer);
// 返回
return template;
}
}

StringRedisTemplate

为了节省内存空间,可以不使用 JSON 序列化器来处理 value,而是统一使用 String 序列化器,要求只能存储 String 类型的 key 和 value。当需要存储 Java 对象时,手动完成对象的序列化和反序列化

省去了自定义 RedisTemplate 的序列化方式的步骤,而是直接使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Autowired
private StringRedisTemplate stringRedisTemplate;
// JSON序列化工具
private static final ObjectMapper mapper = new ObjectMapper();

@Test
void testSaveUser() throws JsonProcessingException {
// 创建对象
User user = new User("renkelin", 21);
// 手动序列化
String json = mapper.writeValueAsString(user);
// 写入数据
stringRedisTemplate.opsForValue().set("user:200", json);

// 获取数据
String jsonUser = stringRedisTemplate.opsForValue().get("user:200");
// 手动反序列化
User user1 = mapper.readValue(jsonUser, User.class);
System.out.println("user1 = " + user1);
}

总结

RedisTemplate 的两种序列化实践方案

方案一:

  1. 自定义 RedisTemplate
  2. 修改 RedisTemplate 的序列化器为 GenericJackson RedisSerializer

方案二:

  1. 使用 StringRedisTemplate
  2. 写入 Redis 时,手动把对象序列化为 JSON
  3. 读取 Redis 时,手动把读取到的 JSON 反序列化为对象

缓存

什么是缓存

缓存就是数据交换的缓冲区(称作 Cache),是存储数据的临时地方,一般读写性能较高

缓存的作用:

  • 降低后端负载
  • 提高读写效率,降低响应时间

缓存的成本:

  • 数据一致性成本
  • 代码维护成本
  • 运维成本

缓存更新策略

内存淘汰 超时剔除 主动更新
说明 不用自己维护,利用 Redis 的内存淘汰机制,当内存不足时自动淘汰部分数据。下次查询时跟更新缓存 给缓存数据添加 TTL 时间,到期后自动删除缓存。下次查询时更新缓存 编写业务逻辑,在修改数据库的同时,更新缓存
一致性 一般
维护成本

业务场景:

  • 低一致性需求:使用内存淘汰机制。例如店铺类型的查询缓存
  • 高一致性需求:主动更新,并以超时剔除为兜底方案。例如店铺详情查询的缓存

主动更新策略

  1. Cache Aside Pattern(常用) :由缓存的调用者,在更新数据库的同时更新缓存
  2. Read/Write Through Pattern :缓存与数据库整合为一个服务,由服务来维护一致性。调用者调用该服务,无需关心缓存一致性问题
  3. Write Behind Caching Pattern :调用者只操作缓存,由其它线程异步的将缓存数据持久化到数据库,保证最终一致

操作缓存和数据库时有三个问题需要考虑:

  1. 删除缓存还是更新缓存?
    • 更新缓存:每次更新数据库都更新缓存,无效写操作较多(×)
    • 删除缓存:更新数据库时让缓存失效,查询时再更新缓存(√)
  2. 如何保证缓存与数据库操作的同时成功或失败?
    • 单体系统,将缓存与数据库操作放在一个事务
    • 分布式系统,利用 TCC 等分布式事务方案
  3. 先操作缓存还是先操作数据库?
    • 先删除缓存,再操作数据库
    • 先操作数据库,再删除缓存

最佳实践方案

  1. 低一致性需求:使用 Redis 自带的内存淘汰机制
  2. 高一致性需求:主动更新,并以超时剔除作为兜底方案
    • 读操作:
      • 缓存命中则直接返回
      • 缓存未命中则查询数据库,并写入缓存,设定超时时间
    • 写操作:
      • 先写数据库,然后再删除缓存
      • 要确保数据库与缓存操作的原子性

Redis
https://www.renkelin.vip/2024/01/22/Redis/
Author
Kolin
Posted on
January 22, 2024
Licensed under