参考:
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服务端:
|
|
输入之后,会显示如下界面:
Linux 源码安装
下载地址,下载最新稳定版本
建议下载到 /usr/local/src
目录编译安装:
|
|
如果没有出错,应该就安装成功了。默认的安装路径是在 /usr/local/bin
目录下,该目录已经默认配置到环境变量,因此可以在任意目录下运行这些命令。其中:
- redis-cli:是redis提供的命令行客户端
- redis-server:是redis的服务端启动脚本
- redis-sentinel:是redis的哨兵启动脚本
redis的启动方式有三种:默认启动、指定配置启动、开机自启
- 默认启动:安装完成后,在任意目录输入redis-server命令即可启动Redis
|
|
这种启动属于前台启动,会阻塞整个会话窗口,窗口关闭或者按下CTRL + C
则Redis停止。不推荐使用。
- 指定配置启动:如果要让Redis以后台方式启动,则必须修改Redis配置文件,就在之前解压的redis源码目录下(
/usr/local/src/redis-6.2.6
),名字叫redis.conf
,修改redis.conf
文件中的一些配置
|
|
Redis的其它常见配置:
|
|
启动Redis:
|
|
停止服务:
|
|
- 开机自启
新建一个系统服务文件:
|
|
内容如下:
|
|
然后重载系统服务:
|
|
现在,可以用下面这组命令来操作redis服务:
|
|
执行下面的命令,可以让redis开机自启:
|
|
Redis客户端
Redis客户端,包括:命令行客户端、图形化桌面客户端、各编程语言客户端
- Redis命令行客户端:Redis安装完成后就自带了命令行客户端:redis-cli,使用方式如下:
|
|
其中常见的[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
的交互控制台
- 图形化桌面客户端:GitHub上的大神编写了Redis的图形化桌面客户端,不过该仓库提供的是RedisDesktopManager的源码,并未提供windows安装包。在这个仓库可以找到安装包
- 各编程语言客户端:Golang Redis client
Redis默认有16个仓库,编号从0至15. 通过配置文件可以设置仓库数量,但是不超过16,并且不能自定义仓库名称。如果是基于redis-cli连接Redis服务,可以通过select命令来选择数据库:
|
|
Redis命令
Redis是一个key-value的数据库,key一般是String类型,不过value的类型多种多样:
Redis官网将操作不同数据类型的命令进行了分组,可以查看到不同数据类型的命令:Commands | Docs
也可以在命令行中获取命令提示
|
|
通用命令
通用命令所有数据类型都能使用
Keys
命令用于查找所有符合给定模式 pattern 的 key
|
|
*
代表任意个,?
代表任意一个- 模糊查询效率低,又由于Redis是单线程,容易阻塞,所以不建议在生产环境下使用
DEL
:用于在 key 存在时删除 key
|
|
EXISTS
:检查给定 key 是否存在
|
|
EXPIRE
:给目标 key 设置过期时间,以秒计
|
|
TTL
:以秒为单位,返回目标 key 的剩余生存时间(TTL, time to live)
|
|
- 返回值:
-1
永久有效;-2
已过期
String 类型
String类型,也就是字符串类型,是Redis中最简单的存储类型。根据字符串的格式不同,又可以分为3类:
- string:普通字符串
- int:整数类型,可以做自增、自减操作
- float:浮点类型,可以做自增、自减操作
不管是哪种格式,底层都是字节数组形式存储,只不过是编码方式不同。字符串类型的最大空间不能超过512MB
- 设置或修改
|
|
- 获取
|
|
- 自增或自减
|
|
Key 的结构
Redis没有类似MySQL中的Table的概念,该如何区分不同类型的key。例如,需要存储用户、商品信息到redis,有一个用户id是1,有一个商品id恰好也是1
Redis的key允许有多个单词形成层级结构,多个单词之间用:
隔开,格式如下:
|
|
这个格式并非固定,根据自己的需求来删除或添加词条。
例如的项目名称叫 project1
,有user
和product
两种不同类型的数据,可以这样定义key:
user
相关的key
:project1:user:1
product
相关的key
:project1:product:1
如果Value是一个对象,例如一个User对象,则可以将对象序列化为JSON
字符串后存储:
Hash 类型
Redis hash 是一个 string 类型的 field(字段) 和 value(值) 的映射表,hash 特别适合用于存储对象
Redis 中每个 hash 可以存储 232 - 1 键值对(40多亿)
- 设置或修改
|
|
- 获取
|
|
- 自增或自减
|
|
List 类型
Redis中的List类型可以看做是一个双向链表结构,既支持正向检索也支持反向检索。
- 有序
- 元素可以重复
- 插入和删除快
- 查询速度一般
常用来存储一个有序数据,例如:朋友圈点赞列表,评论列表等。
- 添加
|
|
- 获取或删除
|
|
用List结构模拟栈:入口和出口在同一边
用List结构模拟队列:入口和出口在不同边
用List结构模拟阻塞队列:出队时采用BLPOP或BRPOP
Set 类型
Redis 的 Set 是 String 类型的无序集合。Set 成员是唯一的,这就意味着 Set 中不能出现重复的数据。Set 对象的编码可以是 intset 或者 hashtable。Redis 中 Set 是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。
- 无序
- 元素不可重复
- 查找快
- 支持交集、并集、差集等功能
- 添加
|
|
- 获取或删除
|
|
- 集合间运算
|
|
SortedSet 类型
Redis 的 SortedSet 是一个可排序的 set 集合。SortedSet 中的每一个元素都带有一个 double 类型 score 属性,可以基于 score 属性为集合中的成员进行从小到大的排序,底层的实现是一个跳表(SkipList)加 hash表。有序集合的成员是唯一的,但分数(score)却可以重复。
SortedSet 具备下列特性:
- 可排序
- 元素不重复
- 查询速度快
因为 SortedSet 的可排序特性,经常被用来实现排行榜这样的功能。
注意:所有的排名默认都是升序,如果要降序则在命令的Z
后面添加REV
即可
- 添加或修改
|
|
- 删除
|
|
- 获取
|
|
- 集合运算
|
|
Redis客户端
在Redis官网中提供了各种语言的客户端
Go
Redis的Go客户端:go-redis ,提供了各种类型的客户端:
Java
Redis的Java客户端
- Jedis:以Redis命令作为方法名称,学习成本低,简单实用。但是Jedis实例是线程不安全的,并且频繁的创建和销毁连接会有性能损耗,多线程环境下需要基于连接池来使用
- 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
类,它的key
和value
的序列化方式默认就是String
方式。省去了自定义RedisTemplate
的过程
应用场景
短信登陆
查询缓存
缓存更新策略
当数据库中数据更新后,缓存中数据未更新,这就会发生缓存一致性问题。缓存更新策略三种方式:
内存淘汰 | 超时剔除 | 主动更新 | |
---|---|---|---|
说明 | 不用自己维护,利用Redis的内存淘汰机制,当内存不足时自动淘汰部分数据。下次查询时更新缓存 | 给缓存数据添加TTL时间,到期后自动删除缓存。下次查询时更新缓存 | 编写业务逻辑,在修改数据库的同时,更新缓存 |
一致性 | 差 | 一般 | 好 |
维护成本 | 无 | 低 | 高 |
- 低一致性需求:使用内存淘汰机制。例如店铺类型的查询缓存
- 高一致性需求:主动更新,并以超时剔除作为兜底方案。例如店铺详情查询的缓存
主动更新策略的具体业务实现有三种方式:
- 旁路缓存模式(Cache Aside Pattern):由缓存的调用者,在更新数据库的同时更新缓存
- 读写穿透模式(Read/Write Through Pattern):缓存与数据库整合为一个服务,由服务来维护一致性。调用者调用该服务,无需关心缓存一致性问题
- 异步写后缓存模式(Write Behind Caching Pattern):调用者只操作缓存,由其它线程异步的将缓存数据持久化到数据库,保证最终一致。
旁路缓存模式
操作缓存和数据库时有三个问题需要考虑:
1.删除缓存还是更新缓存?
- 更新缓存:每次更新数据库都更新缓存,无效写操作较多❌
- 删除缓存:更新数据库时让缓存失效,查询时再更新缓存✔
2.如何保证缓存与数据库的操作的同时成功或失败?
- 单体系统,将缓存与数据库操作放在一个事务
- 分布式系统,利用TCC(Try-Confirm-Cancel)等分布式事务方案
3.先操作缓存还是先操作数据库?(线程安全问题)
- 先操作数据库,再删除缓存
旁路缓存模式的特殊情况分析
在并发未加锁的情况下
- 若先删缓存,再更新数据库,大概率会出现数据不一致情况。例如线程A删除缓存后更新数据库之前(硬盘比内存慢,写比查慢),线程B查询该数据,由于缓存被线程A删除,所以未命中,线程B会查询数据库,由于线程A对数据库的更新未完成,所以线程B会得到旧值,旧值会回写到缓存。最后线程A更新完数据库,此时缓存中是旧值,数据库中是新值。
- 若先操作数据库,再更新缓存,极小概率会出现数据不一致情况。因为需要满足情况一:线程A查询缓存时,缓存恰好过期失效;情况二:必须在线程A查询数据库与写缓存的timing之间完成线程B的更新数据库和删除缓存操作
- 由于内存比硬盘快,所以情况二不易发生
总结
缓存更新策略的最佳实践方案:
低一致性需求:使用Redis自带的内存淘汰机制
高一致性需求:主动更新,并以超时剔除作为兜底方案
- 读操作:
- 缓存命中则直接返回
- 缓存未命中则查询数据库,并写入缓存,设定超时时间作为兜底方案
- 写操作:
- 先写数据库,然后再删除缓存
- 要确保数据库与缓存操作的原子性
缓存穿透
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。
常见的解决方案有两种:
- 缓存空对象
- 优点:实现简单、维护方便
- 缺点:额外的内存消耗、可能造成短期的不一致
- 布隆过滤
- 优点:内存占用较少,没有多余key
- 缺点:实现复杂、存在误判可能
缓存雪崩
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
解决方案:
- 给不同的Key的TTL添加随机值
- 利用Redis集群提高服务的可用性
- 给缓存业务添加降级限流策略
- 给业务添加多级缓存
缓存击穿
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key
突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
常见的解决方案有两种:
- 互斥锁
- 逻辑过期
解决方案 | 优点 | 缺点 |
---|---|---|
互斥锁 | 没有额外的内存消耗、保证一致性、实现简单 | 线程需要等待、性能受影响、可能有死锁风险 |
逻辑过期 | 线程无需等待、性能较好 | 不保证一致性、有额外内存消耗、实现复杂 |
基于互斥锁方式解决缓存击穿问题
基于逻辑过期方式解决缓存击穿问题