Redis6学习

169 阅读18分钟

5大基本类型

String

插入:set key value

查询:get key

批插入:mset key value[key value...]

批查询:mget key [key...]

递增数字:incr key

增加指定的数字:incrby key number

递减数字:decr key

递减指定的数字:decrby key number

获取字符串长度:strlen key

分布式锁:setnx key value/set key value [EX seconds] [PX milliseconds] [NX|XX]

  • EX:key在多少之后过期。

  • PX:key在多少毫秒之后过期。

  • NX:当key不存在的时候,才创建key,效果等同于setnx。

  • XX:当key存在的时候,覆盖key

例子:set lock 123 ex 10 nx

应用场景:点赞

List

数据结构:一个双端链表的结构, 容量是2的32次方减1个元素,大概40多亿,主要功能有push/pop等,一般用在栈、队列、消息队列等场景

向列表左边添加元素:LPUSH key value[value...]

向列表右边添加元素:RPUSH key value[value...]

查看列表:LRANGE key start stop

获取列表中的元素的个数:LLEN key

应用场景:

  • 一个商品会被不同的用户进行评论,保存商品评论时,要按时间顺序排序。
  • 用户在前端页面查询该商品的评论,需要按照时间顺序降序排序。

Hash

Map<String,Map<Object,Object>>

一次设置一个字段值:HSET key field value

一次获取一个字段值:HGET key field

一次设置多个字段值:HMSET key field value[field value...]

一次获取多个字段值:HMGET key field[field...]

获取所有字段值:hgetall key

获取某个key内的全部数量:hlen key

删除一个key:hdel key

Set

添加元素:sadd key member[member...]

删除元素:srem key member[member...]

遍历集合中的所有元素:smembers key member

判断元素是否在集合中:sismember key member

获取集合中的元素总数:scard key

从集合中随机弹出一个元素,元素不删除:srandmember key[数字]

从集合中随机弹出一个元素,出一个删一个:spop key[数字]

集合运算:

  • 差集

    sdiff key[key...]

  • 交集

    sinter key[key...]

  • 并集

    sunion key[key...]

应用场景:

  • 微信抽奖小程序

    • 1.sadd key 用户ID
    • 2.scard key (计算参加总数)
    • 3.srandmember key 2 (随机抽奖2个人)
  • 微信朋友圈点赞

    • sadd pub:被用户ID [点赞用户1 点赞用户2...] (点赞)

    • srem pub:被用户ID (取消点赞)

    • smembers pub:被用户ID (展示点赞用户)

    • scard pub:被用户ID (统计点赞数)

  • 微博好友关注社交关系

    • 共同关注的人 sinter s1 s2

Zset

向有序集合中加入一个元素和该元素的分数

添加元素:zadd key score member[score member]

按照元素分数从小到大的顺序返回索引从start到stop之间的所有元素 :zrange key start stop [withscores]

获取元素的分数:zscore key member

删除元素:zrem key member[member]

获取指定分数范围的分数:zincrbyscore key min max [withscores] [limit offset count]

增加某个元素的分数:zincrby key incement member

获取集合中元素的数量:zcard key

获得指定分数范围内的元素个数:zcount key min max

按照排名范围删除元素:zremrangebyrank key start stop

获取元素的排名:

  • 从小到大

    zrank key member

  • 从大到小

    zrevrank key member

应用场景:

  • 根据商品销售对商品进行排序显示

    • zadd good:sellsort 9 1001 15 1002 (商品编号1001的销量是9,商品编号1002的销量是15)

    • zincrby goods:sellsort 2 1001 (有一个客户又买了2件商品1001,商品编号1001销量加2)

    • ZRANGE goods:sellsort 0 10 withscores (商品销量前10名)

  • 抖音热搜

stream(不推荐使用)

亿级系统统计类型

常见的四种统计

  • 聚合统计

    • 统计多个集合元素的聚合结果(交差并等集合统计)

    • 交并差集和聚合函数的应用

  • 排序统计

    • 抖音视频最新评论留言的场景(排序+分页)

      使用list/zest

      list带来的问题:每来一个新评论就用LPUSH命令把它插入List的队头。但是,如果在演示第二页前,又产生了一个新评论,第2页的评论不一样了。

      如果数据更新频繁或者需要分页显示,建议使⽤ZSet

  • 二值统计

    只有两个值需要存储,集合元素的取值就只有0和1两种。例如钉钉打卡

    使用bitmap

  • 基数统计

    指统计⼀个集合中不重复的元素个数

    使用HyperLogLog

Bitmap

image.png

由0和1状态表现的二进制位的bit数组

设置指定下标值:setbit key offset value

获取指定下标值:getbit key offset

get命令的底层编码实质是二进制的ascil编码对应

