Redis笔记(4)独立功能的实现

时间:2021-10-07 20:18:52

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命令,客户端就会变成一个监视器。实时打印出服务器当前处理的命令请求的相关信息。