1. 初识Redis

redis是一种键值型的NoSql数据库,这里有两个关键字:

  • 键值型

  • NoSql

其中键值型,是指redis中存储的数据都是以key、value对的形式存储,而value的形式多种多样,可以是字符串、数值、甚至json:

redis中存储的数据形式

NoSql则是相对于传统关系型数据库而言,有很大差异的一种数据库。

1.1 认识NoSQL

NoSql可以翻译做Not Only Sql(不仅仅是SQL),或者是No Sql(非Sql的)数据库。是相对于传统关系型数据库而言,有很大差异的一种特殊的数据库,因此也称之为非关系型数据库

1.1.1 结构化与非结构化

传统关系型数据库是结构化数据,每一张表都有严格的约束信息:字段名、字段数据类型、字段约束等等信息,插入的数据必须遵守这些约束:

结构化数据

而NoSql则对数据库格式没有严格约束,往往形式松散,自由。

可以是键值型:

键值型

也可以是文档型:

文档型

甚至可以是图格式:

图格式

1.1.2 关联和非关联

传统数据库的表与表之间往往存在关联,例如外键:

外键

而非关系型数据库不存在关联关系,要维护关系要么靠代码中的业务逻辑,要么靠数据之间的耦合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
id: 1,
name: "张三",
orders: [
{
id: 1,
item: {
id: 10, title: "荣耀6", price: 4999
}
},
{
id: 2,
item: {
id: 20, title: "小米11", price: 3999
}
}
]
}

此处要维护 ‘张三’ 的订单与商品 ‘荣耀’ 和 ‘小米11’ 的关系,不得不冗余的将这两个商品保存在张三的订单文档中,不够优雅。还是建议用业务来维护关联关系。

1.1.3 查询方式

传统关系型数据库会基于Sql语句做查询,语法有统一标准;而不同的非关系数据库查询语法差异极大,五花八门各种各样。

查询方式

1.1.4 事务

  • 传统关系型数据库能满足事务ACID的原则。

  • 而非关系型数据库往往不支持事务,或者不能严格保证ACID的特性,只能实现基本的一致性。

1.1.5 总结

除了上述四点以外,在存储方式、扩展性、查询性能上关系型与非关系型也都有着显著差异,总结如下:

SQL与NoSQL总结

  • 存储方式
    • 关系型数据库基于磁盘进行存储,会有大量的磁盘IO,对性能有一定影响。
    • 非关系型数据库,他们的操作更多的是依赖于内存来操作,内存的读写速度会非常快,性能自然会好一些。
  • 扩展性
    • 关系型数据库集群模式一般是主从,主从数据一致,起到数据备份的作用,称为垂直扩展。
    • 非关系型数据库可以将数据拆分,存储在不同机器上,可以保存海量数据,解决内存大小有限的问题。称为水平扩展。
    • 关系型数据库因为表之间存在关联关系,如果做水平扩展会给数据查询带来很多麻烦。

1.2 认识Redis

redis诞生于2009年全称是 Remote Dictionary Server 远程词典服务器,是一个基于内存的键值型NoSQL数据库。

redis的官方网站地址:https://redis.io/

特征

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

作者:Salvatore Sanfilippo(网名:Antirez)

PS:redis的作者Antirez由于部分原因早在2020年就宣布退隐不在维护redis了。再见了,Redis之父!,不过他是一个挺有趣的人,有兴趣的话可以在网上查看一下关于他的其他信息。

2. Redis常见命令

redis是典型的 key-value 数据库,key一般是字符串,而value包含很多不同的数据类型:

value数据类型

redis为了方便我们学习,将操作不同数据类型的命令也做了分组,在官网可以查看到不同的命令:

官网查看redis命令

PS:英语不好的小伙伴也可以参考redis中文网的redis命令手册😀。

不同类型的命令称为一个group,我们也可以通过 help 命令来查看各种不同group的命令:

查看各种不同group的命令

接下来,我们就学习常见的五种基本数据类型的相关命令。

2.1 Redis通用命令

通用指令是部分数据类型的,都可以使用的指令,常见的有:

  • KEYS:查看符合模板的所有key
  • DEL:删除一个指定的key
  • EXISTS:判断key是否存在
  • EXPIRE:给一个key设置有效期,有效期到期时该key会被自动删除
  • TTL:查看一个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

2.2 String类型

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

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

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

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

String类型存储

2.2.1 String的常见命令

String的常见命令:

