大神:Redis实战(一):Redis一键安装脚本,Redis 介绍及 NIO 原理介绍_寒泉脚本-CSDN博客
简介
它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动分区(Cluster)提供高可用性(high availability)。
安装部署(cluster)
Redis集群搭建(单机集群)_单机部署redis集群-CSDN博客
redis使用epoll
(续上图)下图右侧是零拷贝的过程
Redis使用的epoll
redis数据结构和数据类型及应用
2 万字 + 20张图| 细说 Redis 九种数据类型和应用场景 - 小林coding - 博客园 (cnblogs.com)
(SUB)redis数据结构和数据类型及应用-CSDN博客编辑https://blog.csdn.net/weixin_38681369/article/details/133363259
redis为什么是单线程
问:Redis单线程是为了减少用户态到内核态的切换吗?
答:不是,至少主要原因不是。
操作系统为了响应多用户的请求,而进行的从用户态到内核态的切换,造成的的性能损耗,远不及为了保证数据一致性加锁带来的损耗。
Redis单线程是为了避免加锁的过程。
redis管道
发布订阅模式(实现消息队列)
启动不同的redis去接收订阅的消息,有的用来推送给用户,有的用来发给kafka,继而存储到数据库中
Redis事务
假设是client1绿色先开启的事务multi,client2黄色后开启的事务,
并且假设client2黄色的exec先到达,client1绿色的exec后到达:是先发送exec的客户端先执行命令
在事务中 watch 监控某个 key,如果发生改变,就不执行事务
不支持rollback
以下是这种做法的优点:
Redis 命令只会因为错误的语法而失败(并且这些问题不能在入队时发现),或是命令用在了错误类型的键上面:这也就是说,从实用性的角度来说,失败的命令是由编程错误造成的,而这些错误应该在开发的过程中被发现,而不应该出现在生产环境中。
因为不需要对回滚进行支持,所以 Redis 的内部可以保持简单且快速。
命令入队报错
在事务提交之前,客户端执行的命令缓存(队列)失败,比如命令的语法错误(命令参数个数错误,不支持的命令等等)。
如果发生这种类型的错误,Redis将向客户端返回包含错误提示信息的响应,同时Redis会清空队列中的命令并取消事务。
redis事务中出现运行错误
前面入队的正常命令可执行成功
乐观锁导致失效
由于乐观锁失败,事务提交时将丢弃之前缓存的所有命令序列。
WATCH
监视并不是在事务中某一条使用了被监视键的命令执行前检查,而是在整个事务开始前就检查所有被监视的键是否被修改。
watch 监控 key 所起的作用实际上是一个乐观锁,它所监控的是在事务期间有没有其他客户端对所监控的值进行修改
在Redis中可以通过开启两个redis客户端并结合watch命令模拟这种失败场景。
示例代码如下,开启两个客户端:
# 客户端1
127.0.0.1:6379> set name mengmeng # 客户端1设置name
OK
127.0.0.1:6379> watch name # 客户端1通过watch命令给name加乐观锁
OK
# 客户端2
127.0.0.1:6379> get name # 客户端2查询name
"mengmeng"
127.0.0.1:6379> set name qianqian # 客户端2修改name值
OK
# 客户端1
127.0.0.1:6379> multi # 客户端1开启事务
OK
127.0.0.1:6379> set name lili # 客户端1修改name
QUEUED
127.0.0.1:6379> exec # 客户端1提交事务,返回空
(nil)
127.0.0.1:6379> get name # 客户端1查询name,发现name没有被修改为lili
"qianqian"
持久化
fork()
fork是系统调用,copy on write是内核机制。
在fork子进程的时候,只拷贝指针,并不发生内存的复制。
只有当其中的某一个进程试图对该区域进行写操作时,内核就会在物理存储器中为子进程开辟一个新的物理页面,将需要写的区域将父进程的内容复制一份给子进程,然后对新的物理页面进行写操作。
这时就是实现了对不同进程的操作而不会产生影响其他的进程,同时也节省了很多的物理存储器。
并且根据经验来看,不可能父子进程将所有数据都改一遍。下图redis也用了这个机制,而且redis的子进程不会去修改数据:
可以用ref记录被引用的数量redis 子进程用来RDB持久化落盘,父进程用来提供服务:
fork会创建子进程copyonwrite子进程和父进程指向同一内存,当父进程数据改变时,父进程指向新数据,子进程依然指向原数据。原则父子进程数据互不影响数据隔离。子进程可看到父进程export的数据
$$
优先级高于管道优先于$BASHPID
![watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3NpbmF0XzQyNDgzMzQx,size_1,color_FFFFFF,t_70](https://i-blog.csdnimg.cn/blog_migrate/8de2a2737bb4a6bf7475ff1d4d5ac8e1.png)
RDB(Redis Database Backup)
也被叫做Redis数据快照,简单来说就是把内存中所有的数据都记录在磁盘中,当Redis实例故障重启后,从磁盘读取快照文件,恢复数据。快照文件称为RDB文件,默认是保存在当前的运行目录(RDB可以理解为U盘拷贝,将Redis中的数据直接进行复制操作)
每次触发RDB的时候,会单独创建( fork )一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。
触发策略
① 关闭Redis实例的时候,redis会在关闭之前主动的进行一次RDB(关闭不是宕机,宕机则数据丢失)
② 当你使用save/bgsave命令的时候,redis也会进行内存数据的持久化
③ 通过配置文件的配置触发:redis.conf配置文件
save 900 1
– 表示在900秒内,redis中有1个key发生改变,那么就进行一次bgsave
save 300 10
– 表示在300秒内,redis中有1个key发生改变,那么就进行一次bgsave
save 60 10000
– 表示在60秒内,redis中有10000个key发生改变,那么就进行一次bgsave
save/bgsave的不同
前面说到可以使用save命令或者bgsave命令来触发RDB,那么两者有什么区别呢?
- 如果使用的是save命令,数据备份就是由主线程来操作的,由于Redis是单线程的,所以如果使用save命令来进行内存的数据备份,那么在数据备份的时候redis是无法响应用户的请求的。当需要备份的数据非常大的时候,就可能导致请求被阻塞超时的情况
- 如果使用的是bgsave命令,那么实际上是主线程fork个子线程,让子线程来进行RDB操作,主线程只是在fork子线程的时候阻塞,之后便可以继续响应用户的请求。接下来子进程即可读取内存数据并进行持久化,生成新的RDB文件替换旧的RDB文件
RDB缺点:
- RDB执行间隔时间长,两次RDB之间写入数据有丢失风险(即丢失最后一次RDB后的数据)
- fork子进程、压缩、写出RDB文件都比较耗时
RDB优点:
- 使用RDB文件进行数据的恢复速度快、效率高(类似于文件拷贝)
- 相比于AOF持久化的文件,RDB的文件更小
相关配置
snapshotting里就是快照相关的配置 :
# 表示的是是否开启压缩,不建议开启,虽然节省空间,但是会耗费CPU
rdbcompression yes
# 默认的rdb文件名称
dbfilename dump.rdb
# 在redis.conf配置rdb文件存放路径
dir ./
# 触发策略
# 15分钟内有1个key发生改变,那么就保存
save 900 1
# 5分钟内有10个key发生改变,那么就保存
save 300 10
# 1分钟内有10000个key发生改变,那么就保存
save 60 10000
# 执行的都是bgsave
rdbchecksum 检查rdb文件的正确性
stop-writes-on-bgsave-error yes 当磁盘写不进去,或者慢的话,停止rdb
rdbcompression 持久化文件是否进行压缩
动态停止RDB : redis-cli config set save "" # save后给空值,表示禁用保存策略
AOF
追加日志 append only file
redis的写操作记录到文件中,类似于mysql的binlog
非阻塞,redis继续对外提供服务
同时数据能够落地
RDB和AOF可以同时开启。如果开启了AOF,重启服务器的时候,只会用AOF恢复。
4.0版本之后,AOF中包含RDB全量,增加记录新的写操作。
触发策略
配置项 | 刷盘时机 | 优点 | 缺点 |
---|---|---|---|
Always | 同步刷盘(每次命令都刷) | 可靠性低,几乎不丢数据 | 性能影响大 |
everysec | 每秒刷盘(先刷入AOF缓冲区,间隔1s刷盘) | 性能适中 | 最多丢失1s数据 |
no | 操作系统控制 | 性能最好 | 可靠性较差,可能丢失大量数据 |
重写机制
对同一个key的多个操作将被整合重写为最终的结果。通过使用bgrewriteaof命令,可以让AOF文件执行重写功能,用最少的命令达到相同的效果。执行完该指令后,所以的冗余指令就会被删除,达到AOF文件压缩的效果。
步骤:
a. bgrewriteaof触发重写,判断是否当前有bgsave或bgrewriteaof在运行,如果有,则等待该命令结束后再继续执行。,
b. 主进程fork 出子进程执行重写操作,保证主进程不会阻塞。v
c. 子进程遍历redis内存中数据到临时文件,客户端的写请求同时写入aof_buff缓冲区,aof_rewrite_buf重写缓冲区保证原AOF文件完整以及新AOF文件生成期间的新的数据修改动作不会丢失。
d. 子进程写完新的AOF文件后,向主进程发信号,父进程更新统计信息。主进程把aof_rewrite_buf中的数据写入到新的AOF文件。
e. 使用新的AOF文件覆盖旧的AOF文件,完成AOF重写。
redis4.0版本后的重写,实质就是把rdb 的快照,以二级制的形式附在新的aof头部,作为已有的历史数据,替换掉原来的流水账操作。
AOF优点:
- 通过配置,可以使得备份的数据更加完整安全
- 每次进行AOF时占用的CPU资源较少(因为是追加,RDB则是全部重新复制一遍)
AOF缺点:
- 通过使用AOF文件进行恢复的速度较慢,需要依次执行所有指令
- AOF文件可能会比RDB大得多,记录的是所有的写操作
相关配置
# 表示的是开启,默认是no
appendonly yes
# 这里表示的是AOF文件名称
appendfilename "appendonly.aof"
# AOF文件比上次文件增长超过百分之百则触发重写
auto-aof-rewrite-percentage 100
# AOF文件体积最小多大以上才能触发重写
auto-aof-rewrite-min-size 64mb
而当no-appendfsync-on-rewrite选项处于打开状态时,在执行BGSAVE命令或者BGREWRITEAOF命令期间,服务器会暂时停止对AOF文件的同步,从而尽可能地减少I/O阻塞。
no-appendfsync-on-rewrite会影响Redis事务的持久性。因为在服务器停止对AOF文件的同步期间,事务结果可能会因为停机而丢失。因此,如果服务器打开了no-appendfsync-on-rewrite选项,那么即使服务器运行在always模式的AOF持久化之下,事务也不具有持久性。
在默认配置下,no-appendfsync-on-rewrite处于关闭状态。
no-appendfsync-on-rewrite
aof-load-truncated yes
Redis 在恢复时会忽略最后一条可能存在问题的指令,默认为 yes。即在 AOF 写入时,可能存在指令写错的问题(突然断电、写了一半),这种情况下 yes 会 log 并继续,而 no 会直接恢复失败
RDB的改进(混合模式)
redis集群
主从复制
https://blog.csdn.net/succing/article/details/121230604
Redis没有使用强一致性。
实现三台redis的主从复制,三台机器端口号分别是6379(主),6380(从),6381(从)
1、在三台redis的配置文件中,关闭aof。启动三台redis,在6380(从),6381(从)的client端执行REPLICAOF 127.0.0.1 6379
我们查看6379(主)的部分日志,可以看到6380连接进来了
假设6381(从)挂掉了,那么在使用redis-server ./6381.conf --replicaof 127.0.0.1 6379
重启之后,仍然会将挂掉这段时间的增量同步过来。
replication_buffer
当且仅当slave与master首次或者出于某种原因,需要全量rdb传输数据后,然后会把replication_buffer中的数据,再次全量传给slave。
参数设置:
通过client-output-buffer-limit slave 参数设置,当这个值太小会导致主从复制链接断开,从而引发严重问题。
问题解析:参数值设置太小,就会导致replication_buffer不够用,新增的数据也就无法存入该缓冲区。反向会强迫master为了不让即将溢出的这部分丢失,后台自动的进行bgsave落成新的rdb。因此也会导致主从之间不得不再次全量同步,问题严重的话,maser会不停的bgsave,主从之间会不停的全量rdb同步数据,从而影响到整个服务的性能和质量。
语法:client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>
class可选值有三个,Normal,Slaves和Pub/Sub;
hard limit: 缓冲区大小的硬性限制。
soft limit: 缓冲去大小的软性限制。
soft seconds: 缓冲区大小达到了(超过)soft limit值的持续时间。Normal: 普通的客户端。默认limit 是0,也就是不限制。
Pub/Sub: 发布与订阅的客户端的。默认hard limit 32M,soft limit 8M/60s。
Slaves: 从库的复制客户端。默认hard limit 256M,soft limit 64M/60s。示例如下:
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit slave 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60
replication_backlog_buffer
当主从全量rdb后,master会把rdb通信期间收到新的数据的操作命令,写入 replication buffer,同时也会把这些数据操作命令也写入 repl_backlog_buffer 这个缓冲区,它里面保存着最新传输的命令。
如果从节点和主节点间发生了网络断连,等从节点再次连接后,可以从repl_backlog_buffer中同步尚未复制的命令操作。
对主从同步的影响:如果从节点和主节点间的网络断连时间过长,复制积压缓冲区(repl_backlog_buffer)可能被新写入的命令覆盖。此时,从节点就没有办法和主节点进行增量复制了,而是只能进行全量复制。针对这个问题,应对的方法是调大复制积压缓冲区的大小
参数设置:
通过repl-backlog-size参数设置,默认大小是1M。
大体算法如下:
每秒产生的命令 乘以((master执行rdb bgsave的时间)+ (master发送rdb到slave的时间) + (slave load rdb文件的时间) ) ,来估算积压缓冲区的大小,repl-backlog-size 值不小于这两者的乘积。例如,如果主服务器平均每秒产生1 MB的写数据,而从服务器断线之后平均要5秒才能重新连接上主服务器,那么复制积压缓冲区的大小就不能低于5MB。
为了安全起见,可以将复制积压缓冲区的大小设为2*5=10M,这样可以保证绝大部分断线情况都能用增量从而避免全量同步数据。
分布式(解决单节点容量不足问题)
如果数据可以分类,交集不多,可以考虑按业务拆分
如果数据没有办法划分拆解:
采用sharding分片
- 使用random随机分配节点,适合做消息队列
- 使用kmeta一致性哈希,规划一个环形哈希环
一致性哈希
物理节点:本来有的物理节点。
虚拟节点:可以让一个物理设备出现在好几个节点上,一个物理节点对应多个虚拟节点,将环上能落到虚拟节点的数据放到对应的物理节点上以解决数据倾斜。
这种方案更倾向于作为缓存,而不是数据库用。
(拓扑关系图)
redis cluster采用hash slot方式
Redis分布式存储的3种常见方案与Redis集群切片的几种常见方式_redis分布式存储方案-CSDN博客
Redis为什么不使用一致性哈希
虽然一致性哈希算法在分布式系统中有很多优点,但是在Redis中并不适用的原因有以下几点:
1. 简化的数据分片逻辑
Redis采用了简化的数据分片逻辑,将数据划分为固定数量的槽位,然后根据哈希值选择相应的槽位存储数据。这种简化的分片方式使得Redis的实现更加简单,减少了复杂性和潜在的错误。
2. 方便的数据迁移
由于Redis采用固定数量的槽位进行数据分片,当节点数量变化时,数据迁移也非常简单。只需要将一个槽位的数据从一个节点移动到另一个节点即可,而不需要重新计算哈希值和重新映射数据。这种简单的数据迁移方式使得Redis能够快速地进行水平扩展和缩减。
3. 避免数据热点问题
一致性哈希算法在节点数量变化时只需要迁移少量的数据,但是它可能会导致数据在节点上的分布不均衡,进而导致数据热点问题。当某个节点上的数据过多时,会造成该节点的负载过大,影响系统的性能和可靠性。
而Redis采用简化的数据分片方式可以避免这个问题,因为它将数据平均地分布到固定数量的槽位上,每个节点都能够均衡地处理请求。这种均衡的数据分布可以降低单个节点的负载,提高系统的整体性能和可靠性。
-----------------------------------
©著作权归作者所有:来自51CTO博客作者mob649e81693c66的原创作品,请联系作者获取转载授权,否则将追究法律责任
redis为什么不用一致性hash
https://blog.51cto.com/u_16175519/6923427
server端连接压力
redis连接对server端的成本很高,是对server端造成的
解决方式:
类似于nginx反向代理,增加一个接入层
我们增加一个代理层,我们只需要关注代理层性能就可以了。
代理层里面有了逻辑实现,例如modula,random,kemata,这叫无状态的。这样减轻了客户端的代码。
如果请求压力太大,代理层hold不住怎么办?我们看图的下半部分这个模型。
在代理层前面加一个LVS,LVS做一个主备,主备之间通过keepalived,除了监控两个LVS的健康状态之外,也监控proxy的健康状态。
https://github.com/twitter/twemproxy
无论你企业后面技术多么复杂,对于客户端,都是透明的。你要考虑客户端代码逻辑的复杂度成本,
不要因为技术而技术。redis连多线程都没有使用,它并不希望redis被引入那么多的功能。
以上三种模式的弊端是不能做分布式数据库用。一致性哈希增删节点的时候会让一部分数据时间窗不可用。
新增节点会对算法带来挑战,比如rehash等,怎么解决这个问题?
我们引入预分区的概念
预分区
以前我们模3,如果现在我们模10,中间再加一层mapping
新增节点,迁移数据的时候,把槽位中部分数据拿出来,放进新的节点即可
redis cluster模式:无主模型
http://redis.cn/topics/partitioning.html
客户端可以去任意一个redis节点取数据,每一个redis都有所有key的映射关系算法,知道别人持有哪些分片,会给客户端返回应该重定向到的redis,由客户端自己去正确的redis上取。
例如,客户端去redis3上找k1,redis2说,你应该去redis3找。于是客户端就去redis3上取k1
数据分治会带来一个问题:聚合操作很难实现。
事务很难实现
两个set取交集,可以实现但是redis并没有去实现,因为其中有一个数据移动的过程->redis的作者想要计算向数据移动,而不是去移动数据。redis的作者做了取舍,将影响性能的功能都取消掉了。
redis代理关闭了一些操作:
可以由人去实现:hash tag
例如我们将带有相同前缀的key放在一个节点上(我手动把它放到一起去,就能实现事务了)
Predixy
github上有完整的中文安装步骤
Redis内存回收机制
由于C语言并没有自动的内存回收机制,在redisObject结构中有一个refcount引用计数属性,当该值为0,也就是该对象不再被其他所引用时,就会释放内存。
Redis对象共享
可以让多个键共用一个值对象,只需要将指针指向它,并且将该值对象的引用计数+1便可以。注意这里只对整数值的字符串对象进行共享。如果对包含字符串值的对象进行共享,那么需要把两个字符串遍历一遍,时间复杂度为O(N),如果包含多个值的列表或者对象,那么时间复杂度为O(N^2).而整数值只需要转换完比较就ok了。所以为了时间效率,Redis只共享整数值的字符串
Redis对象的空转时长
redisObject还有一个属性lru:LRU_BITS:记录了该对象最后一次被访问的时间。用于内存不足时的回收
使用 object idletime命令便可以打印对象的空转时长:当前时间减去对象的lru属性值
redis缓存淘汰策略
数据逐出算法
LRU和LFU(LFU4.0出现)
LFU (Least Frequently Used) :最近最不频繁使用,跟使用的次数有关,淘汰使用次数最少的。
LRU (Least Recently Used):最近最少使用,跟使用的最后一次时间有关,淘汰最近使用时间离现在最久的。
逐出策略
在逐出算法中,根据用户设置的逐出策略,选出待逐出的key,直到当前内存小于最大内存值为主.
可选逐出策略如下:
- volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用 的数据淘汰
- volatile-lfu: 从已设置过期时间的数据中挑选不经常使用的数据进行淘汰
- volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数 据淘汰
- volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据 淘汰
- allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
- allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
- allkeys-lfu: 从所有数据中挑选不经常使用的数据进行淘汰;
- no-enviction(驱逐):禁止驱逐数据(4.0默认)
最佳实践
通常情况下推荐优先使用 allkeys-lru 策略。这样可以充分利用 LRU 这一经典缓存算法的优势,把最近最常访问的数据留在缓存中,提升应用的访问性能。
如果你的业务数据中有明显的冷热数据区分,建议使用 allkeys-lru 策略。
如果业务应用中的数据访问频率相差不大,没有明显的冷热数据区分,建议使用 allkeys-random 策略,随机选择淘汰的数据就行。
如果没有设置过期时间的键值对,那么 volatile-lru,volatile-lfu,volatile-random 和 volatile-ttl 策略的行为, 和 noeviction 基本上一致。
————————————————
版权声明:本文为CSDN博主「2301_78435703」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/2301_78435703/article/details/131239667
LFU算法分析
并不是简单的判断一段时间的访问次数,低的淘汰,这样会导致如果某个key在一段时间访问量极大,后面再也没有访问过,但却不会淘汰的现象
LFU 实现比较复杂,需要考虑几个问题:
- 如果实现为链表,当对象被访问时按访问次数移动到链表的某个有序位置可能是低效的,因为可能存在大量访问次数相同的 key,最差情况是O(n)
- 某些 key 访问次数可能非常之大,理论上可以无限大,但实际上我们并不需要精确的访问次数
- 访问次数特别大的 key 可能以后都不再访问了,但是因为访问次数大而一直占用着内存不被淘汰,需要一个方法来逐步“驱除”(有点 LRU的意思),最简单的就是逐步衰减访问次数
本着能省则省的原则,Redis 只用了 24bit (server.lruclock 也是24bit)来记录上述的信息,是的不是 24byte,连32位指针都放不下!
- 16bit : 上一次递减时间 (解决第三个问题)
- 8bit : 访问次数 (解决第二个问题)
访问次数的计算如下:
uint8_t LFULogIncr(uint8_t counter) {
if (counter == 255) return 255;
double r = (double)rand()/RAND_MAX;
double baseval = counter - LFU_INIT_VAL;
if (baseval < 0) baseval = 0;
double p = 1.0/(baseval*server.lfu_log_factor+1);
if (r < p) counter++;
return counter;
}
核心就是访问次数越大,访问次数被递增的可能性越小,最大 255,可以在配置 redis.conf 中写明访问多少次递增多少。由于访问次数是有限的,所以第一个问题也被解决了,直接一个255数组或链表都可以。
16bit 部分保存的是时间戳的后16位(分钟),表示上一次递减的时间,算法是这样执行,随机采样N个key,检查递减时间,如果距离现在超过 N 分钟(可配置),则递减或者减半(如果访问次数数值比较大)。
此外,由于新加入的 key 访问次数很可能比不被访问的老 key小,为了不被马上淘汰,新key访问次数设为 5。
过期淘汰策略
Redis数据过期和淘汰策略详解_25% redis 过期-CSDN博客
背景
Redis作为一个高性能的内存NoSQL数据库,其容量受到最大内存限制的限制。
用户在使用阿里云Redis时,除了对性能,稳定性有很高的要求外,对内存占用也比较敏感。在使用过程中,有些用户会觉得自己的线上实例内存占用比自己预想的要大。
事实上,实例中的内存除了保存原始的键值对所需的开销外,还有一些运行时产生的额外内存,包括:
- 垃圾数据和过期Key所占空间
- 字典渐进式Rehash导致未及时删除的空间
- Redis管理数据,包括底层数据结构开销,客户端信息,读写缓冲区等
- 主从复制,bgsave时的额外开销
- 其它
处理时机
- 惰性:访问Key时,会判断Key是否过期,逐出过期Key;
- 定时、定期:CPU空闲时在定期serverCron任务中,逐出部分过期Key;
- 每次事件循环执行的时候,逐出部分过期Key;
过期数据清理算法
Redis过期Key清理的机制对清理的频率和最大时间都有限制,在尽量不影响正常服务的情况下,进行过期Key的清理,以达到长时间服务的性能最优.
Redis会周期性的随机测试一批设置了过期时间的key并进行处理。测试到的已过期的key将被删除。具体的算法如下:
- Redis配置项hz定义了serverCron任务的执行周期,默认为10,即CPU空闲时每秒执行10次;
- 每次过期key清理的时间不超过CPU时间的25%,即若hz=1,则一次清理时间最大为250ms,若hz=10,则一次清理时间最大为25ms;
- 清理时依次遍历所有的db;
- 从db中随机取20个key,判断是否过期,若过期,则逐出算法逐出;
- 若有5个以上key过期,则重复步骤4,否则遍历下一个db;
- 在清理过程中,若达到了25%CPU时间,退出清理过程;
这是一个基于概率的简单算法,基本的假设是抽出的样本能够代表整个key空间,redis持续清理过期的数据直至将要过期的key的百分比降到了25%以下。这也意味着在长期来看任何给定的时刻已经过期但仍占据着内存空间的key的量最多为每秒的写操作量除以4.
相关最佳实践
- 不要放垃圾数据,及时清理无用数据
实验性的数据和下线的业务数据及时删除; - key尽量都设置过期时间
对具有时效性的key设置过期时间,通过redis自身的过期key清理策略来降低过期key对于内存的占用,同时也能够减少业务的麻烦,不需要定期手动清理了. - 单Key不要过大
给用户排查问题时遇到过单个string的value有43M的,也有一个list 100多万个大成员占了1G多内存的。这种key在get的时候网络传输延迟会比较大,需要分配的输出缓冲区也比较大,在定期清理的时候也容易造成比较高的延迟. 最好能通过业务拆分,数据压缩等方式避免这种过大的key的产生。 - 不同业务如果公用一个业务的话,最好使用不同的逻辑db分开
从上面的分析可以看出,Redis的过期Key清理策略和强制淘汰策略都会遍历各个db。将key分布在不同的db有助于过期Key的及时清理。另外不同业务使用不同db也有助于问题排查和无用数据的及时下线.
作为缓存常见问题(击穿、穿透、雪崩)
redis作为缓存的常见问题(三大问题+缓存一致性)-CSDN博客
redis做分布式锁
Redis 分布式锁实现方案 - 知乎 (zhihu.com)
演进:
解决:setnx px xxxms设置一个锁key,并设置超时时间
setnx key px 1000ms
问题:假设现在有 A/B 两个session,A先拿到锁,但是速度慢,还没执行完,锁就过期了,B顺理成章的拿到了锁。A执行完之后,解锁,这时候锁是属于B的,但是由于锁是同一把锁,所以A可以解锁。A把B的锁给解了,B还没执行完,锁就没了,那B不就直接暴露了?
解决:
通过设置钥匙解锁(解锁之前加个判断机制判断是不是自己锁的)
问题:A 先拿到锁,自己配了一把钥匙,但是速度慢,锁快过期之前执行完了,向 redis 查询当前锁配对的钥匙。查完之后,锁过期了,B顺利成章的拿到了锁,又配了一把钥匙。A 拿到了匹配信息,去解锁,然后又把 B 的锁给释放了。
解决:lua将判断钥匙是自己的和删除放在一起
lua脚本
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
问题:只是解决了A超时B进入A误删B上的锁的问题,依然没解决A执行过程锁超时,B进入并发的问题
解决:watchdog在锁即将到期时重置过期时间
Redisson
Redisson 是一个在 Redis 的基础上实现的 Java 驻内存数据网格(In-Memory Data Grid)。
Redisson的宗旨是促进使用者对 Redis 的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。
Redisson 内置了一个“看门狗”,只要 A线程活着,锁就不会过期。看门狗会定期将过期时间补足,如果A挂了,狗也就不复存在了,锁也就会过期了。
Redisson 是Java实现的
Redisson 也封装 可重入锁(Reentrant Lock)、公平锁(Fair Lock)、联锁(MultiLock)、红锁(RedLock)、读写锁(ReadWriteLock)、 信号量(Semaphore)、可过期性信号量(PermitExpirableSemaphore)、 闭锁(CountDownLatch)等。
大体流程如下:
redission使用
Redis 分布式锁实现方案 - 知乎 (zhihu.com)
watchdog
watch dog自动延期机制:
看门狗启动后,对整体性能也会有一定影响,默认情况下看门狗线程是不启动的。如果使用redisson进行加锁的同时设置了锁的过期时间,也会导致看门狗机制失效
- lock.lock(); 是阻塞式等待的,默认加锁时间是30s;如果业务超长,运行期间每隔10秒都会自动再续成30秒。不用担心业务时间长,锁自动过期被删掉;加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认会在30s内自动过期,不会产生死锁问题;
- 也可以自己指定解锁时间lock.lock(10,TimeUnit.SECONDS),10秒钟自动解锁,自己指定解锁时间redis不会自动续期;
- 默认情况下,看门狗的续期时间是30s,也可以通过修改Config.lockWatchdogTimeout来另行指定。watchdog 会每 lockWatchdogTimeout/3时间,去延时。
源码
private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
RFuture<Long> ttlRemainingFuture;
//如果指定了加锁时间,会直接去加锁
if (leaseTime != -1) {
ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
} else {
//没有指定加锁时间 会先进行加锁,并且默认时间就是 LockWatchdogTimeout的时间
//这个是异步操作 返回RFuture 类似netty中的future
ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
}
//这里也是类似netty Future 的addListener,在future内容执行完成后执行
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e != null) {
return;
}
// lock acquired
if (ttlRemaining == null) {
// leaseTime不为-1时,不会自动延期
if (leaseTime != -1) {
internalLockLeaseTime = unit.toMillis(leaseTime);
} else {
//这里是定时执行 当前锁自动延期的动作,leaseTime为-1时,才会自动延期
scheduleExpirationRenewal(threadId);
}
}
});
return ttlRemainingFuture;
}
// 枷锁
protected RFuture<Boolean> renewExpirationAsync(long threadId) {
return this.evalWriteAsync(this.getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('pexpire', KEYS[1], ARGV[1]); return 1; end; return 0;", Collections.singletonList(this.getRawName()), this.internalLockLeaseTime, this.getLockName(threadId));
}
// 延时
private void renewExpiration() {
ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ee == null) {
return;
}
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ent == null) {
return;
}
Long threadId = ent.getFirstThreadId();
if (threadId == null) {
return;
}
RFuture<Boolean> future = renewExpirationAsync(threadId);
future.onComplete((res, e) -> {
if (e != null) {
log.error("Can't update lock " + getRawName() + " expiration", e);
EXPIRATION_RENEWAL_MAP.remove(getEntryName());
return;
}
if (res) {
//如果 没有报错,就再次定时延期
// reschedule itself
renewExpiration();
} else {
cancelExpirationRenewal(null);
}
});
}
// 这里我们可以看到定时任务 是 lockWatchdogTimeout 的1/3时间去执行 renewExpirationAsync
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
问题:高可用呢?redis挂了不就凉了?
解决:
REDLOCK
为了进一步实现高可用
redission中包含了redlock锁的实现
Redlock算法是Antirez在单Redis节点基础上引入的高可用模式。
在Redis的分布式环境中,我们假设有N个完全互相独立的Redis节点,在N个Redis实例上使用与在Redis单实例下相同方法获取锁和释放锁。
现在假设有5个Redis主节点(大于3的奇数个),这样基本保证他们不会同时都宕掉,获取锁和释放锁的过程中,客户端会执行以下操作:
1.获取当前Unix时间,以毫秒为单位
2.依次尝试从5个实例,使用相同的key和具有唯一性的value获取锁。
当向Redis请求获取锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间,这样可以避免客户端死等3.客户端使用当前时间减去开始获取锁时间就得到获取锁使用的时间。
当且仅当从半数以上的Redis节点取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功4.如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间,这个很重要
5.如果因为某些原因,获取锁失败(没有在半数以上实例取到锁或者取锁时间已经超过了有效时间),
客户端应该在所有的Redis实例上进行解锁,无论Redis实例是否加锁成功,
因为可能服务端响应消息丢失了但是实际成功了,毕竟多释放一次也不会有问题