返回

Redis基础知识

参考:

Redis基础

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

Redis 是一个开源的使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可基于内存、分布式、可选持久性的键值对(Key-Value)存储数据库,并提供多种语言的 API。

Redis 通常被称为数据结构服务器,因为值(value)可以是字符串(String)、哈希(Hash)、列表(list)、集合(sets)和有序集合(sorted sets)等类型。

  • Redis 官网:https://redis.io/
  • 源码地址:https://github.com/redis/redis

Redis简介

  • Redis(Remote Dictionary Server)是一个开源的内存数据库,遵守 BSD 协议,它提供了一个高性能的键值(key-value)存储系统,常用于缓存、消息队列、会话存储等应用场景。
    • **性能极高:**Redis 以其极高的性能而著称,能够支持每秒数十万次的读写操作。这使得Redis成为处理高并发请求的理想选择,尤其是在需要快速响应的场景中,如缓存、会话管理、排行榜等。
    • **丰富的数据类型:**Redis 不仅支持基本的键值存储,还提供了丰富的数据类型,包括字符串、列表、集合、哈希表、有序集合等。这些数据类型为开发者提供了灵活的数据操作能力,使得Redis可以适应各种不同的应用场景。
    • **原子性操作:**Redis 的所有操作都是原子性的,这意味着操作要么完全执行,要么完全不执行。这种特性对于确保数据的一致性和完整性至关重要,尤其是在高并发环境下处理事务时。
    • **持久化:**Redis 支持数据的持久化,可以将内存中的数据保存到磁盘中,以便在系统重启后恢复数据。这为 Redis 提供了数据安全性,确保数据不会因为系统故障而丢失。
    • **支持发布/订阅模式:**Redis 内置了发布/订阅模式(Pub/Sub),允许客户端之间通过消息传递进行通信。这使得 Redis 可以作为消息队列和实时数据传输的平台。
    • **单线程模型:**尽管 Redis 是单线程的,但它通过高效的事件驱动模型来处理并发请求,确保了高性能和低延迟。单线程模型也简化了并发控制的复杂性。
    • **主从复制:**Redis 支持主从复制,可以通过从节点来备份数据或分担读请求,提高数据的可用性和系统的伸缩性。
    • **应用场景广泛:**Redis 被广泛应用于各种场景,包括但不限于缓存系统、会话存储、排行榜、实时分析、地理空间数据索引等。
    • **社区支持:**Redis 拥有一个活跃的开发者社区,提供了大量的文档、教程和第三方库,这为开发者提供了强大的支持和丰富的资源。
    • **跨平台兼容性:**Redis 可以在多种操作系统上运行,包括 Linux、macOS 和 Windows,这使得它能够在不同的技术栈中灵活部署。
    • **支持 Lua 脚本:**Redis 支持使用 Lua 脚本来编写复杂的操作,这些脚本可以在服务器端执行,提供了更多的灵活性和强大的功能。

Redis安装

Windows 下安装

下载地址

运行下面的命令运行redis服务端:

1
redis-server.exe redis.windows.conf

输入之后,会显示如下界面:

Redis 安装

Linux 源码安装

下载地址,下载最新稳定版本

建议下载到 /usr/local/src 目录编译安装:

1
2
3
4
wget http://download.redis.io/releases/redis-6.2.6.tar.gz
tar -xzf redis-6.2.6.tar.gz
cd redis-6.2.6
make && make install

如果没有出错,应该就安装成功了。默认的安装路径是在 /usr/local/bin目录下,该目录已经默认配置到环境变量,因此可以在任意目录下运行这些命令。其中:

  • redis-cli:是redis提供的命令行客户端
  • redis-server:是redis的服务端启动脚本
  • redis-sentinel:是redis的哨兵启动脚本

redis的启动方式有三种:默认启动、指定配置启动、开机自启

  1. 默认启动:安装完成后,在任意目录输入redis-server命令即可启动Redis
1
redis-server