命令 描述
SET key value 添加或者修改已经存在的一个String类型的键值对
GET key 根据key获取String类型的value,如果key不存在,返回 nil 。如果key储存的值不是字符串类型,返回一个错误。
MSET key1 value1 key2 value2 .. keyN valueN 批量添加多个String类型的键值对
MGET key1 key2 .. keyN 根据多个key获取多个String类型的value
INCR key 让一个整型的key自增1
INCRBY key incr_amount 让一个整型的key自增并指定步长,例如:incrby num 2 让num值自增2
INCRBYFLOAT key incr_amount 让一个浮点类型的数字自增并指定步长
SETNX key value 添加一个String类型的键值对,前提是这个key不存在,否则不执行
SETEX key timeout value 添加一个String类型的键值对,并且指定有效期

2.2.2 Key结构

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

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

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

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

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

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

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

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

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

如果Value是一个Java对象,例如一个User对象,则可以将对象序列化为JSON字符串后存储:

KEY VALUE
heima:user:1 {“id”:1, “name”: “Jack”, “age”: 21}
heima:product:1 {“id”:1, “name”: “小米11”, “price”: 4999}
1
2
set heima:user:1 '{"id":1,  "name": "Jack", "age": 21}'
set heima:product:1 '{"id":1, "name": "小米11", "price": 4999}'

PS:注意JSON字符串外面包裹一个' '

并且,在redis的桌面客户端中,还会以相同前缀作为层级结构,让数据看起来层次分明,关系清晰:

redis客户端相同前缀作为层级结构

2.3 Hash类型

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

String结构是将对象序列化为JSON字符串后存储,当需要修改对象某个字段时很不方便:

对象序列化为JSON字符串后存储

Hash结构可以将对象中的每个字段独立存储,可以针对单个字段做CRUD:

对象中每个字段独立存储

Hash的常见命令有:

命令 描述
HSET key field value 添加或者修改hash类型key的field的值
HGET key field 获取一个hash类型key的field的值
HMSET key fifld1 value1 …fifldN valueN 批量添加多个hash类型key的field的值
HMGET key fifld1…fifldN 批量获取多个hash类型key的field的值
HGETALL key 获取一个hash类型的key中的所有的field和value
HKEYS key fifld incr_by_number 获取一个hash类型的key中的所有的field
HINCRBY key fifld incr_by_number 让一个hash类型key的字段值自增并指定步长
HSETNX key fifld value 添加一个hash类型的key的field值,前提是这个field不存在,否则不执行

2.4 List类型

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

特征也与LinkedList类似:

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

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

List的常见命令:

命令 描述
LPUSH key value1.. valueN 向列表左侧插入一个或多个元素
LPOP key 移除并返回列表左侧的第一个元素,没有则返回 nil
LPUSH key value1.. valueN 向列表右侧插入一个或多个元素
RPOP key 移除并返回列表右侧的第一个元素
LRANGE key star end 返回一段角标范围内的所有元素
BLPOP/BRPOP list1 list2 .. listN timeout 与LPOP和RPOP类似,只不过在没有元素时等待指定时间,而不是直接返回 nil

2.5 Set类型

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

  • 无序

  • 元素不可重复

  • 查找快

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

Set的常见命令:

命令 描述
SADD key value1 .. valueN 向set中添加一个或多个元素
SREM key member1 .. memberN 移除set中的指定元素
SCARD key 返回set中元素的个数
SISMEMBER key value 判断一个元素是否存在于set中
SMEMBERS key value 获取set中的所有元素
SINTER key key1 .. keyN 求key1与key2的交集

例如两个集合:s1和s2

两个集合s1和s2

求交集:SINTER s1 s2,求s1与s2的不同:SDIFF s1 s2

s1和s2关系

2.6 SortedSet类型

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

SortedSet具备下列特性:

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

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

SortedSet的常见命令有:

命令 描述
ZADD key score1 value1 .. scoreN valueN 添加一个或多个元素到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 start stop 按照score排序后,获取指定排名范围内的元素
ZRANGEBYSCORE key min max 按照score排序后,获取指定score范围内的元素
ZDIFFZINTERZUNION 求差集、交集、并集

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

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

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

3. Redis的Java客户端

前面我们讲解了redis的常用命令,这些命令是我们操作redis的基础,那么我们在java程序中应该如何操作redis呢?这就需要使用redis的Java客户端,就如同我们使用JDBC操作MySQL数据库一样。在redis官网中提供了各种语言的客户端,地址:https://redis.io/docs/clients/

对于Java客户端而言,目前官方推荐两种方式:

  • Jedis, for synchronous applications.
  • Lettuce, for asynchronous and reactive applications.

Jedis和Lettuce这两个主要是提供了redis命令对应的API,方便我们操作redis,而SpringDataRedis又对这两种做了抽象和封装,因此我们后期会直接以SpringDataRedis来学习。

3.1 Jedis客户端

Jedis的官网地址: https://github.com/redis/jedis

3.1.1 快速入门

