1.前言
本节记录一下redis的一些功能上的实现,包括发布订阅、事务、Lua脚本、排序、二进制位数组、慢查询日志和监视器。
2.发布订阅
上一章介绍sentinel的时候说到了sentinel会订阅主从服务器的hello频道,每个sentinel通过往这个频道中传递各自的状态,让其它sentinel发现自己并更新相关状态。
Redis的发布与订阅功能是由PUBLISH、SUBSCRIBE、PSUBSCRIBE等命令组成。一个客户端订阅某个频道,一旦有消息推送到该频道,其就会知道相关消息。具体操作如下:
SUBSCRIBE “xxx" PUBLISH "xxx" "yyy" 订阅xxx频道的客户端就会接收到yyy消息。
还可以使用匹配模式的方式订阅多个频道:
PSUBSCRIBE ”new.[ie]t" PUBLISH “new.it" PUBLISH "new.et"
实现原理:
redisServer中有一个字段dict *pubsub_channels保存了所有频道和订阅关系。键是频道,值是客户端构成的链表。
有一个订阅者就会添加到这个字段中。
退订频道时,从这里面删除。
UNSUBSCRIBE
模式的订阅与退订:
redisServer中有一个字段list *pubsub_patterns保存了所有模式订阅关系.
PSUBSCRIBE的时候就会添加到这里面。
退订就是使用PUNSUBSCRIBE方法。
发送消息:
PUBLISH方法会将发送的消息,遍历pubsub_channels和pubsub_patterns找到对应的客户端发送给他们。
查看订阅信息:
PUBSUB命令是redis2.8新增加的命令之一,可以通过这个查看频道或者模式的相关信息。
PUBSUB CHANNELS 或者PUBSUB CHANNELS “news.[is]*"
PUBSUB NUMSUB查看输入的频道的订阅者数量
PUBSUB NUMPAT 查看当前订阅模式有多少个
3.事务
redis通过MULTI、EXEC和WATCH等命令来实现事务功能。提供了一种将多个命令请求打包,然后一次性、按顺序执行多个命令的机制,并且在事务执行期间,服务器不会中断事务而改去执行其他客户端的命令请求,它会将事务中的所有命令执行完毕。
例如: MULTI
SET "name" "xxx"
GET “name"
SET "author" "xx"
EXEC
事务的实现:
MULTI意味着开始,客户端切换到事务状态,修改flags。
EXEC、DISCARD、WATCH、MULTI四个命令会立刻执行。其他命令进入事务队列,返回QUEUED回复。事务队列在multiState mstate事务状态中。
接收到EXEC命令的时候,服务器会遍历事务队列,执行所有的结果并返回。
WATCH:
watch命令是一个乐观锁,可以在执行EXEC命令之前监视任何数量的数据库键。并在执行EXEC命令时,检查被监视的键是否被修改,如果是就会拒绝执行事务。
WATCH "name"
MULTI
SET "name" "peter"
EXEC
redisDb中有一个dict *watched_keys,键是具体的键,值是监视这个键的客户端链表。执行命令前先判断这个键有没有被监视,被监视了修改所有客户端的标志成REDIS_DIRTY_CAS。意味着事务安全性被破坏。EXEC时候会检查这个标志,就能判断是否可以提交。
事务的ACID性质:
原子性,redis的事务要不全部执行要不都不执行。
一致性,redis入队时会检查命令是否正确,执行过程中错误的命令不会对数据库进行修改,停机状态没持久化就没了,有持久化能够恢复。
隔离性,redis没有并发,所以有隔离性
耐久性,redis没有提供额外的持久化,都是通过redis的持久化模式决定的。加个SAVE可以有,但是效率太低。
4. Lua脚本
略。
5.排序
SORT可以对列表键,集合键或者有序集合键的值进行排序,即list,set,zset。
实现原理:
创建了一个与排序元素数量相同的数组,结构是redisSortObject,每个都对应一个元素。将元素进行排序,从小到大。最后遍历数组,返回排序后的元素。
默认只能进行数字类型比较大小,字符串用字符串的比较方式要命令添加ALPHA选项。
SORT可以配合ASC选项和DESC选项来实现。影响的就是结果的排序倒序还是顺序,SORT name ASC
BY选项的实现:
by选项可以实现某些字符串键或者hash键的某些域作为元素的权重进行排序。
比如:SADD fruits "apple" "banana" "cherry"
字典里面有MSET apple-price 8 banana-price 5.5 cherry-price 7
对fruits按照价格排序 SORT fruits by *-price
就会返回 banana cherry apple
By选项默认保存的是数字值,如果权重保存的是字符串,要同时使用ALPHA选项
mset apple-id "xxx" banana-id "yyy" cherry-id "zzz"
SORT fruits by *-id ALPHA
LIMIT选项:
返回指定范围的元素 SORT name LIMIT skip size
比如SORT fruits ALPHA 0 4
GET选项:
前面的都是返回排序的键的结果,如果我们想要返回关联的内容就要使用GET,比如对student进行排序,返回学生的全名。
SADD students jack peter tom
MSET peter-name "Peter White" jack-name "Jack Snow" tom-name "Tom Smith"
SORT students ALPHA GET *-name
就可以拿到学生排序后的相关学生姓名了。
STORE选项:
SORT只返回排序结果,通过STORE选项可以保存排序结果,以便下次使用。
SORT students ALPHA STORE sorted_students
下次取sorted_students键即可。
多个选项的执行顺序:
SORT <key> ALPHA DESC BY <by-pattern> LIMIT <offset> <count> GET <get-pattern> STORE <store_key>
执行顺序是:排序,限制长度,获取外部键,保存排序结果,向客户端返回结果。
除了GET选项,改变顺序不会影响执行顺序。
6.二进制位数组
redis提供了SETBIT、GETBIT、BITCOUNT、BITOP四个命令用于处理二进制位数组。
SETBIT bit 0 1 # 0000 0001
SETBIT bit 3 1 # 0000 0101
GETBIT bit 3 得到的就是1
BITCOUNT bit 得到的就是2
BITOP可以进行与、或、异或运算,按位取反
BITOP AND and-result x y z
BITOP OR or-result x y z
BITOP XOR xor-result x y z
BITOP NOT not-value value
redis用sds结构保存位数组。len表示保存了几个1字节长的位数组。
GETBIT命令的实现:
GETBIT <bitarray> <offset>
计算下标 offset / 8 确定在哪个位数组上,取出这个1字节长的byte
计算offset mod 8 + 1,计算偏移量,确定这个byte的第几个二进制位
返回这个值
SETBIT命令的实现:
SETBIT <bitarray> <offset> <value>
计算len offset / 8 + 1 ,确定需要多少个字节
检查bitarray当前的len,不够扩容到计算出的长度,扩容部分全部填0
byte = offset / 8 确定byte
bit = offset mod 8 + 1 确定二进制位
设置byte的bit位的值
返回oldValue
注意对于单个byte是使用逆序的方式保存内容的。
BITCOUNT命令的实现:
1.最直接的方法,遍历二进制位计数。假设数组长度为100MB,遍历要执行100 * 1024 * 1024 * 8,10来亿次计算,这个就可怕了。
2.查表法:创建一个表记录每个值的1的个数,比如8位字节的查表,8位字节可以组成0~255范围,制作一张表0的时候对应的1的个数就是0,1的时候对应的个数就是1
0000 0000 0
0000 0001 1
0000 0010 1
……
这样就可以不需要检测各个位,直接获得相关的1的个数了。表越大,计算次数就越少。但是这个方法有两个问题,一是用空间换时间,表越大耗费的空间越多。二是CPU缓存命中降低,CPU缓存能保存的表比例越低,查表无法命中,缓存的换入换出频繁,影响实际效率。
3.二进制统计算法:variable-precision SWAR算法
计算汉明距离:
i = (i & 0x55555555) + ((i >> 1) & 0x55555555);
i = (i & 0x33333333) + ((i >> 1) & 0x33333333);
i = (i & 0x0F0F0F0F) + ((i >> 1) & 0x0F0F0F0F);
i = (i * (0x01010101) >> 24);
第一步按两个二进制位进行一组,每组的十进制就是该组的汉明重量
第二步按四个二进制位进行分组,每组的十进制就是该组的汉明重量
第三步按八个二进制位进行分组,每组的十进制就是该组的汉明重量
最后计算bitarray的汉明重量,并记录在二进制位的最高八位,右移24位得到汉明距离
这个方法每次可以计算32个二进制位的汉明重量,比遍历快32倍,比键长8位的查表法快4倍。
4.二进制统计算法:redis实现
BITCOUNT命令使用了查表法和variable-precisionSWAR两种算法。
查表法使用8位的表,记录0000 0000到1111 1111的汉明重量
SWAR算法,每次循环载入128个二进制位,调用4次32位的SWAR算法计算这128个位的汉明重量。
BITCOUNT会根据未处理的二进制位数量来决定使用哪种算法:
未处理大于等于128位,使用SWAR算法。
小于128位,使用查表法。
100MB只需要执行625万次循环,比遍历8亿次快太多。
BITOP命令的实现:
创建一个空白数组位,然后对做操作的数组按字节进行与或操作,保存在这个空白数组中,最后返回。
7.慢查询日志
慢查询日志用于记录执行时间超过给定时长的命令请求,用户可以通过这个功能产生的日志来监视和优化查询速度。
slowlog-log-slower-than 超过多少微妙保存记录
slowlog-max-len 最多保存多少条慢查询日志,新的会让旧的被删除
可以通过CONFIG SET命令设置这两个参数,如CONFIG SET slowlog-max-len 5
SLOWLOG GET获取保存的慢查询日志:第一个是日志唯一标识,第二个是执行时的unix时间戳,第三个是执行耗时微妙,最后是命令及参数
SLOWLOG RESET重置慢查询日志
8.监视器
执行MONITOR命令,客户端就会变成一个监视器。实时打印出服务器当前处理的命令请求的相关信息。