这种启动属于前台启动,会阻塞整个会话窗口,窗口关闭或者按下CTRL + C则Redis停止。不推荐使用。

  1. 指定配置启动:如果要让Redis以后台方式启动,则必须修改Redis配置文件,就在之前解压的redis源码目录下(/usr/local/src/redis-6.2.6),名字叫redis.conf,修改redis.conf文件中的一些配置
1
2
3
4
5
6
7
8
# 将该配置文件备份一份
cp redis.conf redis.conf.bck
# 允许访问的地址,默认是127.0.0.1,会导致只能在本地访问。修改为0.0.0.0则可以在任意IP访问,生产环境不要设置为0.0.0.0
bind 0.0.0.0
# 守护进程,修改为yes后即可后台运行
daemonize yes 
# 密码,设置后访问Redis必须输入密码
requirepass 123321

Redis的其它常见配置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 监听的端口
port 6379
# 工作目录,默认是当前目录,也就是运行redis-server命令时的目录,日志、持久化等文件会保存在这个目录
dir .
# 数据库数量,设置为1,代表只使用1个库,默认有16个库,编号0~15
databases 15
# 设置redis能够使用的最大内存
maxmemory 512mb
# 日志文件,默认为空,不记录日志,可以指定日志文件名
logfile "redis.log"

启动Redis:

1
2
3
4
# 进入redis安装目录 
cd /usr/local/src/redis-6.2.6
# 启动
redis-server redis.conf

停止服务:

1
2
3
# 利用redis-cli来执行 shutdown 命令,即可停止 Redis 服务,
# 因为之前配置了密码,因此需要通过 -u 来指定密码
redis-cli -u 123321 shutdown
  1. 开机自启

新建一个系统服务文件:

1
vi /etc/systemd/system/redis.service

内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
[Unit]
Description=redis-server
After=network.target

[Service]
Type=forking
ExecStart=/usr/local/bin/redis-server /usr/local/src/redis-6.2.6/redis.conf
PrivateTmp=true

[Install]
WantedBy=multi-user.target

然后重载系统服务:

1
systemctl daemon-reload

现在,可以用下面这组命令来操作redis服务:

1
2
3
4
5
6
7
8
# 启动
systemctl start redis
# 停止
systemctl stop redis
# 重启
systemctl restart redis
# 查看状态
systemctl status redis

执行下面的命令,可以让redis开机自启:

1
systemctl enable redis

Redis客户端

Redis客户端,包括:命令行客户端、图形化桌面客户端、各编程语言客户端

  1. Redis命令行客户端:Redis安装完成后就自带了命令行客户端:redis-cli,使用方式如下:
1
redis-cli [options] [commonds]

其中常见的[options]有:

  • -h 127.0.0.1:指定要连接的redis节点的IP地址,默认是127.0.0.1
  • -p 6379:指定要连接的redis节点的端口,默认是6379
  • -a 123321:指定redis的访问密码

其中的[commonds]就是Redis的操作命令,例如:

  • ping:与redis服务端做心跳测试,服务端正常会返回pong

不指定[commonds]时,会进入redis-cli的交互控制台

  1. 图形化桌面客户端:GitHub上的大神编写了Redis的图形化桌面客户端,不过该仓库提供的是RedisDesktopManager的源码,并未提供windows安装包。在这个仓库可以找到安装包
  2. 各编程语言客户端:Golang Redis client

Redis默认有16个仓库,编号从0至15. 通过配置文件可以设置仓库数量,但是不超过16,并且不能自定义仓库名称。如果是基于redis-cli连接Redis服务,可以通过select命令来选择数据库:

1
2
# 选择 0号库
select 0

Redis命令

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

Redis官网将操作不同数据类型的命令进行了分组,可以查看到不同数据类型的命令:Commands | Docs

也可以在命令行中获取命令提示

1
2
3
4
5
help
# 可以按组查看不同数据类型的命令
help@
# 查看某个具体命令的用法
help [command]

通用命令