1、引入依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!--jedis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>5.0.0</version>
</dependency>
<!-- 单元测试 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.10.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.88.132", 6379);
//jedis = JedisConnectionFactory.getJedis();
// 2.设置密码
jedis.auth("123456");
// 3.选择库,默认0
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); // result = OK
// 获取数据
String name = jedis.get("name");
System.out.println("name = " + 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); // {name=Jack, age=21}
}

4、释放资源

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

3.1.2 连接池

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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.88.132", 6379, 1000, "123456");
}

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

3.2 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封装到了不同的类型中:

RedisTemplate封装的操作

3.2.1 快速入门

SpringBoot已经提供了对SpringDataRedis的支持,使用非常简单。PS:这里使用的是SpringBoot的3.2.5版本。

首先,新建一个SpringBoot项目,勾选Spring Data Redis(Access + Driver)和 Lambook依赖,然后按照下面步骤执行:

1、引入依赖

1
2
3
4
5
6
7
8
9
10
11
<!--redis依赖-->
<!--勾选Spring Data Redis(Access + Driver)自动引入-->
<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>

2、配置redis

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

PS:SpringDataRedis默认引入的是 lettuce,必须要手动配置lettuce的 pool 连接池才会生效。

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
16
17
@Test
void testString() {
// 写入一条String数据
redisTemplate.opsForValue().set("name", "木又枯了");
// 获取string数据
Object name = redisTemplate.opsForValue().get("name");
System.out.println("name = " + name); // name = 木又枯了
}

@Test
void testSaveUser() {
// 写入数据
redisTemplate.opsForValue().set("user:100", new User("木又枯了", 18));
//获取数据
User user = (User) redisTemplate.opsForValue().get("user:100");
System.out.println("user = " + user); // user = User(name=木又枯了, age=18)
}

3.2.2 自定义序列化

RedisTemplate可以接收任意Object作为值写入redis:

接收任意Object作为值

只不过写入前会把Object序列化为字节形式,默认是采用JDK序列化工具ObjectOutputStream,得到的结果是这样的:

Object序列化为字节形式

缺点:

  • 可读性差
  • 内存占用较大

我们可以 自定义RedisTemplate 的序列化方式:

1、引入依赖:

1
2
3
4
5
<!--Jackson依赖-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>

2、编写RedisConfig:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
// 创建RedisTemplate对象
RedisTemplate<String, Object> template = new RedisTemplate<>();
// 设置连接工厂
template.setConnectionFactory(connectionFactory);
// 设置Key的序列化
template.setKeySerializer(RedisSerializer.string());
template.setHashKeySerializer(RedisSerializer.string());
// 设置Value的序列化
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
// 返回
return template;
}
}

这里采用了JSON序列化来代替默认的JDK序列化方式。

3、运行测试

写入name,最终结果如图:

JSON序列化name

写入一个对象,最终结果如图:

JSON序列化对象

整体可读性有了很大提升,并且能将Java对象自动的序列化为JSON字符串,并且查询时能自动把JSON反序列化为Java对象。不过,其中记录了序列化时对应的class名称,目的是为了查询时实现自动反序列化。这会带来额外的内存开销。

3.2.3 StringRedisTemplate

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

手动完成对象的序列化和反序列化

因为存入和读取时的序列化及反序列化都是我们自己实现的,SpringDataRedis就不会将class信息写入Redis了。

这种用法比较普遍,因此SpringDataRedis就提供了RedisTemplate的子类:StringRedisTemplate,它的key和value的序列化方式默认就是 String 方式,源码如下:

1
public class StringRedisTemplate extends RedisTemplate<String, String> {}

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

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
@SpringBootTest
class RedisStringTests {

@Autowired
private StringRedisTemplate stringRedisTemplate;

// JSON序列化工具
private static final ObjectMapper mapper = new ObjectMapper();

@Test
void testSaveUser() throws JsonProcessingException {
// 创建对象
User user = new User("木又枯了", 18);
// 手动序列化
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); // user1 = User(name=木又枯了, age=18)
}

}

写入一个对象,最终结果如图:

自定义RedisTemplate序列化结果

使用fastjson2实现序列化与反序列化

1
2
3
4
5
6
<!-- fastjson2 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.49</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
void testSaveUser() {
// 创建对象
User user = new User("木又枯了", 18);
// 手动序列化
String json = JSON.toJSONString(user);
// 写入数据
stringRedisTemplate.opsForValue().set("user:200", json);

// 获取数据
String jsonUser = stringRedisTemplate.opsForValue().get("user:200");
// 手动反序列化
User user1 = JSON.parseObject(jsonUser, User.class);
System.out.println("user1 = " + user1); // user1 = User(name=木又枯了, age=18)
}