Redis
文章目录
1 概述
非关系型NoSQL的键值对数据库
- 基于内存
- 数据结构简单,操作简单
- 单线程,避免上下文切换
- 多路IO复用,非阻塞IO
- 支持发布和订阅pub/sub
redis 🆚 memcache
- memcache没有数据结构, 从其中取数据需要全量取出,然后反序列化后获取数据
- 对于redis来说其提供了许多数据结构,可以将数据的一部分计算和操作交由redis来完成,从而可以减少数据的移动量,提高程序的效率,也体现了计算向数据移动的特点
使用场景:
- 计数器
- 缓存
- 消息队列
- 分布式锁
- UUID
常用命令
> keys * # 查看当前库所有key
> key a* # 查看a开头的所有key
> exists <key> # 判断某个key是否存在
> type <key> # 判断key是什么类型
> del <key> # 删除指定key的数据
> unlink key #根据value选择非阻塞删除
expire key 10 # 为key设定过期时间为10sec
ttl key # 查看还有多少秒过期, -1表示永不过期,-2表示已过期
select 0 # 选择0号数据库
dbsize # 查看当前库的key的数量
flushdb 0 # 清空0号数据库
flushall # 清空全部库
2 五大数据类型
key:
- string
value:
- string
- str
- 数字, 可以进行自增自减操作
- 二进制,可读写二进制数据
- list
- set
- hash
- zset
- 可用于排行榜
- bitmap
- HyperLogLog
- geospatial
2.1 String
字符串类型是二进制安全的,意味着可以存入图片等,value最大为512M
set key value ex time # 设置,ex是过期时间
get key
append key value # 将value最近到原值末尾
strlen key # 获取key的长度
mset key1 value1 key2 value2 ... # 同时设置一个或多个key-value, 原子性,一个失败都失败
mget key1 key2 ... # 同时获取一个或多个value
getrange key 起始位置 结束位置 # 获取范围的值
setrange key 起始位置 value # 用value覆写key所存储的字符串值,从起始位置开始,索引从0开始
setex key 过期时间 value
getset key value # 以新值换旧值
原子操作:不会被线程调度机制打断的操作。这种操作一旦开始,就一致运行到结束,中间不会切换到另一个线程
redis但命令的原子性主要得益于redis的单线程
2.2 List
redis的列表是简单的字符串列表,底层是双向链表。
它采用了quicklist(快速列表)= list + ziplist(压缩表),压缩表是一块联系的内存空间,元素之间数据紧挨着没有空隙
lpush/rpush key value1 value2 value3 .. # 从左边或右边插入一个或多个数据
lpop/rpop key # 从左边或右边吐出一个值, 值在键在,值光键亡
lrange key startindex endindex # 查看数据
lindex key index # 根据索引下标取值
llen key # 获取列表长度
lset key index value # 设定列表中的某个值
2.3 Set
具有去重功能,底层是一个hash表,只不过所有的value都指向同一个内部值。
sadd key value1 value2 ... # 将一个或多个元素加入到集合key中
smembers key # 取出该集合的所有值
sismember key value # 判断某个值是不是在集合中
scard key # 集合中元素个数
spop key # 从集合中随机吐出一个值
2.4 Hash
redis中的hash是一个string类型的field和value的映射表,hash适用于存储对象
类似Java里面的Map<String, Object>
hset key field value
hget key field
hmset key field1 value1 field2 value2 ...
hgetall key # 获取该hash中所有键值对
hexists key field # 判断是否存在field
hkeys key # 列出该hash中的所有field
hvals key # 列出该hash中的所有value
hlen key
hdel key field
2.5 Zset
有序集合,元素唯一,每个元素都有评分,评分不唯一,评分从小到大排序
zadd key score1 value1 score2 value2 # 将一个或多个元素及其评分加入到有序集key中
zrange key start stop [WITHSCORES] # 返回元素,可以设置同时返回score
zrangebyscore key minscore maxscore # 根据评分区间取值
zrevrangebyscore key maxscore minscore # 根据评分区间倒序取值
底层实现是跳跃表,又称跳表
3 发布和订阅
redis发布定于(pub/sub)是一种消息通信模式:发送者pub发送消息,订阅者sub接受消息
redis客户端可以订阅任意数量的频道
subscribe channel1 # 订阅频道1,当有消息加入频道1后能获得相关数据
publish channel1 hello # 向channel1中发布hello
4 过期、淘汰
过期策略
设置Redis中缓存的key的过期时间,如果key过期,如何处理?常见的过期策略有以下三种:
- 定时过期:到了时间就立即移除
- 对内存友好,但会占用大量CPU资源去处理过期数据,从而影响缓存的响应时间和吞吐量
- 惰性过期:只有当访问一个key时,才会判断key是否已过期,过期则清除
- 该策略可以最大化节省CPU资源,
- 可能出现大量过期key没有再次被访问,从而不会被清除,占用大量内存的极端情况
- 定期过期:每隔一段时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key
- 该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果
redis中同时使用了惰性过期和定期过期两者过期策略
淘汰策略
redis内存用完了会发生什么?
- 达到设定上限,redis写命令返回报错信息
- 配置内存淘汰策略
MySQL中有2000w数据,Redis中有20w数据,如何保证redis中的数据都是热点数据?
redis内存数据集达到某定值,需要额外申请内存空间,执行内存淘汰策略。redis中淘汰策略有哪些?
- 全局键空间选择性移除
- noeviction 内存不足时,新写入操作报错,保证数据不会丢失,系统默认方式
- allkeys-lru 在键空间移除最近最少使用的key (常用)
- allkeys-random 随机移除某个key
- 设置过期时间的键空间选择性移除
- volatile-lru 移除最近最少使用的key
- volatile-random 随机移除某个key
- volatile-ttl 移除几个最解决淘汰时间的key
redis内存优化
- 利用号list、set、hash、sorted set等数据类型,很多小的key-value可以用更紧凑的方式存放到一起
- 尽可能使用散列表,因为散列表使用内存非常小,所以尽可能将数据模型抽象到一个散列表中
5 Jedis
Springboot 中提供了StringRedisTemplate 和 RedisTemplate<Object, Object>
StringRedisTemplate ,即RedisTemplate<String, String>, key和value都是String。当需要存储实体类时,需要先转为String,再存入Redis。一般转为Json格式的字符串,所以使用StringRedisTemplate,需要手动将实体类转为Json格式,可以结合fastjson使用
另外常自定义 RedisTemplate<String, Object>
//
6 Redis事务
6.1 redis事务概念
Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化,按顺序执行。事务在执行过程中,不会被其他客户端发送来的命令请求打断。
redis事务分为两个阶段:组队 + 执行
- multi,开始命令组队
- exec,执行事务
- discard,放弃执行
如果组队中某个命令出现错误,执行时整个的所有队列都会被取消
如果组队没错,而执行中某个命令报错,则只有报错的命令不会被执行,而其他命令都会被执行
redis事务的特性
- 单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行过程中,不会被其他客户端发送来地命令请求所打断
- 没有隔离级别概念:队列中地命令没有提交之前都不会实际被执行,因为事务提交前任何命令都不会被实际执行
- 不保证原子性:事务中如果有一条命令执行失败,其后地命令仍然会被执行,没有回滚。
6.2 悲观锁和乐观锁
对一个数据上锁会影响效率,为了提升效率,引入了读锁和写锁。即数据加上写锁后,其他人无法读和写;而加上读锁后,其他人可读不可写,以此来提升效率。
读锁分为悲观锁和乐观锁:
- 悲观锁:在添加读锁的过程中,悲观地认为一定会有人修改数据,所以,每次为数据添加上读锁后,别人不可以写数据
- 乐观锁:在添加读锁的过程中,乐观地任务不会有人修改数据,所以,每次为数据添加上读锁后,别人可以获取写锁,可以修改数据。所以在更新时,会判断一些在此期间别人有没有更新数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。
Redis中使用乐观锁
watch key [key2 ...]
在执行multi之前,先执行watch key 命令,可以监听一个或多个key,如果在事务执行前这个key被其他命令改动,那么事务就被打断了。
6.3 秒杀
商品库存减1,秒杀成功者清单加1
7 持久化Persistence
- RDB:在指定地时间间隔将内存中地数据集快照写入磁盘,恢复时将快照文件直接读取到内存中
- 优点:
- 只有一个dump.rdb文件,方便持久化,且文件可以保存在安全磁盘
- 性能快,fork子进程完成持久化操作,主进程不会进行任何IO操作,保证Redis高性能
- 对于大数据集,比AOF启动效率高
- 缺点:
- 数据安全性低,隔一段时间持久化,如果持久化间Redis发生故障,会有数据丢失问题
- 优点:
- AOF:以日志地形式来记录每个写操作(增量保存),将redis执行过的所有指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据。
- 优点:
- 数据安全
- 通过append方式写文件,即使中途宕机,可以通过
redis-check-aof
命令恢复数据 - rewrite模式会对大文件进行合并重写,删除其中的某些命令
- 缺点:
- AOF文件比RDB文件大,恢复时启动动慢
- 优点:
❓ 如何选用合适的持久化方式
- 建议两者都开启,如果两者都开启,优先使用AOF
- 可以承受数据丢失,可以只使用RDB
- 只希望数据在服务器允许时存在,比如只用作缓存,可以不使用任何持久化方式
8 主从复制
一主多从,主负责写,从负责读,并将数据复制到其他从节点
主机数据更新后根据配置和策略,自动同步到备份机器的master/slaver机制,master以写为主,slaver以读为主
优点:
- 读写分离
- 容灾快速恢复
docker搭建redis主从复制
Docker搭建redis主从复制_霓虹深处-CSDN博客_docker搭建redis主从
docker 配置redis主从,哨兵sentinel - 知乎 (zhihu.com)
特点:
-
当其中一个从服务器挂掉重启后,需要重新添加主服务器配置。在它挂掉期间在主服务器上添加的数据,依然也能在从服务器上看到
-
当主服务器挂掉重器后,依然还是主服务器,还能保有从服务器数据,而从服务器也保有主服务器数据
主从复制过程:
- 当slaver连上master后,从向master发送进行数据同步的消息
- master接到slaver同步数据的消息后,把master的数据进行持久化,生成rdb文件,把rdb文件发送给slaver服务器,slaver服务器拿到rdb进行读取
- 每次master进行写操作后,和slaver进行数据同步(1,2属于全量复制,3属于增量复制)
全量复制:slaver服务器在接收到数据库文件数据后,将其存盘并加载到内存中
增量复制:master将新的所有操作依次传给slaver,完成同步
❓ 如果所有slave节点的数据的复制和同步都由master节点来处理,会给master造成很大的压力,可以使用主-从-从结构来解决
薪火相传:一个slaver可以是下一个slaver的master,slaver同样可以接收其他slaver的连接和同步请求,那么该slaver作为了链条中下一个master,可以有效减轻master的写压力,去中心化降低风险
master–>slaver–>slaver
反客为主: 当一个master宕机后,后面的slaver可以立刻升为master,其后面的slaver不用做任何修改
slave no one # 手动将slaver变为master
哨兵模式:自动完成反客为主, 能够在后台监控主机是否故障,如果故障将根据投票数自动将一个salver变为master
# sentinel.conf# 配置哨兵内容sentinel monitor mymaster 127.0.0.1 6379 1
-
mymaster 为监控对象起的服务器名称
-
1 只有有多少个哨兵统一迁移的数量
# 启动哨兵
9 集群
Redis集群实现了对redis的水平扩容,即启动N个redis节点,将整个数据库分布式存储在者N个节点上,每个节点存储总数为1/N
Redis集群通过分区来提供一定程度的可用性:即使集群中有一部分节点失效或者无法进行通讯,集群也可以继续处理命令请求
基于Docker搭建Redis集群(主从集群) - niceyoo - 博客园 (cnblogs.com)
redis如何分配6个节点?
一个集群中至少有三个主节点, 每个节点一个主一个从
slots 哈希槽
一个redis集群包含16384个插槽,数据库中的每个键都属于这16384个插槽中的一个,集群使用CRC16(key) % 16384
来计算键属于哪个槽,其中CRC(16)key用于计算key的CRC16校验和
集群中每个节点负责处理一部分插槽
读取数据时,当客户端操作的key没有分配在该节点上时,redis会返回转向指令,指向正确的节点
优点:
- 无中心架构,支持动态扩容,对业务透明
- 具备哨兵监控
- 客户端不行连接集群所有节点,连接任何一个节点即可
- 高性能,客户端直连redis服务,免去了proxy代理的损耗
缺点:
- 运维复杂,数据库迁移需要人工干预
- 只能使用0号数据库
- 不支持批量操作
- 分布式逻辑和存储模块耦合
10 缓存
10.1 缓存穿透
key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会到数据源,从而可能压垮数据源。比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。
解决方法:
- 接口增加参数校验,拦截无效请求
- 缓存无效key
- 采用布隆过滤器:将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截,从而避免对底层存储系统的查询压力
布隆过滤器
布隆过滤器的数据结构就是一个很大的位数组和几个不一样的无偏哈希函数(能把元素的哈希值算得比较平均,能让元素被哈希到位数组中的位置比较随机)。
位数组的默认值是0,向布隆过滤器中添加元素时,会使用多个无偏哈希函数对元素进行哈希,每个无偏哈希函数都会得到一些不同的位置。再把位数组的这几个位置都设置为1。
向布隆过滤器查询元素是否存在时,和添加元素一样,也会把哈希的几个位置算出来,然后看看位数组中对应的几个位置是否都为1,只要有一个位为0,那么就说明布隆过滤器里不存在这个元素。如果这几个位置都为1,并不能完全说明这个元素就一定存在其中,有可能这些位置为1是因为其他元素的存在,这就是布隆过滤器会出现误判的原因。
当布隆过滤器说,某种东西存在时,这种东西可能不存在;当布隆过滤器说,某种东西不存在时,那么这种东西一定不存在。
优点
- 比set, map更高效
- 占用空间更少
缺点
- 可能出现误判
使用场景:
- redis缓存穿透
- 黑名单校验
10.2 缓存击穿
key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
解决方式:
- 预先设置热门数据,加大这些热门数据key的时长
- 实时调整,监控热门数据,调整key过期时长
- 使用锁:在缓存失效的时候不是直接去取数据库,而是加排他锁,让其中一个线程从db中获取数据,成功后缓存到redis中,其他线程过一会重试,之后从缓存中取数据
10.3 雪崩击穿
当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如DB)带来很大压力。
解决方式:
- 构建多级缓存架构
- 使用锁或队列,但是效率低,不适用于高并发
- 设置过期标志,记录缓存数据是否过期,如果过期会触发通知另外的线程在后台去更新实际key的缓存
- 将缓存失效时间分散开
11 分布式锁
分布式锁:加锁后,集群中的所有机器都能获得这把锁,对每个节点都有效
常见方案:
- 基于数据库,性能低
- 基于redis,性能高
- 基于zookeeper ,可靠性高
redis中实现分布式锁的方法:
- setnx
- RedLock
setnx
setnx key value EX timeout # 不存在才能设置, 上锁del key # 删除数据, 释放锁
死锁问题
由于不存在才能设置,所以一旦某个线程设置后,集群中其他线程都无法设置,只有等它失效
为了防止锁一直被某个线程占有,可以设定过期时间
有效期导致的安全问题
如果处理耗时操作,需要在锁过期前,修改过期时间,对锁进行“续命”
高并发下造成的无锁问题
场景:如果业务逻辑的执行时间是7s。执行流程如下
-
index1业务逻辑没执行完,3秒后锁被自动释放。
-
index2获取到锁,执行业务逻辑,3秒后锁被自动释放。
-
index3获取到锁,执行业务逻辑
-
index1业务逻辑执行完成,开始调用del释放锁,这时释放的是index3的锁,导致index3的业务只执行1s就被别人释放。
最终等于没锁的情况。
解决:setnx获取锁时,设置一个指定的唯一值(例如:uuid);释放前获取这个值,判断是否自己的锁
改进,使用Lua脚本来保证删除的原子性
分布式锁的特性:
- 互斥性
- 不会发生死锁
- 解铃还须系铃人,加锁和解锁必须是同一客户端
- 加锁与解锁必须具有原子性, 使用Lua脚本进行删除
❓ 这种锁的问题是,如果在master节点上拿到了锁,但是这个锁还没有同步到slaver节点,master就发生了故障,slaver节点升级为master节点,导致锁丢失。因此引入了RedLock
RedLock
2个前提:
- 不再需要部署从库和哨兵实例,只部署主库
- 但主库要部署多个,官方推荐至少5个
也就是说,想使用 Redlock,你至少要部署 5 个 Redis 实例,而且都是主库,它们之间没有任何关系,都是一个个孤立的实例。
注意:不是部署 Redis Cluster,就是部署 5 个简单的 Redis 实例。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8rMzAkzZ-1631544135424)(redlock.jpg)]
Redlock 具体如何使用呢?
整体的流程是这样的,一共分为 5 步:
- 客户端先获取「当前时间戳T1」
- 客户端依次向这 5 个 Redis 实例发起加锁请求(SET 命令),且每个请求会设置超时时间(毫秒级,要远小于锁的有效时间),如果某一个实例加锁失败(包括网络超时、锁被其它人持有等各种异常情况),就立即向下一个 Redis 实例申请加锁
- 如果客户端从 >=3 个(大多数)以上 Redis 实例加锁成功,则再次获取「当前时间戳T2」,如果 T2 - T1 < 锁的过期时间,此时,认为客户端加锁成功,否则认为加锁失败
- 加锁成功,去操作共享资源
- 加锁失败,向「全部节点」发起释放锁请求(Lua 脚本释放锁)
总结一下,有 4 个重点:
- 客户端在多个 Redis 实例上申请加锁
- 必须保证大多数节点加锁成功
- 大多数节点加锁的总耗时,要小于锁设置的过期时间
- 释放锁,要向全部节点发起释放锁请求
为什么要在多个实例上加锁?
本质上是为了「容错」,部分实例异常宕机,剩余的实例加锁成功,整个锁服务依旧可用。
为什么大多数加锁成功,才算成功?
多个 Redis 实例一起来用,其实就组成了一个「分布式系统」。
如果存在「故障」节点,只要大多数节点正常,那么整个系统依旧是可以提供正确服务的。
为什么步骤 3 加锁成功后,还要计算加锁的累计耗时?
因为操作的是多个节点,所以耗时肯定会比操作单个实例耗时更久,而且,因为是网络请求,网络情况是复杂的,有可能存在延迟、丢包、超时等情况发生,网络请求越多,异常发生的概率就越大。
所以,即使大多数节点加锁成功,但如果加锁的累计耗时已经「超过」了锁的过期时间,那此时有些实例上的锁可能已经失效了,这个锁就没有意义了。
为什么释放锁,要操作所有节点?
在某一个 Redis 节点加锁时,可能因为「网络原因」导致加锁失败。
例如,客户端在一个 Redis 实例上加锁成功,但在读取响应结果时,网络问题导致读取失败,那这把锁其实已经在 Redis 上加锁成功了。
所以,释放锁时,不管之前有没有加锁成功,需要释放「所有节点」的锁,以保证清理节点上「残留」的锁。
优点:
- 安全特性:互斥访问,永远只有一个client能拿到锁
- 避免死锁:最终client都可能拿到锁,不会出现死锁的情况
- 容错性,只要大部分redis节点存活就可以正常提供服务
12 分布式UUID
当使用数据库来生成ID性能不够要求的时候,我们可以尝试使用Redis来生成ID。这主要依赖于Redis是单线程的,所以也可以用生成全局唯一的ID。可以用Redis的原子操作 INCR和INCRBY来实现。