统计字节数占用多少:strlen key

统计多少含有多少个1: bitcount key

集合运算:bitop operation destkey key [key ...]

  • BITOP AND destkey key [key ...] ,对一个或多个 key 求逻辑并,并将结果保存到 destkey 。
  • BITOP OR destkey key [key ...] ,对一个或多个 key 求逻辑或,并将结果保存到 destkey 。
  • BITOP XOR destkey key [key ...] ,对一个或多个 key 求逻辑异或,并将结果保存到 destkey 。
  • BITOP NOT destkey key ,对给定 key 求逻辑非,并将结果保存到 destkey 。

HyperLogLog

基数:是一种数据集,去重复后的真实个数

牺牲精确率还空间,误差仅仅只有0.81%左右

概率计算不直接存储数据本身,只是进行不重复的基数统计,不是集合也不保存数据,只记录数量而不是具体内容。

为什么redis集群的最大槽数是16384个?

Redis 集群有163 84个哈希槽 ,每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽。

CRC16算法产生的hash值有16bit,该算法可以产生2^16=65536个值。换句话说值是分布在0~65535之间。那作者在做mod运算的时候,为什么不mod65536,而选择mod16384?

github.com/redis/redis…

正常的心跳数据包带有节点的完整配置,可以用幂等方式用旧的节点替换旧节点,以便更新旧的配置。这意味着它们包含原始节点的插槽配置,该节点使用2k的空间和16k的插槽,但是会使用8k的空间(使用65k的插槽)。 同时,由于其他设计折衷,Redis集群不太可能扩展到1000个以上的主节点。 因此16k处于正确的范围内,以确保每个主机具有足够的插槽,最多可容纳1000个矩阵,但数量足够少,可以轻松地将插槽配置作为原始位图传播。请注意,在小型群集中,位图将难以压缩,因为当N较小时,位图将设置的slot / N位占设置位的很大百分比。 

  • (1)如果槽位为65536,发送心跳信息的消息头达8k,发送的心跳包过于庞大。 

在消息头中最占空间的是myslots[CLUSTER_SLOTS/8]。 当槽位为65536时,这块的大小是: 65536÷8÷1024=8kb  

因为每秒钟,redis节点需要发送一定数量的ping消息作为心跳包,如果槽位为65536,这个ping消息的消息头太大了,浪费带宽。 

  • (2)redis的集群主节点数量基本不可能超过1000个。 

集群节点越多,心跳包的消息体内携带的数据越多。如果节点过1000个,也会导致网络拥堵。因此redis作者不建议redis cluster节点数量超过1000个。 那么,对于节点数在1000以内的redis cluster集群,16384个槽位够用了。没有必要拓展到65536个。 

  • (3)槽位越小,节点少的情况下,压缩比高,容易传输 

Redis主节点的配置信息中它所负责的哈希槽是通过一张bitmap的形式来保存的,在传输过程中会对bitmap进行压缩,但是如果bitmap的填充率slots / N很高的话(N表示节点数),bitmap的压缩率就很低。 如果节点数很少,而哈希槽数量很多的话,bitmap的压缩率就很低。

添加元素:pfadd key element[element]

统计key值:pfcount key

合并key至新key:pgmerge new_key key1 key2

Geo

地理位置的处理

添加:geoadd key longitude(经度) latitude(纬度) member(位置名称)

从键里面返回所有给定位置元素的位置(经度和纬度):geopos

redis单线程和多线程

Redis主要的性能瓶颈是内存或者网络带宽而并非CPU

IO多路复用

通过监测文件的读写事件再通知线程执行相关操作,保证Redis的非阻塞IO能够执行完成的机制

多路指的是多个socket连接

复用指的是复用一个线程。多路复用主要有三种技术:select epoll poll

单个线程高效的执行多个请求

image.png

常见名词

UV:(Unique Visitor)独立访客,一般理解为客户端IP。考虑去重

PV:(Page view)页面浏览量

DAU:(Dail Active User)日活跃用户量

MAU:(Monthly Active User)月活跃用户量

缓存预热

将数据库中的一部分数据迁移到redis中

缓存雪崩

  1. redis主机挂了,Redis 全盘崩溃

  2. 比如缓存中有大量数据同时过期

防止雪崩

  • redis缓存集群实现高可用(1.主从+哨兵 2.Redis Cluster)

  • ehcache本地缓存 + Hystrix或者阿里sentinel限流&降级

  • 开启Redis持久化机制aof/rdb,尽快恢复缓存集群

缓存击穿

大量的请求同时查询一个 key 时,此时这个key正好失效了,就会导致大量的请求都打到数据库上面去

防止击穿

  • 不设置过期时间

  • 加互斥独占锁

  • 双redis使用差异失效时间

