一、发布订阅
Redis提供了发布订阅功能,可以用于消息的传输
Redis的发布订阅机制包括三个部分,publisher,subscriber和Channel
发布者和订阅者都是Redis客户端,Channel则为Redis服务器端。
发布者将消息发送到某个的频道,订阅了这个频道的订阅者就能接收到这条消息。
指令详情
- SUBSCRIBE / PSUBSCRIBE : 订阅,精确、或者按匹配符
- UNSUBSCRIBE / PUNSUBSCRIBE : 退订,精确、或者按匹配符
- PUBLISH : 发送
- PUBSUB :查看消息列表
发布订阅的命令演示
subscribe
订阅 subscribe channel1 channel2 ..
Redis客户端1订阅频道1和频道2
127.0.0.1:6379> subscribe ch1 ch2
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "ch1"
3) (integer) 1
1) "subscribe"
2) "ch2"
3) (integer) 2
publish
发布消息 publish channel message
Redis客户端2将消息发布在频道1和频道2上
127.0.0.1:6379> publish ch1 hello
(integer) 1
127.0.0.1:6379> publish ch2 world
(integer) 1
Redis客户端1接收到频道1和频道2的消息
1) "message"
2) "ch1"
3) "hello"
1) "message"
2) "ch2"
3) "world"
unsubscribe:退订 channel
Redis客户端1退订频道1
127.0.0.1:6379> unsubscribe ch1
1) "unsubscribe"
2) "ch1"
3) (integer) 0
psubscribe
模式匹配 psubscribe +模式
Redis客户端1订阅所有以ch开头的频道
127.0.0.1:6379> psubscribe ch*
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "ch*"
3) (integer) 1
Redis客户端2发布信息在频道5上
127.0.0.1:6379> publish ch5 helloworld
(integer) 1
Redis客户端1收到频道5的信息
1) "pmessage"
2) "ch*"
3) "ch5"
4) "helloworld"
punsubscribe
退订模式
127.0.0.1:6379> punsubscribe ch*
1) "punsubscribe"
2) "ch*"
3) (integer) 0
使用场景
在Redis哨兵模式中,哨兵通过发布与订阅的方式与Redis主服务器和Redis从服务器进行通信
Redisson是一个分布式锁框架,在Redisson分布式锁释放的时候,是使用发布与订阅的方式通知的
注:重业务的消息,推荐用消息队列
二、事务
所谓事务,是指作为单个逻辑工作单元执行的一系列操作
事务内的操作要么都成功,要么都失败
ACID特性就是刚性事务的特性
- Atomicity(原子性):构成事务的的所有操作必须是一个逻辑单元,要么全部执行,要么全部不执行。
- Consistency(一致性):数据库在事务执行前后状态都必须是稳定的或者是一致的。
- Isolation(隔离性):事务之间不会相互影响。
- Durability(持久性):事务执行成功后必须全部写入磁盘。
那在redis中的事务如何实现的呢?
Redis 事务的本质是一组命令的集合,就是一次执行多个命令
- Redis的事务是通过multi、exec、discard和watch这四个命令来完成的。
- Redis的单个命令都是原子性的,所以这里需要确保事务性的对象是命令集合。
- Redis将命令集合序列化并确保处于同一事务的命令集合连续且不被打断的执行
- Redis不能保障失败回滚
注意!redis的事务远远弱于mysql,严格意义上,它不能叫做事务,只是一个命令打包的批处理,不能一定保障失败回滚。
工作原理
- 调用multi指令后,redis其实是开启了一个命令队列,后续的命令被提交到队列(还没有执行)
- 期间出现问题了(比如down机),终止操作,队列清空
- 到exec命令后,批量提交,事务完成
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set s1 1
QUEUED
127.0.0.1:6379(TX)> get s1
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) "1"
redis如何做到事务回滚?
注意!回滚要看两种情况:
- 直接语法错误,redis完全无法执行,Redis 2.6.5之前的版本不会回滚,之后版本整个事务回滚
- 执行期的错误,redis不会回滚,其他正确的指令会照样执行
语法错误:
#旧value是a
127.0.0.1:9010> set a a
OK
127.0.0.1:9010> get a
"a"#开启事务
127.0.0.1:9010> multi
OK
#设置成b,语法没问题,进入队列
127.0.0.1:9010> set a b
QUEUED
#语法错误!
127.0.0.1:9010> set a
(error) ERR wrong number of arguments for 'set' command
#提交事务:失败,操作被回滚
127.0.0.1:9010> exec
(error) EXECABORT Transaction discarded because of previous errors.#最终结果:a没有被修改
127.0.0.1:9010> get a
"a"
执行错误:
#旧值a
127.0.0.1:9010> get a
"a"#开启事务
127.0.0.1:9010> multi
OK
#正确的语法,没毛病!
127.0.0.1:9010> set a b
QUEUED
#语法也对,但是类型肯定是不对的,这不是一个list!
#会进入队列,执行期才会发现这个问题
127.0.0.1:9010> lpush a 1
QUEUED
#提交事务!
#发现正确的1号命令执行ok,2号错误
127.0.0.1:9010> exec
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value#最终结果,a被修改,事务没有回滚!
127.0.0.1:9010> get a
"b"
watch监听致使回滚:
Redis Watch 命令用于监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断
关于上面的操作,如果遇到各种错误,multi可以自动帮你回滚
而watch命令提供了另一种机制,它通过监控某个key的变动,来决定是不是回滚。
#开启两个终端 T1, T2
#T1执行过程与上面一致#以下是T1的操作过程:
#初始化,a=a , b=1
127.0.0.1:9010> set a a
OK
127.0.0.1:9010> set b 1
OK
#监控a的变动
127.0.0.1:9010> watch a
OK
#开启事务,内部对b进行操作
127.0.0.1:9010> multi
OK
127.0.0.1:9010> set b 2
QUEUED# !!!这一步注意切换到T2:
#在T1的watch和exec之间执行一个 set a 123,a的值被别的终端修改了!!!#再切回T1,注意!exec得不到ok,得到了一个nil,说明队列被清空了!
127.0.0.1:9010> exec
(nil)
#来查看b的值,没有被改为2,事务回滚了!
127.0.0.1:9010> get b
"1"
三、Lua脚本
redis中的事务操作相当于一个命令的批处理执行,但是又不能真正意义上保证原子性。那么有没有什么办法来解决呢?答案就是Lua脚本
lua其实是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。
redis自2.6.0起可用,通过内置的lua编译/解释器,可以使用EVAL命令对lua脚本进行求值
redis嵌入了lua脚本,其好处:
- 减少网络开销。可以将多个请求通过脚本的形式一次发送,减少网络时延。
- 原子操作。redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。因此在编写脚本的过程中无需担心会出现竞态条件,无需使用事务。
- 复用。客户端发送的脚本会永久存在redis中,这样,其他客户端可以复用这一脚本而不需要使用代码完成相同的逻辑。
如何使用 EVAL命令
EVAL script numkeys key [key ...] arg [arg ...]
命令说明:
script:参数是一段 Lua 5.1 脚本程序。脚本不必(也不应该)定义为一个 Lua 函数
numkeys: 用于指定键名参数的个数。
key [key ...],是要操作的键,可以指定多个,在lua脚本中通过KEYS[1], KEYS[2]获取
arg [arg ...],附加参数,在lua脚本中通过ARGV[1], ARGV[2]获取。
如:
eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
lua脚本中调用redis命令
-
redis.call():
- 返回值就是redis命令执行的返回值
- 如果出错,则返回错误信息,不继续执行
-
redis.pcall():
- 返回值就是redis命令执行的返回值
- 如果出错,则记录错误信息,继续执行
eval "return redis.call('set',KEYS[1],ARGV[1])" 1 n1 zhaoyun
四、慢查询日志
客户端请求的生命周期的完整生命周期,4个阶段
注意:慢查询只统计步骤3的时间,所以没有慢查询并不代表客户端没有超时问题。换句话说。redis的慢查询记录时间指的是不包括像客户端响应、发送回复等IO操作,而单单是执行一个查询命令所耗费的时间。
面试题:日常在使用redis的时候为什么要用慢查询日志?
答:
- 慢查询日志是为了记录执行时间超过给定时长的redis命令请求
- 让使用者更好地监视和找出在业务中一些慢redis操作,找到更好的优化方法
慢查询会监控redis执行的命令中,一些超过提前设定的阈值的命令,将他存放到慢查询日志中,供我们查看
慢查询配置相关的参数
-
slowlog-log-slower-than
:选项指定执行时间超过多少微秒(默认1秒=1,000,000微秒)的命令请求会被记录到日志上。例:如果这个选项的值为100,那么执行时间超过100微秒的命令就会被记录到慢查询日志; 如果这个选项的值为500 , 那么执行时间超过500微秒的命令就会被记录到慢查询日志;
-
slowlog-max-len
:选项指定服务器最多保存多少条慢查询日志。服务器使用先进先出的方式保存多条慢查询日志: 当服务器储存的慢查询日志数量等于slowlog-max-len选项的值时,服务器在添加一条新的慢查询日志之前,会先将最旧的一条慢查询日志删除。例:如果服务器slowlog-max-len的值为100,并且假设服务器已经储存了100条慢查询日志, 那么如果服务器打算添加一条新日志的话,它就必须先删除目前保存的最旧的那条日志, 然后再添加新日志。
在Redis中有两种修改配置的方法,一种是修改配置文件,另一种是使用config set命令动态修改;
慢查询配置相关的命令
- config set slowlog-log-slower-than 20000
- config set slowlog-max-len 1024
- showlog get # 查看慢查询日志
慢查询日志的访问和管理相关命令
- 获取[n条]慢查询队列 slowlog get [n]
- 获取慢查询队列的当前长度 slowlog len
- 清空慢查询队列 slowlog reset
比如:
# 设置慢查询时长:
config set slowlog-log-slower-than 0 # 0表示将所有命令都记录为慢查询
# 设置最多保存多少条慢查询日志:
config set slowlog-max-len 3
# 获得慢查询日志:
slowlog get
慢查询相关知识总结
1、slowlog-max-len:线上建议调大慢查询列表,记录慢查询时Redis会对长命令做阶段操作,并不会占用大量内存.增大慢查询列表可以减缓慢查询被剔除的可能,例如线上可设置为1000以上.2、slowlog-log-slower-than:默认值超过10毫秒判定为慢查询,需要根据Redis并发量调整该值.3、慢查询只记录命令的执行时间,并不包括命令排队和网络传输时间.因此客户端执行命令的时间会大于命令的实际执行时间.因为命令执行排队机制,慢查询会导致其他命令级联阻塞,因此客户端出现请求超时时,需要检查该时间点是否有对应的慢查询,从而分析是否为慢查询导致的命令级联阻塞.4、由于慢查询日志是一个先进先出的队列,也就是说如果慢查询比较多的情况下,可能会丢失部分慢查询命令,为了防止这种情况发生,可以定期执行slowlog get命令将慢查询日志持久化到其他存储中(例如:MySQL等),然后可以通过可视化工具进行查询.