通用命令所有数据类型都能使用

  1. Keys 命令用于查找所有符合给定模式 pattern 的 key
1
KEYS pattern
  • *代表任意个,?代表任意一个
  • 模糊查询效率低,又由于Redis是单线程,容易阻塞,所以不建议在生产环境下使用
  1. DEL:用于在 key 存在时删除 key
1
DEL key [key ...]
  1. EXISTS:检查给定 key 是否存在
1
EXISTS key [key ...]
  1. EXPIRE:给目标 key 设置过期时间,以秒计
1
EXPIRE key seconds
  1. TTL :以秒为单位,返回目标 key 的剩余生存时间(TTL, time to live)
1
TTL key
  • 返回值:-1永久有效;-2已过期

String 类型

String类型,也就是字符串类型,是Redis中最简单的存储类型。根据字符串的格式不同,又可以分为3类:

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

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

  1. 设置或修改
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 1.添加或修改指定 key 的值
SET key value
# 2.同时设置一个或多个 key-value 对
MSET key value [key value ...]
# 3.只有在 key 不存在时设置 key 的值,否则不执行
SETNX key value
# 也可以在 SET 的时候加 NX,起到和 SETNX 同样效果
SET key value nx
# 4.添加或修改指定 key 的值,并且设置过期时间,以秒计
SETEX key seconds value
# 也可以在 set 的时候加`EX`,起到和`SETEX`同样效果
set key value ex seconds
  1. 获取
1
2
3
4
# 1.获取指定 key 的值
GET key
# 2.根据多个key获取多个String类型的value
MGET key1 [key2..]
  1. 自增或自减
1
2
3
4
5
6
# 1.将 key 中储存的数字值增一
INCR key
# 2.将 key 所储存的值加上给定的增量值(increment)
INCRBY key increment
# 3.将 key 所储存的值加上给定的浮点增量值(increment)
INCRBYFLOAT key increment

Key 的结构

Redis没有类似MySQL中的Table的概念,该如何区分不同类型的key。例如,需要存储用户、商品信息到redis,有一个用户id是1,有一个商品id恰好也是1

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

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

这个格式并非固定,根据自己的需求来删除或添加词条。

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

  • user相关的keyproject1:user:1
  • product相关的keyproject1:product:1

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

Hash 类型

Redis hash 是一个 string 类型的 field(字段) 和 value(值) 的映射表,hash 特别适合用于存储对象

Redis 中每个 hash 可以存储 232 - 1 键值对(40多亿)

  1. 设置或修改
1
2
3
4
5
6
7
8
# 1.添加或者修改hash类型key的field的值
HSET key field value
# 2.批量添加多个hash类型key的field的值
HMSET key field1 value1 [field2 value2 ...]
# 3.添加一个hash类型的key的field值,前提是这个field不存在,否则不执行
HSETNX key field value
# 也可以在 SET 的时候加 NX,起到和 SETNX 同样效果
HSET key field value nx
  1. 获取
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 1.获取一个hash类型key的field的值
HGET key field
# 2.批量获取多个hash类型key的field的值
HMGET key field1 [field2]
# 3.获取一个hash类型的key中的所有的field和value
HGETALL key
# 4.获取一个hash类型的key中的所有的field
HKEYS key
# 5.获取一个hash类型的key中的所有的value
HVALS key
  1. 自增或自减
1
2
3
4
# 1.为哈希表 key 中的指定字段的整数值加上增量(increment)
HINCRBY key field increment
# 2.为哈希表 key 中的指定字段的浮点数值加上增量(increment)
HINCRBYFLOAT key field increment

List 类型

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

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

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

  1. 添加
