redis原理
原子性
一个事务是一个不可分割的最小工作单位,事务中包括的诸操作要么都做,要么都不做。
redis所有单个命令的执行都是原子性的,这与它的单线程机制有关;所以redis命令的原子性使得我们不用考虑并发问题,可以方便的利用原子性自增操作,实现简单计数器功能;
事务操作
由multi开启事务,然后将多个命令加入队列中,最后exec命令触发事务,依次执行队列中的所有命令。
命令 | 说明 |
---|---|
multi | 开启事务 |
exec | 触发事务 |
Inrc [key] | key的value+1,如果key不存在,value先初始化为0,再+1 |
事务的执行并不是原子性的
redis单个命令的执行是原子性的,但redis并没有在事务上增加任何原子性机制(事务不会回滚),所以redis事务的执行不是原子性的。
redis的事务可以理解为一个打包批量执行的脚本,但批量指令并不是原子性的,中间某条指令失败并不会导致所有指令回滚,也不会造成后续指令不做。
事务失败处理
exec 之前发生错误,编译器错误(语法错误)
语法错误会导致事务提交失败,队列中的所有命令都不会执行,保留原值。
exec 之后发生错误,运行时错误(Redis类型错误)
运行时报错,此时事务没有回滚,而是调过错误命令继续执行。
放弃执行事务(discard )
在exec之前,如果中途不想执行事务了,怎么办?
可以调用 discard 可以清空事务队列,放弃执行。
watch 实现事务的回滚
可以为 Redis 事务提供 CAS 乐观锁行为(Check and Set / Compare and Swap),借助于命令WATCH,让Redis事务完全具有事务回滚的能力。
在MULTI之前使用WATCH监控某些键值对,当EXEC执行事务时,首先会比对WATCH所监控的键值对,如果没发生改变,则执行事务队列中的命令,提交事务;如果发生变化,将不会执行事务中的任何命令,同时事务回滚。当然无论是否回滚,Redis都会取消执行事务前的WATCH命令。
https://www.cnblogs.com/fengguozhong/p/12161363.html
内存回收
过期删除
Redis 中同时使用了惰性过期和定期过期两种过期策略。
命令 | 说明 |
---|---|
expire key seconds | 单位秒,返回值 1–设置成功 0–设置失败或key不存在 |
ttl [key] | 查看key的过期时间还剩多少,返回值: -1–key没有设置过期时间 -2–key不存在 |
persist [key] | 取消key的过期时间设置 |
定时过期(主动淘汰):
每个设置过期时间的 key 都需要创建一个定时器,到过期时间就会立即清除。
该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的 CPU 资源去处理过期的 数据,从而影响缓存的响应时间和吞吐量。
惰性过期(被动淘汰):
key被访问时,如果key失效,则删除
该策略可以最 大化地节省 CPU 资源,却对内存非常不友好。极端情况可能出现大量的过期 key 没有再 次被访问,从而不会被清除,占用大量内存。
定期过期:
周期性的从过期的key中选择一部分删除,每秒操作10次
- 随机测试20个带有timeout信息的key
- 删除其中过期的key
- 如果超过25%的key被删除,则重复1
该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和 每次扫描的限定耗时,可以在不同情况下使得 CPU 和内存资源达到最优的平衡效果。
内存回收策略(maxmemory-policy)
当内存使用达到最大内存极限时,需要使用淘汰算法来 决定清理掉哪些数据,以保证新数据的存入。
maxmemory-policy noeviction // 默认,内存不够时申请内存报错
maxmemory-policy allkeys-lru // 从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰。适用场景:缓存的是热点数据,类似二八理论。
maxmemory-policy allkeys-random // 随机移除某个key。适用场景:缓存的key访问概率相等
maxmemory-policy volatile-random // 从设置过过期时间的数据中,随机选择数据淘汰
maxmemory-policy volatile-lru // ……选择最少使用的数据淘汰
maxmemory-policy volatile-tt // ……选择即将过期的数据淘汰
注意:
redis中lru并不是可靠的lru算法,也就是说实际淘汰的数据未必是真正使用最少的数据。这里涉及一个权衡的问题,如果要绝对满足最少使用,那就会增加系统开销,redis是单线程的,所以耗时的操作要谨慎些。
发布/订阅
发布者向指定频道发送消息,订阅者可以订阅多个频道。channel可以是全名,也可以通过正则匹配。
注意:该消息不会持久化,丢失了找不回。即订阅者收不到订阅之前的消息。
命令 | 说明 |
---|---|
publish [channel][message] | 发布消息,返回值:表示收到这条消息的订阅者数量 |
subscribe [channel][channel]…… | 订阅某频道 |
占位符 * 、?
?代表一个字符
*代表 0 个或者多个字符
持久化机制(RDB、AOF)
RDB(Redis DataBase)
RDB 是 Redis 默认的持久化方案。
主进程fork一个子进程,按指定规则,会把当前内存中的数据写入磁盘,生成一个快照文件 dump.rdb,替换上次的文件。
Redis 重启会通过加载 dump.rdb 文件恢 复数据。
优点:
- 主线程不进行任何IO操作,确保了性能。
- fock子进程,不是线程,避免的锁的问题,保证数据安全
缺点:
- 最后一次持久化之后的数据会丢。
- RDB 方式数据没办法做到实时持久化/秒级持久化。因为 bgsave 每次运行都要 执行 fork 操作创建子进程,频繁执行成本过高。
如何触发快照
-
设置配置规则
save 900 1 //900秒内,1个及以上的key被改过,则触发一次快照 save 300 10 //300秒……(同上) save 60 10000 //10000 秒……(同上)
-
. 手动命令–save/bgsave
save:阻塞所有请求,同步进行快照操作 bgsave:异步进行快照操作,服务器可继续响应请求。 lastsave -- 最后一次快照的时间
-
flushall – 清空redis内存数据
只要1的配置规则存在,flushall操作后会立即执行一次快照
- 执行复制操作
主从模式下,redis会在复制初始化时自动快照
- shutdown 触发
AOF(Append Only File)
Redis 默认不开启。
AOF 采用日志的形式来记录每个写操作,并追加到文件中。
开启后,执行更改 Redis 数据的命令时,就会把命令写入到 AOF 文件中。
Redis 重启时会根据日志文件的内容把写指令从前到后执行一次以完成数据的恢复 工作。
AOF文件重写
频繁操作后,AOF文件会很大,增加redis服务器存储压力,增加AOF还原数据所需时间,所以需要重写。重写后新的AOF文件包含恢复当前数据所需的最小指令集。
- 主进程fock一个子进程进行AOF重写。
- 子进程全量遍历内存数据,再逐个序列化到新的AOF文件中(重写的过程并不基于原有的AOF文件)。
- 子进程重写期间,主进程依旧对外提供服务,如果主进程发生更新操作,会把数据缓存到aof_rewrite_buf(单独开辟出来的缓存空间)中,最后子进程重写完后再把aof_rewrite_buf中的数据追加到新的AOF文件中。
注意:
并不是每次更新操作后都写入AOF文件,为了保证性能,有缓存机制:先把操作写入缓存,最后缓存内的操作写入AOF文件。
配置
appendonly yes //开启AOF,AOF默认没有开启。
dir //AOF文件的位置。
appendfilename //AOF文件名,默认文件名:apendonly.aof
auto-aof-rewrite-percentage 100 //当前aof文件超过上一次aof文件的百分比时重写。默认100%
auto-aof-rewrite-min-size 64mb //重写大小,低于64mb不重写
appendfsync always //每次更新操作后,都写入AOF文件,性能较慢
appendfsync everysec //默认
appendfsync no //不主动完成同步,由操作系统完成
RDB、AOF总结
如果对于数据安全性要求比较高,RDB和AOF可以一起用,毕竟RDB的快照数据备份、恢复比较快。那么就要牺牲一定的性能。
多路复用
redis单线程处理并发请求,避免了线程安全问题。
为什么用单线程?
redis的瓶颈不在cpu,主要在服务器内存和网络带宽。
I/O模型
同步/异步:指的是用户线程和内核的交互方式。
阻塞/非阻塞:指的是用户线程调用内核IO操作的方式是阻塞还是非阻塞。
同步阻塞:传统IO模型。用户线程等待内核空间的数据准备好之后才返回。
同步非阻塞:用户线程不等待内核空间的数据,直接返回。并轮询去内核空间看数据准备好没有,浪费CPU资源,类似自旋。默认创建的socket是阻塞的,socket设置为NONBLOCK即为非阻塞。
异步阻塞(多路复用):内核的数据准备好之后会有通知,然后用户线程去取数据。
异步非阻塞(异步IO): 用户线程不等待内核空间的数据,直接返回。
Lus脚本
Lua脚本的必要性
- 较少网络开销:多个命令放在同一脚本中运行。
redis有提供pipeline管道模型,一个请求中包含多个命令。但并不保证各个命令的顺序(类似内存屏障),如果各个命令之间有依赖,则存在风险。 - 原子操作:redis多个命令操作,本身没有原子性,所以redis会把整个脚本作为一个整体执行,中间不会插入其他命令。
- 复用性:客户端发送的脚本会永远存在redis中,这意味着其他客户端可以复用这个脚本,完成统一的逻辑。
管道模式(Pipeline)
redis 采用的是cs架构和http请求协议类似的。一个client通过一个socket连接发送一个命令。如果有多个client请求,那么就会阻塞,等待redis服务去处理。redis服务将处理完的报文返回给其中的一个client , 然后处理下一个client的请求。造成批量处理连接时延迟问题比较严重。
采用管道模式,服务端未及时响应的时候,客户端也可以继续发送命令请求,做到客户端和服务端互不影响,服务端将所有请求合并为一次IO,最终返回所有服务端的响应,大大提高了C/S模型交互的响应速度。
public static void main(String[] args) {
Jedis jedis=new Jedis("192.168.11.152",6379);
Pipeline pipeline=jedis.pipelined();
for(int i=0;i<1000;i++){
pipeline.incr("test");
}
pipeline.sync(); //一次性响应
}