文章目录
1、发布订阅模式
发布订阅(pub/sub) 是一种消息通讯模式:
redis客户端可以订阅任意数量的频道,channel的消息是不会持久化的
- 客户端订一个或多个频道(频道可以不存在)支持通配符
subscribe test test2
psubscribe *est*
- 客户端向指定频道发送消息
publish test "大家好"
publish test2 "这是管理频道嘛?"
- 查看当前所有频道
pubsub channels
- 客户端退订一个或多个频道(支持通配符)(不能再订阅模式下使用)
unsubscribe test test2
punsubscribe ?es*
- 如何再代码中取消订阅?想想
2、Redis事务
一个事务从开始到执行会经历以下三个阶段:开始事务、命令入队、执行事务
开始事务
命令入队
执行事务
redis事务可以一次执行多个命令,并且带有以下三个重要的保证:(编译时异常都不会执行)
批量操作在发送exec命令前被放入队列缓存
收到exec命令后进行事务执行,事务中任意命令执行失败(运行时异常),其余的命令依然被执行
在事务执行过程,其他客户端提交的命令不会插入到事务执行命令序列中
redis事物的特点:按进入队列的顺序执行;不会受到其他客户端请求的影响。
总结:由上得出,redis就是支持一个批量执行操作而已,所以并不是原子性的。(单个命令是原子性的)
-
使用事务
// 开启事务 multi // 命令入队 set foo bar set foo1 bar1 set foo2 bar2 // 执行事务 exec
-
其他事务命令
- 取消事务
discard
- 监视一个或多个key,如果在 事务执行前 绑定一个或多个key被其他命令所改动,那么整个事务命令将被取消
watch foo foo1
- 测试1:get #key# 不会影响监控,事务会执行成功
- 测试2:incrby #key# 0 不会影响监控,事务会执行成功
- 测试3:incrby #key# 1 & decrby #key# 1 事务会被取消
- 测试4:在事务执行过程中,其他客户端在修改数据之前执行unwatch,事务会执行成功
- 测试5:在事务执行过程中,其他客户端修改数据后,执行unwatch, 事务会被取消
- 测试6:事务执行过程中输入错误命令入队, 事务会被取消
- 取消watch命令对所有key的监视
unwatch
- 取消事务
3、Lua脚本
Lua /ˈluə/ 是一种轻量级脚本语言,它是用C语言写的,跟数据库的存储过程有限类似。
a、使用的好处
- 一次发送多个命令,渐少网络开销。
- Redis会将整个脚本作为一个整体执行,不会被其他请求打断,保持原子性。
- 对于复杂的组合命令,我们可以放在文件中,可以实现程序之间的命令集复用。
b、在Redis中调用Lua脚本
eval lua-script key-num [key1 key2 key3 ...]/[value1 value2 value3 ...]
-
eval
代表执行 Lua 语言的命令 -
lua-script
代表 Lua 语言脚本内容 -
key-num
表示参数中有多少个key,需要注意的是Redis 中 key 是从1开始的,如果没有key的参数,那么些0 -
[key1 key2 key3 ...]
是key作为参数传递给 Lua 语言,也可以不填写,但是需要和key-num 的个数对那个起来 -
[value1 value2 value3 ...]
这些参数传递给 Lua 语言,他们是可填可不填写的 -
示例:
evel "return 'hello word'" 0
c、在Lua中调用Redis脚本
使用 redis.call(command,key[param1,param2…]" 操作
- command 是命令,包括set、get、del等。
- key 是倍操作的键
- param1,param2… 代表给key的参数
-
设置键值对:等价于 set lilei 2673
eval "return redis.call('set',KEYS[1],ARGV[1])" 1 lilei 2673
-
在Redis中调用Lua脚本名, 操作Redis
通常在redis-cli 中直接写Lua脚本不够方便,也不能实现编辑和复用,通常我们会把脚本放在文件里面,然后执行这个文件
-
创建一个Lua脚本:qilou.lua,内容如下,(后缀lua)
redis.call('set','gupao','lua666') return redis.call('get','gupao')
-
执行脚本
redis-cli --eval qilou.lua 0
-
d、用Lua脚本-限流
需求:在X秒内只能访问Y次
设计思路:用key记录IP,用value 记录访问次数
拿到IP以后,对IP + 1,如果是第一次访问,对key 设置过期时间(参数1)。否则判断次数,超过限定次数(参数2),返回0,如果没有超过次数则返回1.超过时间,key过期只有,可以再次访问。
ip_limit.lua 脚本
--ip_limit.lua
--IP 限流,对某个 IP 频率进行限制 ,6 秒钟访问 10 次
local num = redis.call('incr',KEYS[1]);
if tonumber(num) == 1 then
redis.call('expire',KEYS[1],ARGV[1])
return 1
elseif tonumber(num) > tonumber(ARGV[2]) then
return 0
else
return 1
end;
-- KEYS[1] 是ip
-- ARGV[1] 是过期时间X
-- ARGV[2] 是限制访问的次数
-- redis-cli –eval [lua脚本] [key…]空格,空格[args…]
测试命令:redis-cli --eval "ip_limit.lua" app:ip:limit:192.168.154.5 , 6 10
e、Lua脚本缓存
在脚本比较长的情况下,每次调用脚本都需要把脚本传递给Redis服务端,会产生比较大的网络开销。为了解决这个问题,Redis提供了EVALSHA命令,允许开发者通过脚本内容的SHA1摘要来执行脚本
示例:
script load "return 'hello world'"
-- "xxxxx 会返回一个很长的字符串"
evalsha "#输入返回的字符串#" 0
-- "hello world"
-
缓存脚本自乘运算
local curVal = redis.call("get",KEYS[1]); if curVal == false then curVal = 0 else curVal = tonumber(curVal) end; curVal = curVal * tonumber(ARGV[1]) ; redis.call("set",KEYS[1],curVal); return curVal
-
通过把脚本变成单行,语句之间用分号隔开来进行缓存
script load 'local curVal = redis.call("get",KEYS[1]);if curVal == false then curVal = 0 else curVal = tonumber(curVal) end; curVal = curVal * tonumber(ARGV[1]) ; redis.call("set",KEYS[1],curVal); return curVal' -- 结果:949920badd714b4750ab347103fe32a4a6c2e734 -- 记得先set num 2;初始化一个值哈
-
测试:
EVALSHA "949920badd714b4750ab347103fe32a4a6c2e734" 1 num 6
f、脚本超时
因为redis指令是单线程的,这个线程还要执行客户端,如果lua脚本执行超时,或者陷入死循环呢?
eval 'while(true)do end' 0
- 上面这种没有修改数据,可以配置lua-time-limit 5000 来保证超时后可以通过script kill 来取消这个脚本命令。但是数据修改了就不行了。例如下面:
eval "redis.call('set','gupao','666') while true do end" 0
- 只能采取shutdown nosave 或 shutdown
4、Redis效率
自己本地渣渣虚拟机测试
a、效率测试
-
redis-benchmark-tset,lpush-n100000-q
结果(本地虚拟机):
SET: 68161.68 requests per second —— 每秒钟处理 6 万多次 set 请求
LPUSH: 68017.96 requests per second —— 每秒钟处理 6 万多次 lpush 请求
-
redis-benchmark-n100000-qscriptload"redis.call('set','foo','bar')"
scriptloadredis.call(‘set’,‘foo’,‘bar’): 69735.01requestspersecond—— 每秒钟 69000 次 lua 脚本调用
b、为什么这么快?
-
纯内存KV
- KV结构的内存数据库,时间复杂度O(1)。
-
单线程
- 没有创建线程、销毁线程带来的消耗
- 避免了上下文切换导致的CPU消耗
- 避免了线程之间带来的竞争问题,例如加锁释放锁死锁等等
-
多路复用I/O
多路指的是多个TCP连接(Socket或Channel),复用指的是复用一个或多个线程
- 异步非阻塞I/O,多路复用处理并发连接
- 内部实现采用epoll,采用了epoll+自己实现的简单的事件框架。
- epoll中的读、写、关闭、连接都转化成了事件,然后利用epoll的多路复用特性,绝不在io上浪费一点时间 这3个条件不是相互独立的,特别是第一条,如果请求都是耗时的,采用单线程吞吐量及性能可想而知了。应该说redis为特殊的场景选择了合适的技术方案。
- 异步非阻塞I/O,多路复用处理并发连接
5、内存回收
redis 所有数据都是存储在内存中的,在某霞情况下需要对占用的内存空间进行回收。内存回收主要分两类,一类是key过期,一类是内存使用达到上限(max_memory)触发内存淘汰
所以redis自身有过期策略的处理如下:
a、过期策略
-
定时过期(主动淘汰)
每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。改策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量
-
惰性过期(被动淘汰)
当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化节约CPU资源,却对内存不友好。极端情况下可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
例如String:在getCommand里面会调用expireIfNeeded
源码:server.c expireIfNeeded(redisDb * db, robj *key)
第二种其航空,每次写入key时,发现内存不够,调用 activeExpireCycle释放一部分内存
源码:expire.c activeExpireCycle(int type)
-
定期过期
源码:server.h
typedefstructredisDb{ dict*dict; /* 所有的键值对 */ dict*expires; /* 设置了过期时间的键值对 */ dict*blocking_keys; /*Keys withclientswaitingfordata(BLPOP)*/ dict*ready_keys; /*BlockedkeysthatreceivedaPUSH*/ dict*watched_keys; /*WATCHEDkeysforMULTI/EXECCAS*/ intid; /*DatabaseID*/ longlongavg_ttl; /*Average TTL,justforstats*/ list*defrag_later; /*Listofkeynamestoattempttodefragonebyone,gradually.*/ }redisDb;
每隔一段时间,会扫描一定数量的数据库的expires 字段中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使用CPU和内存资源达到最优的平衡效果。
b、淘汰策略
Redis 的内存淘汰册罗,是指当额你存使用达到最大内存极限时,需要使用淘汰算法来决定清理掉哪些数据,以保证新数据的存入。
内存存储达到极限异常:OOM command not allowed when used memory > ‘maxmemory’
-
最大内存设置
- maxmemory
- 如果不设置maxmemory 或者设置为0,64位系统不限制内存,32位系统最多使用3GB内存
- 动态修改 config set maxmemory 2GB
-
淘汰策略
https://redis.io/topics/lru-cache
通过看配置文件中的[maxmemory-policy noeviction] 知道默认的是noeviction
动态修改策略:config set maxmemory-policy volatile-lru
同时在配置文件中可以看到如下可配置的策略
#volatile-lru -> EvictusingapproximatedLRUamongthekeys withanexpireset. #allkeys-lru -> EvictanykeyusingapproximatedLRU. #volatile-lfu -> EvictusingapproximatedLFUamongthekeyswithanexpireset. #allkeys-lfu -> EvictanykeyusingapproximatedLFU. #volatile-random -> Remove arandomkeyamongtheoneswithanexpireset. #allkeys-random -> Removearandomkey,anykey. #volatile-ttl -> Removethekeywiththenearestexpiretime(minorTTL) #noeviction -> Don'tevictanything,justreturnanerroronwriteoperations.
-
策略 含义 volatile-lru 根据 LRU 算法删除设置了超时属性(expire)的键,直到腾出足够内存为止。如果没有可删除的键对象,回退到noeviction策略。 allkeys-lru 根据LRU算法删除键,不管数据有没有设置超时时间,直到腾出足够额你存为止。 volatile-lfu 在带有过期时间的键中选择最不常用的。 allkeys-lfu 在所有的键中选择最不常用的,不管数据有没有设置超时属性。 volatile-random 在带有过期时间的键中随机选择。如果没有可删除的键对象,回退到noeviction策略。 allkeys-random 随机删除所有键,直到腾出足够内存为止。 volatile-ttl 根据键值对象的 ttl 属性,删除最近将要过期数据。如果没有,回退到 noeviction策略。 noeviction 默认策略,不会删除任何数据,拒绝所有写入操作并返回客户端错误信息(error)OOM command not allowed when used memory,此时 Redis 只相应读操作.
-
6、持久化机制
Redis 速度快,很大一部分原因是因为它所有的数据都存储在内存中.如果断电或者宏机,都会导致内存中的数据丢失.为了实现重启后数据不丢失,Redis 提供了两种持久化反感,一种是 RDB 快照(Redis DataBase),一种是AOF(Append Only FIle).
a、RDB
RDB 是 Redis 默认的持久化方案.当满足一定条件的时候,会把当前内存中的数据写入磁盘,生成一个快照文件 dump.rdb 。Redis 重启会通过加载 dump.rdb 文件恢复数据
如下有相关的一些redis.conf配置
# 文件路径 dir ./ # 文件名称 dbfilename dump.rdb # 是否用 LZF 压缩 rdb 文件,但是会消耗一些cpu的计算时间,默认开启 rdbcompression yes # 开启数据效验,使用CRC64算法来进行数据效验,但是会增加大约10%的性能消耗,如果希望最大性能则可关闭 rdbchecksum yes
查看最近备份时间:lastsave
-
RDB触发
-
自动触发
- 配置规则触发
redis.conf 文件中的SNAPSHOTTING 处,定义了把数据保存到磁盘的触发频率。如果不需要 RDB 方案,直接把 save 或配置成空字符串""。支持多个配置,满足任意就触发
save 900 1 # 900秒内至少一个key 被修改(包括添加) save 300 10 # 300秒内至少有10个key 被修改 save 60 10000 # 60秒内至少有10000个key被修改
为什么停止Redis服务时候没有save ,重启数据还在?
- shutdown 触发,保证服务器正常关闭
- flushall,会生成一份rdb文件
-
手动触发
- save,阻塞当前线程,并且生成快照
- bgsave,非阻塞当前线程,fork操作创建一个子线程(copy-on-write) 来负责持久化,不会记录fork之后的后序命令(不过fork阶段一般都很短)
-
-
RDB文件的优势和劣势
- 优点
- RDB是一个非常紧凑(compact)的文件,它保存了 redis 在某个时间点上的数据集。这种文件非常适合用于进行备份和灾难恢复。
- 生成 RDB 文件的时候,redis 主进程会fork() 一个子进程来处理所有保存工作,主进程不需要进行任何磁盘IO操作。
- RDB 在恢复大数据集的速度比 AOF 的回复速度要快。
- 缺点
- RDB 方式数据没办法做到实时持久化/秒级持久化。因为bgsave 每次运行都要执行fork 操作创建子进程,频繁执行成本过高。
- 在一定间隔时间做一次备份,所以如果redis 意外down 掉的话,就会丢失最后一次快照之后的所有修改(数据有丢失)
所以,如果数据相对重要,希望将损失降低到最低,就可以用AOF方式进行持久化
- 优点
b、AOF
Append Only File
AOF:Redis 默认不开启。AOF 采用日志的形式来记录每一个写操作,并追加到文件中。开启后,执行更改redis数据的命令时,就会把命令写入到AOF文件中。
Redis重启时会根据日志文件的内存把写指令从前到后执行一次以完成数据的恢复工作。
如下有相关的一些redis.conf配置
# 开关 redis默认开启RDB持久化,开启aof 需要修改为yes appendonly on # 文件名 ,路径也是通过dir参数配置 appendfilename “appendonlyname.aof”
-
AOF配置
由于操作系统的缓存机制,AOF 数据并没有真正地写入磁盘,而是进入了系统的硬盘缓存。什么时候把缓存区的内容写入到AOF文件?
参数 说明 appendfsync everysec AOF持久化策略(硬盘缓存到磁盘),默认everysec
● no 表示不执行fsync,由操作系统保证数据同步到磁盘,速度很快,但是不太安全。
● always 表示每次写入都执行fsync,以保证数据同步到磁盘,效率很低。
● everysec 表示每秒执行一次fsync,可能会导致丢失者1s数据。通常选择 everysec,兼顾安全性和效率如果出现 set qilou 666 ,执行1000次,怎么办?
- 为了解决这个问题,redis 新增了重写机制,当aof 文件的大小超过所设定的阔值时,redis 就会启动aof 文件的内存压缩,只保留可以恢复数据的最小指令集
-
AOF重写机制
aof文件重写并不是对原文件进行重新整理,而是直接读取服务器现有的键值对,然后用一条命令去代替之前记录这个键值对的多条命令,生成一个新的文件后去替换原来的AOF文件。
也可以使用
bgrewriteaof
来触发重写机制如下有相关的一些redis.conf配置
# 下面是默认值(两个参数都需要满足) auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb # 重写过程中aof文件被修改了呢? no-appendfsync-on-rewrite no aof-load-truncated yes
参数 说明 auto-aof-rewrite-percentage 默认值为100。aof自动重写配置。当目前aof文件大小超过上一次重写的aof文件大小百分之多少进行重写,即当aof文件增长到一定大小的时候,redis能够调用bgrewriteaof对日志文件进行重写。当前aof文件大小是上次日志重写得到aof文件大小的两倍(设置为100)时,自动启动新的日志重写过程。 auto-aof-rewrite-min-szie 默认64M。设置允许重写的最小aof文件大小。避免了达到约定的百分比但尺寸仍然很小的情况还要重写 no-appendfsync-on-rewrite 在aof重写或者写入aof文件的时候,会执行大量IO,此时对于everysec 和always的aof模式来说,执行fsync会造成阻塞过长时间,no-appendfsync-on-rewrite字段设置为默认设置为no。如果对延迟要求很高的应用,这个字段可以设置为yes,否则还是设置为no,这样对持久化特性来说这个更安全的选择。设置为yes表示rewrite期间对新写入操作不fsync,暂时存在内存中,等rewrite完成后再写入,默认为no,建议修改为yes。Linux的默认fsync策略是30秒。可能丢失30秒数据 aof-load-truncated aof文件可能在尾部是不完整的,当redis启动的时候,aof文件的数据被载入内存。重启可能发生在redis所在的主机操作系统宕机后,尤其在ext4文件系统没有加上data=ordered选项,出现这样的现象。redis宕机或者异常终止造成尾部不完整现像,可以选择让redis退出,或者导入尽可能多的数据。如果选择的是yes,当截断的aof文件被导入的时候,会自动发布一个log给客户端后load。如果是no,用户必须手动redis-check-aof修复aof文件才可以。默认值是yes -
AOF数据恢复
重启redis之后会自动进行aof文件的恢复
-
AOF优势与劣势
-
优点
AOF持久化的方法提供了很多的同步频率,即使使用默认的同步频率每秒同步一次,redis最多也就丢失1秒的数据而已。
-
缺点
对于具有相同数据的Redis,AOF文件通常会比RDB文件体积更大(RDB存的是数据快照)
虽然AOF提供了多种同步的频率,默认情况下,每秒同步一次的频率也具有较高的特性。在高并发的情况下,RDB 比 AOF 具有更好的性能保证。
-
c、RDB和AOF比较
同时开启了AOF和RDB,启动服务端,优先用AOF启动
那么在AOF和RDB两种持久化方式,我们一般怎么作出选择?
- 如果可以忍受一小段时间内数据的丢失,毫无疑问使用RDB是最好的,定时生成RDB快照(snapshot)非常便于进行数据库备份,并且RDB恢复数据集的速度也要比AOF恢复的速度更快
- 否则就使用AOF重写。但是一般情况下建议不要单独使用某一种持久化机制,而是应该两种一起使用,在这种情况下,当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据要完整。