1
2
3
4
# 1.向列表左侧插入一个或多个元素
LPUSH key value1 [value2]
# 2.向列表右侧插入一个或多个元素
RPUSH key value1 [value2]
  1. 获取或删除
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 1.移除并返回列表左侧的第一个元素,没有则返回nil
LPOP key
# 2.移除并返回列表右侧的第一个元素
RPOP key
# 3.返回一段指定范围内的所有元素(从0开始)(左闭右闭)
LRANGE key start stop
# 4.移除并获取列表左侧的第一个元素,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
BLPOP key1 [key2 ] seconds
# 移除并获取列表右侧的第一个元素,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
BRPOP key1 [key2 ] seconds

用List结构模拟栈:入口和出口在同一边

用List结构模拟队列:入口和出口在不同边

用List结构模拟阻塞队列:出队时采用BLPOP或BRPOP

Set 类型

Redis 的 Set 是 String 类型的无序集合。Set 成员是唯一的,这就意味着 Set 中不能出现重复的数据。Set 对象的编码可以是 intset 或者 hashtable。Redis 中 Set 是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。

  • 无序
  • 元素不可重复
  • 查找快
  • 支持交集、并集、差集等功能
  1. 添加
1
2
# 向set中添加一个或多个元素
SADD key member1 [member2]
  1. 获取或删除
1
2
3
4
5
6
7
8
# 1.移除set中的指定元素
SREM key member1 [member2]
# 2.返回set中元素的个数
SCARD key
# 3.判断 member 元素是否是集合 key 的成员
SISMEMBER key member
# 4.获取set中的所有元素
SMEMBERS key
  1. 集合间运算
1
2
3
4
5
6
# 1.求key1与key2的交集
SINTER key1 [key2]
# 2.求key1与key2的差集(key1有,key2没有)
SDIFF key1 [key2]
# 3.求key1和key2的并集 
SUNION key1 [key2]

SortedSet 类型

Redis 的 SortedSet 是一个可排序的 set 集合。SortedSet 中的每一个元素都带有一个 double 类型 score 属性,可以基于 score 属性为集合中的成员进行从小到大的排序,底层的实现是一个跳表(SkipList)加 hash表。有序集合的成员是唯一的,但分数(score)却可以重复。

SortedSet 具备下列特性:

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

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

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

  1. 添加或修改
1
2
3
4
# 1.向有序集合添加一个或多个成员,如果已经存在则更新其score值
ZADD key score1 member1 [score2 member2]
# 2.让sorted set中的指定元素自增,步长为指定的increment值
ZINCRBY key increment member
  1. 删除
1
2
# 1.移除有序集合中的一个或多个成员
ZREM key member [member ...]
  1. 获取
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 1.获取sorted set中指定元素的score值
ZSCORE key member
# 2.获取sorted set中指定元素的排名(从0开始计)
ZRANK key member
# 3.获取sorted set中的元素个数
ZCARD key
# 4.获取score值在给定范围内的元素的个数
ZCOUNT key min max
# 5.按照score排序后,获取指定排名范围内的元素
ZRANGE key start stop
# 6.按照score排序后,获取指定score范围内的元素
ZRANGEBYSCORE key min max
  1. 集合运算
1
2
# 求差集、交集、并集
ZDIFF、ZINTER、ZUNION

Redis客户端

在Redis官网中提供了各种语言的客户端

Go

Redis的Go客户端go-redis ,提供了各种类型的客户端:

Java

Redis的Java客户端

  1. Jedis:以Redis命令作为方法名称,学习成本低,简单实用。但是Jedis实例是线程不安全的,并且频繁的创建和销毁连接会有性能损耗,多线程环境下需要基于连接池来使用
  2. Lettuce:Lettuce是基于Netty实现的,支持同步、异步和响应式编程方式,并且是线程安全的。支持Redis的哨兵模式、集群模式和管道模式。

SpringDataRedis

SpringData是Spring中数据操作的模块,包含对各种数据库的集成,其中对Redis的集成模块就叫做SpringDataRedis

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

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

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

SpringDataRedis的序列化方式

RedisTemplate可以接收任意Object作为值写入Redis,只不过写入前会把Object序列化为字节形式,默认是采用JDK序列化,得到的结果可读性差并且内存占用较大