缓存穿透

请求去查询一条记录,先redis后mysql发现都查询不到该条记录,但是请求每次都会打到数据库上面去,导致后台数据库压力暴增。

防止穿透

  • 空对象缓存或者缺省值

    • 第一次打到mysql,空对象缓存redis后第二次就返回null了

    • 由于存在空对象缓存和缓存回写(看自己业务不限死),redis中的无关紧要的key也会越写越多(记得设置redis过期时间)

  • Redis布隆过滤器解决缓存穿透(可用于分布式)

布隆过滤器BloomFilter

缓存过期淘汰策略

出现缓存过期淘汰策略,主要是因为内存大小是有限制的

Redis默认内存:如果不设置最大内存大小或者最大内存大小为0,在64位操作系统下不限制内存大小,在32位操作系统下最多使用3GB内存。

一般生产上如何配置:一般推荐Redis设置内存为最大物理内存的四分之三。

删除方式:

  • 立即删除

对CPU不友好,用处理器性能换取存储空间 (拿时间换空间)

  • 惰性删除

对memory不友好,用存储空间换取处理器性能(拿空间换时间)

  • 定期删除(上面两种方式的则中)

定期删除策略每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。

redis默认每个100ms检查,是否有过期的key,有过期key则删除。 注意: redis不是每隔100ms将所有的key检查一次而是随机抽取进行检查。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。(会存在漏网之鱼)

redis缓存淘汰策略

noeviction: 不会驱逐任何key

allkeys-lru: 对所有key使用LRU算法进行删除

volatile-lru: 对所有设置了过期时间的key使用LRU算法进行删除

allkeys-random: 对所有key随机删除

volatile-random: 对所有设置了过期时间的key随机删除

volatile-ttl: 删除马上要过期的key

allkeys-lfu: 对所有key使用LFU算法进行删除

volatile-lfu: 对所有设置了过期时间的key使用LFU算法进行删除

4个方面 LRU、LFU、random、ttl

2个维度 过期键中筛选、所有键中筛选

分布式锁

锁的类型:

  • 单机版同一个JVM虚拟机内,synchronized或者Lock接口

  • 分布式不同个JVM虚拟机内,单机的线程锁机制不再起作用,资源类在不同的服务器之间共享了。

分布式锁要具有的特征

1.独占性(OnlyOne,任何时刻只能有且仅有一个线程持有)

2.高可用(若redis集群环境下,不能因为某一个节点挂了而出现获取锁和释放锁失败的情况)

3.防死锁(杜绝死锁,必须有超时控制机制或者撤销操作,有个兜底终止跳出方案)

4.不乱抢(不能私下unlock别人的锁,只能自己加锁自己释放)

5.重入性(同一个节点的同一个线程如果获得锁之后,它也可以再次获取这个锁)

集群+CAP redis对比zookeeper

Redis集群(AP):redis异步复制造成的锁丢失,比如:主节点没来的及把刚刚set进来这条数据给从节点,master就挂了,从机上位但从机上无该数据

Zookeeper集群(CP) image.png

image.png

Redlock算法(Distributed locks with Redis)

Redis分布式锁比较正确的姿势是采用redisson这个客户端工具

不使用master-slave模式和哨兵模式,使用全masrer模式

Redlock算法与容错公式

N = 2X + 1

保证数据一致

N:部署机器数

X:宕机个数

底层数据结构

Redis定义了redisObjec结构体,来表示string、hash、list、set、zset等数据类型

geo实质zset,bitmap实质string,hyperloglog实质string

image.png

image.png

image.png

在Redis数据库里,包含字符串值的键值对都是由SDS实现的(Redis中所有的键都是由字符串对象实现的即底层是由SDS实现,Redis中所有的值对象中包含的字符串对象底层也是由SDS实现)。

String数据结构

string提供了三种编码方式:intemstrraw

int:保存long 型(长整型)的64位(8个字节)有符号整数。Redis 启动时会预先建立 10000 个分别存储 0-9999 的 redisObject 变量作为共享对象,这就意味着如果 set字符串的键值在 0-10000 之间的话,则可以  直接指向共享对象而不需要再建立新对象,此时键值不占空间

emstr:代表 embstr 格式的 SDS(Simple Dynamic String 简单动态字符串),保存长度小于44字节的字符串。字符串 sds结构体与其对应的 redisObject 对象分配在同一块连续的内存空间,字符串sds嵌入在redisObject对象之中一样

raw:保存长度大于44字节的字符串。这与OBJ_ENCODING_EMBSTR编码方式的不同之处在于,此时动态字符串sds的内存 与其依赖的redisObject的内存不再连续了

SDS数据结构

image.png

Hash数据结构

hash-max-ziplist-entries:使用压缩列表保存时哈希集合中的最大元素个数。(默认512个)

hash-max-ziplist-value:使用压缩列表保存时哈希集合中单个元素的最大长度。(默认64byte)

1.哈希对象保存的键值对数量小于 512 个;

2.所有的键值对的健和值的字符串长度都小于等于 64byte(一个英文字母一个字节) 时用ziplist,反之用hashtable

当条件其中一条被破环时:ziplist -> hashtable

image.png

ziplist数据结构

1.ziplist 是一个特殊的双向链表没有维护双向指针:prev next;而是存储上一个 entry的长度和当前entry的长度,通过长度推算下一个元素在什么地方。牺牲读取的性能,获得高效的存储空间,因为(简短字符串的情况)存储指针比存储entry长度更费内存。这是典型的“时间换空间”。(在数据结构中双向指针被代替为,上一个 entry的长度和当前entry的长度

image.png

Zset数据结构

server.zset_max_ziplist_entries(默认值为128个)

server.zset_max_ziplist_value(默认值为 64byte)

当条件其中一条被破环时:ziplist -> skiplist

skiplist数据结构

空间换时间

时间复杂度是O(logN),所以空间复杂度是O(N)

跳表是可以实现二分查找的有序链表

image.png

跳表是一个最典型的空间换时间解决方案,而且只有在 数据量较大的情况下 才能体现出来优势。而且应该是 读多写少的情况下 才能使用,所以它的适用范围应该还是比较有限的 

维护成本相对要高 - 新增或者删除时需要把所有索引都更新一遍; 

最后在新增和删除的过程中的更新,时间复杂度也是O(log n)

双写一致性

只要用缓存,就可能会涉及到缓存与数据库双存储双写,那先动哪一个?

分布式系统只有最终一致性,很难做到强一致性

这里要使用到canal

Canal

Canal是基于MySQL变更日志增量订阅和消费的组件

作用:

  • 数据库镜像
  • 数据库实时备份
  • 索引构建和实时维护(拆分异构索引、倒排索引等)
  • 业务 cache 刷新
  • 带业务逻辑的增量数据处理

image.png

MySQL的主从复制将经过如下步骤: 

1、当 master 主服务器上的数据发生改变时,则将其改变写入二进制事件日志文件中; 

2、slave 从服务器会在一定时间间隔内对 master 主服务器上的二进制日志进行探测,探测其是否发生过改变, 

如果探测到 master 主服务器的二进制事件日志发生了改变,则开始一个 I/O Thread 请求 master 二进制事件日志; 

3、同时 master 主服务器为每个 I/O Thread 启动一个dump Thread,用于向其发送二进制事件日志; 

4、slave 从服务器将接收到的二进制事件日志保存至自己本地的中继日志文件中; 

5、slave 从服务器将启动 SQL Thread 从中继日志中读取二进制日志,在本地重放,使得其数据和主服务器保持一致; 

6、最后 I/O Thread 和 SQL Thread 将进入睡眠状态,等待下一次被唤醒;

canal 工作原理 

canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送dump 协议 

MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal ) 

canal 解析 binary log 对象(原始为 byte 流)

image.png

缓存双写一致性讨论

对于读写缓存来说,要想保证缓存和数据库中的数据⼀致,就要采⽤同步直写策略

同步直写策略:写缓存时也同步写数据库,缓存和数据库中的数据⼀致;

Redis:

  • Redis中有数据:需要和数据库中的值相同
  • Redis中无数据:数据库中的值要是最新值

先更新数据库,再更新缓存

问题:数据库更新成功,缓存更新失败,业务会读取到脏数据。

先删除缓存,再更新数据库

问题:如果数据库更新失败,导致B线程请求再次访问缓存时,发现redis里面没数据,缓存缺失,再去读取mysql时, 从数据库中读取到旧值

先更新数据库,再删除缓存

问题:假如缓存删除失败或者来不及,导致请求再次访问redis时缓存命中, 读取到的是缓存旧值。

先更新缓存,再更新数据库

一般不使用这种策略。

总结

在大多数业务场景下,我们会把Redis作为只读缓存使用。假如定位是只读缓存来说,理论上我们既可以先删除缓存值再更新数据库,也可以先更新数据库再删除缓存,但是没有完美方案,两害相衡趋其轻的原则

优先 使用先更新数据库,再删除缓存的方案 。理由如下: 

  • 先删除缓存值再更新数据库,有可能导致请求因缓存缺失而访问数据库,给数据库带来压力,严重导致打满mysql。 

  • 如果业务应用中读取数据库和写缓存的时间不好估算,那么,延迟双删中的等待时间就不好设置。 

image.png

Redis的多路复用

epoll

poll

epoll