Spring默认提供了一个StringRedisTemplate类,它的keyvalue的序列化方式默认就是String方式。省去了自定义RedisTemplate的过程

应用场景

短信登陆

查询缓存

缓存更新策略

当数据库中数据更新后,缓存中数据未更新,这就会发生缓存一致性问题。缓存更新策略三种方式:

内存淘汰 超时剔除 主动更新
说明 不用自己维护,利用Redis的内存淘汰机制,当内存不足时自动淘汰部分数据。下次查询时更新缓存 给缓存数据添加TTL时间,到期后自动删除缓存。下次查询时更新缓存 编写业务逻辑,在修改数据库的同时,更新缓存
一致性 一般
维护成本
  • 低一致性需求:使用内存淘汰机制。例如店铺类型的查询缓存
  • 高一致性需求:主动更新,并以超时剔除作为兜底方案。例如店铺详情查询的缓存

主动更新策略的具体业务实现有三种方式:

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

旁路缓存模式

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

1.删除缓存还是更新缓存?

  • 更新缓存:每次更新数据库都更新缓存,无效写操作较多❌
  • 删除缓存:更新数据库时让缓存失效,查询时再更新缓存✔

2.如何保证缓存与数据库的操作的同时成功或失败?

  • 单体系统,将缓存与数据库操作放在一个事务
  • 分布式系统,利用TCC(Try-Confirm-Cancel)等分布式事务方案

3.先操作缓存还是先操作数据库?(线程安全问题

  • 先操作数据库,再删除缓存

旁路缓存模式的特殊情况分析

在并发未加锁的情况下

  • 若先删缓存,再更新数据库,大概率会出现数据不一致情况。例如线程A删除缓存后更新数据库之前(硬盘比内存慢,写比查慢),线程B查询该数据,由于缓存被线程A删除,所以未命中,线程B会查询数据库,由于线程A对数据库的更新未完成,所以线程B会得到旧值,旧值会回写到缓存。最后线程A更新完数据库,此时缓存中是旧值,数据库中是新值。
  • 若先操作数据库,再更新缓存,极小概率会出现数据不一致情况。因为需要满足情况一:线程A查询缓存时,缓存恰好过期失效;情况二:必须在线程A查询数据库与写缓存的timing之间完成线程B的更新数据库和删除缓存操作
    • 由于内存比硬盘快,所以情况二不易发生

总结

缓存更新策略的最佳实践方案:

低一致性需求:使用Redis自带的内存淘汰机制

高一致性需求:主动更新,并以超时剔除作为兜底方案

  • 读操作:
    • 缓存命中则直接返回
    • 缓存未命中则查询数据库,并写入缓存,设定超时时间作为兜底方案
  • 写操作:
    • 先写数据库,然后再删除缓存
    • 要确保数据库与缓存操作的原子性

缓存穿透

缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。

常见的解决方案有两种:

  1. 缓存空对象
    • 优点:实现简单、维护方便
    • 缺点:额外的内存消耗、可能造成短期的不一致
  2. 布隆过滤
    • 优点:内存占用较少,没有多余key
    • 缺点:实现复杂、存在误判可能

缓存雪崩

缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。

解决方案:

  • 给不同的Key的TTL添加随机值
  • 利用Redis集群提高服务的可用性
  • 给缓存业务添加降级限流策略
  • 给业务添加多级缓存

缓存击穿

缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。

常见的解决方案有两种:

  • 互斥锁
  • 逻辑过期

解决方案 优点 缺点
互斥锁 没有额外的内存消耗、保证一致性、实现简单 线程需要等待、性能受影响、可能有死锁风险
逻辑过期 线程无需等待、性能较好 不保证一致性、有额外内存消耗、实现复杂

基于互斥锁方式解决缓存击穿问题

基于逻辑过期方式解决缓存击穿问题

最后更新于 Nov 28, 2024 22:23 UTC
Built with Hugo
Theme Stack designed by Jimmy