Redis相关知识(持续更新...)

1. Redis发布和订阅

1.1 什么是发布和订阅

Redis发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)订阅消息。 Redis客户端可以订阅任意数量的频道(频道:专门接收某种信息即频道)。

1.2 Redis客户端中发布订阅命令行实现

1、打开客户端订阅channel1

SUSCRIBE channel1(channel1:为订阅的频道名)

boMADP.png

2、打开另一个客户端并向channel1中发送消息

publish channel1 hello(向channel1发送消息hello)

返回1表示在消息已经发送到有一个订阅该频道的客户端接收到消息,返回0则表示没有订阅该频道的客户端接收到消息。

boMUC4.png

下图为订阅该频道的客户端接收到消息

boMIqP.png

注: 发布的消息没有持久化,如果在订阅的客户端收不到hello,只能收到订阅后发布的消息。

2. Redis6新的数据类型

2.1 Bitmaps

2.1.1 简述

  Redis提供了Bitmaps这个“数据类型”可以实现对的操作,合理地使用操作位能有效地提高内存使用率和开发效率:

  • Bitmaps本身不是一种数据类型,实际上它就是字符串(key-value)但它可以对字符串的位进行操作
  • Bitmaps单独提供一套命令,可以在redis中使用Bitmaps和使用字符串的方法不太相同。可以Bitmaps想象成一个以位为单位的数组,数组的每个单元只能存储0和1,数组的下标在Bitmaps中叫做偏移量。

2.1.2 Redis客户端中使用Bitmaps命令

setbit key offset(偏移量) value(值) 设置Bitmaps中某个偏移量的值(0或1)

例如: 每个独立用户是否访问过网站存放在Bitmaps中,将访问的用户记作1,没有访问的用户记作0,用偏移量作为用户的id。 设置键的第offset个位的值(从0算起),假设现在又20个用户,userid=1,6,11,15,19的用户对网站进行了访问,那么当前Bitmaps初始化结果如图。 bo8Wu9.png

“users:20220311”为key,表示2022-02-11这天访问的用户 boJii6.png

注意: 很多应用的用户id以一个指定数字(例如 10000)开头,直接将用户id和Bitmaps的偏移量对应势必会造成一定的浪费,通常的做法是每次做setbit操作时将用户id减去这个指定数字。在第一次初始化Bitmaps时,如果偏移量非常大那么整个初始化过程执行会比较慢,可能会造成Redis堵塞。

getbit key offset 获取Bitmaps中某个偏移量的值

返回1表示访问过,返回0表示没有访问过。

boYfBj.png

bitcount key [start end] 统计字符串从start字节到end字节比特值为1的数量

botEbd.png

bitop and users:and:20220310_11(结果集的名字) users:20220310(10号的key) users:20220311(11号的key)

例如: 计算出202203111和20220310两天都访问过网站的用户数量(即11号访问了,10号也访问了) boaK8x.png

下图表示11号10号都访问网站的有3个人 boaORx.png

使用场景:

  • 当需要存储活跃的用户数很大时(千万级别),考虑使用Bitmaps,因为其所占的内存空间相对set集合更小。
  • 用户签到。很多网站都提供了签到功能,并且需要展示最近一个月的签到情况,这种情况可以使用 BitMap 来实现。根据日期 offset = (今天是一年中的第几天) % (今年的天数),key = 年份:用户id。如果需要将用户的详细签到信息入库的话,可以考虑使用一个一步线程来完成。
  • 统计用户是否在线。如果需要提供一个查询当前用户是否在线的接口,也可以考虑使用 BitMap 。即节约空间效率又高,只需要一个 key,然后用户 id 为 offset,如果在线就设置为 1,不在线就设置为 0。
  • 实现布隆过滤器

参考:Redis Bitmap 学习和使用

2.2 HyperLogLog

2.2.1 简述

在工作中,我们经常会遇到与统计相关的功能需求,比如统计网站PV(PageView 页面访问量),可以使用Redis的incr、incrby轻松实现。但是像UV(UniqueVisitor 独立访客)、独立IP数、搜索记录数等需要去重和计数的问题如何解决?这种集合中求不重复元素个数的问题称为基数问题解决基数问题的方案:(不是最优) (1)数据存储在MySQL表中,使用distinct count计算不重复个数 (2)使用Redis提供的hash、set、bitmaps等数据结构来处理 以上方案结果精确,但随着数据不断增加,导致占用空间越来越大,对于非常大的数据集是不切实际的。 能否通过降低一定的精度来平衡存储空间呢?Redis推出了HyperLogLog。 Redis HyperLogLog是哦那个来做基数统计的算法,其优点是在输入元素的数量或体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。 在Redis里面,每个HyperLogLog键只需花费12KB内存,就可以计算接近2的64次方个不同的基数。这和计算基数时元素越多耗费内存越多的集合形成鲜明的对比。但是因为HyperLogLog只会根据输入元素来计算基数,而不会存储输入元素本身,所以HyperLogLog不能像集合那样,返回输入的各个元素。

2.2.2 Redis客户端中使用HyperLogLog命令

pfadd key element[element...] 添加指定元素到HyperLogLog中。

可以看到,当添加不同值时返回的是1,当添加相同值时返回0,即达到去重的目的 boyduq.png

pfcount key 计算key中的基数的个数

boyXrt.png

pfmerge destkey sourcekey[sourcekey...] 将一个或多个HLL合并后的结果存储在另一个HLL中,比如每月活跃用户可以使用每天的活跃用户来合并计算可得。

bo6co8.png

应用场景:

  • 像UV(UniqueVisitor 独立访客)等需要去重和计数的问题,且数据量非常非常大的时候,建议使用
  • 统计注册 IP 数
  • 统计每日访问 IP 数
  • 统计在线用户数
  • 统计用户每天搜索不同词条的个数等。

2.3 Geospatial

2.3.1 简述

Redis3.2中增加了对GEO类型的支持。GEO(Geographic 地理信息的缩写)该类型就是元素的2维坐标,在地图上就是经纬度。reids基于该类型提供了经纬度设置、查询、范围查询、距离查询、经纬度hash等常见操作。

2.3.2 Redis客户端中使用Geospatial命令

geoadd key longitude latitude member [longitude latitude member...] 添加地理位置(经度、维度、名称)

注意: 目前,北极和南极无法直接添加。一般通过下载城市数据在编写Java程序一次性导入有效的经度为-180度到180度。有效的维度为-8505112878到8505112878度。当坐标位置超出指定范围时,该命令将返回错误,且已经添加的数据无法再次添加进去。 bog0bt.png

geopos key member 根据名字获取对应的经度和维度

bo2ui8.png

geodist key member1 member2 m|km|ft|mi 获取两个位置之间的直线距离

由图知南京栖霞区到南京江宁区的直线距离为18.3027公里,南京栖霞区到上海市的直线距离为264.0680公里

boRpmn.png

georedius key longitude latitude radius m|km|ft|mi 以给定的经纬度为中心,找到某一半径内的元素

由图知在给的经纬度内有南京栖霞区和南京江宁去两个地方 boWHG8.png

参考:Redis:看完就比常人多会三种类型实战,可以拿去炫耀了

3. Redis事务

3.1 简述

Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行过程中,不会被其他客户端发送来的命令请求所打断。主要作用: 串联多个命令防止别的命令插队

3.2 Multi、Exec、discard

从输入Multi命令开始(开启事务),输入的命令会依次进入命令队列中但不会执行,直到输入Exec后(执行事务),Redis会将之前的命令discard来放弃组队(回滚事务)。 bHV7Mq.png bHVtr6.png

注意: 在组队阶段(即multi之后exec之前)如果输入了错误的命令(如set c,没输入value值)则exec之前的所有命令都会不执行,如果exec之前的命令都正确但在执行阶段出错(如在exec前写入 incr c1)则只是不会执行错误的命令

3.3 Redis事务

3.3.1 WATCH KEY

在执行multi之前,先执行watch key1 [key2]可以监视一个(或多个)key,如果在事务执行之前这个(或这些)key被其他命令所改动,那么事务将被打断

例如: 打开两个窗口,其中一个窗口先 set balance 100,然后两个窗口都对key=balance进行监视,再两个窗口都开启事务multi,然后一个窗口incrby balance 10另一个窗口incrby balance 20,使得两次操作都放入队列,最后两个窗口都执行exec操作执行事务,结果发现先执行的incrby balance 10执行成功,后执行的incrby balance 20执行失败,其中的原理就是乐观锁机制 bbIyDK.png

bbIR4H.png

3.3.1 Redis事务的三大特性(与MySQL事务不同)

单独隔离操作

  • 事务中的所有命令都会序列化、按顺序地执行。事务在执行过程中,不会被其他客户端发送来地命令请求所打断。

没有隔离级别的概念

  • 队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行

不保证原子性

  • 事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚

4. Redis持久化

4.1 RDB(Redis DataBase)

4.1.1 简述

在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是Snapshot快照,它恢复时是将快照文件直接读到内存中

4.1.2 redis中的数据如何进行备份?

Redis会单独创建(fork)一个子进程来进行持久化,会将数据写入一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能,如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOP方式更加高效。RDB的缺点最后一次持久化后的数据可能丢失。

4.1.3 Fork

  • Fork的作用是复制一个与当前进程一样的进程。新的进程的所有数据(变量、环境变量、程序计数器等)数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程
  • 在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,处于效率考虑,Linux中引入了写时复制技术
  • 一般情况父进程和子进程会共用同一段物理内存,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程

4.1.4 RDB优势

  • 适合大规模的数据恢复
  • 对数据完整性和一致性要求不高的时候更适合使用
  • 节省磁盘空间
  • 恢复速度更快

4.1.5 RDB劣势

  • Fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑
  • 虽然redis在Fork时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能
  • 备份周期在一定时间间隔做一次备份,所以如果Redis意外down掉了的话,就会丢失最后一次快照后的所有修改

参考: 初识Redis(三):Redis数据备份、恢复与持久化

4.2 AOF(Append Of File)

4.2.1 简介

以日志形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件内容将写指令从前到后执行一次完成数据的恢复工作(AOF是先执行命令,把数据写入内存,然后才记录日志)。

4.2.2 为何要先执行命令?

  1. 避免出现记录错误命令, 写后日志这种方式,就是先让系统执行命令,只有命令能执行成功,才会被记录到日志中,否则,系统就会直接向客户端报错。这样就避免在重放日志时候遇到错误的命令
  2. 它是在命令执行后才记录日志,所以不会阻塞当前的写操作。

4.2.3 AOF持久化流程

  1. 客户端的请求写命令会被append追加到AOF缓存区内
  2. AOF缓冲区根据AOF持久化策略[always,everysec,no]将操作sync同步到磁盘的AOF文件中
  3. AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量

4.2.4 AOF默认不开启

Redis默认开启的是RDB模式,可在redis.conf,将appendonly 改为yes这种配置文件名为appendonly.aof文件的保存路径与RDB一致

4.2.5 AOF和RDB同时开启时,redis听谁的呢?

当两个都开启后,系统默认读取AOF的数据(数据不会存在丢失)

4.2.6 AOF启动/修复/恢复

  • AOF的备份机制和性能虽然和RDB不同,但是备份和恢复的操作同RDB一样,都是拷贝备份文件,需要恢复时再拷贝到redis工作目录下,启动系统即加载。
  • 异常恢复
    1. 修改默认的appendonly no改为yes
    2. 如遇到AOF文件损坏,通过/usr/local/bin/redis-check-aof--fix appendonly.aof进行修复
    3. 备份被写坏的AOF文件
    4. 恢复:重启redis,然后重新加载

4.2.7 AOF同步频率设置

appendfsync always

始终同步,每次Redis的写入都会立刻记入日志;性能较差但数据完整性较好

appendfsync everysec

每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失

appendfsync no

redis不主动进行同步,将同步时机交给操作系统

4.2.8 Rewrite 压缩

什么是Rewrite压缩

AOF采用文件追加方式,文件会越来越大。为避免出现此种情况,新增了重写机制,当AOF文件的大小超过所设定的阈值时,reds就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集,可以使用命令bgrewriteaof

重写原理,如何实现原理?

AOF文件持续增长而过大时,会fork出一条新进程来将文件重写(也是先写临时文件最后在rename)。redis4.0版本后的重写,是指将rdb的快照以二进制的形式附在新的aof头部,作为已有的历史数据,替换掉原理的流水账操作。

触发机制,何时重写?

Redis会记录上次重写时的AOF大小,默认配置时当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时触发 重写虽然可以节约大量磁盘空间,减少恢复时间。但是每次重写还是有一定的负担的,因此设定redis要满足一定条件才会进行重写 auto-aof-rewrite-min-size设置重写的基准值,最小文件64MB。达到这个值开始重写。例如:文件达到70MB开始重写,降到50MB.下次什么时候开始重写?100MB。原因:系统载入时或者上次重写完毕时,redis会记录此时AOF大小,设为base_size,如果当前AOF大小>=base_size+base_size*100%(默认)且当前大小>=64MB(默认)的情况下,Redis会对AOF进行重写。

4.2.9 AOF优点

  • 备份机制更稳健,丢失数据概率更低
  • 可读的日志文本,通过操作AOF稳健,可以处理误操作

4.2.10 AOF缺点

  • 比起RDB占用更多的磁盘空间
  • 恢复备份速度要慢
  • 每次读写都同步的话,有一定的性能压力
  • 存在个别Bug,造成不能恢复

4.2.11 总结

用哪个好?

官方推荐两个都启用。如果对数据不敏感,可以单独选用RDB,不建议单独用AOF,因为可能会出现Bug,如果只是做纯内存缓存可以两个都不用

5. Redis主从复制

5.1 简述

主机数据更新后根据配置和策略,自动同步到备机的master/slaver机制,Master以写为主,Slaver以读为主

bXRGEF.png

5.2 主从复制能干嘛?

  • 读写分离,性能扩展
  • 容灾快速恢复(当一台读(从)服务器宕机,另外一台可以继续工作)

6. Redis集群

6.1 问题

  • 容量不够,redis如何进行扩容?集群!
  • 并发写操作,redis如何分离? 集群!

以及主从模式、主机宕机、导致IP地址变化,应用程序中配置需要修改对应主机地址、端口等信息。 之前有代理主机方式解决,后来Redis3.0提供无中心化集群配置方式的解决方案

6.2 什么是集群

Redis集群实现了对Redis的水平扩容,即启动N个redis节点,将整个数据库分布存储再这N个节点中,每个节点存储总数据的1/N。 Redis集群通过分区来提供一定程度的可用性:即使集群中有一部分节点失效或者无法进行通讯,集群也可以继续处理命令请求。

7. 应用问题解决

7.1 缓存穿透

7.1.1 简述

如果在请求数据时,在缓存层和数据库层都没有找到符合条件的数据,也就是说,在缓存层和数据库层都没有命中数据。 \underline{如果在请求数据时,在缓存层和数据库层都没有找到符合条件的数据,也就是说,在缓存层和数据库层都没有命中数据。} 那么,这种情况就叫缓存穿透。缓存穿透问题在一定程度上与缓存命中率有关。如果我们的缓存设计的不合理,缓存的命中率非常低,那么,数据访问的绝大部分压力都会集中在后端数据库层面。造成缓存穿透的主要原因就是:查询某个 Key 对应的数据,Redis 缓存中没有相应的数据,则直接到数据库中查询。数据库中也不存在要查询的数据,则数据库会返回空,而 Redis 也不会缓存这个空结果。这就造成每次通过这样的 Key 去查询数据都会直接到数据库中查询,Redis 不会缓存空结果。这就造成了缓存穿透的问题。

qS4yU1.png

7.1.2 解决方案

  • 1. 对空值缓存

如果一个查询返回的数据为空(不管数据是否存在,有可能存在但是redis没有命中该数据),我们仍然将这个空(null)结果进行缓存,同时设置空结果一个短的过期时间,最长不超过5分钟。

  • 2. 设置可访问的名单(白名单)

使用bitmaps类型定义一个可以访问的名单,名单id作为bitmaps的偏移量,每次访问和bitmaps里面的id进行比较,如果id不存bitmaps中,则进行拦截,不允许访问。

  • 3. 使用布隆过滤器

布隆过滤器的作用是某个 key 不存在,那么就一定不存在,它说某个 key 存在,那么很大可能是存在(存在一定的误判率)。于是我们可以在缓存之前再加一层布隆过滤器,在查询的时候先去布隆过滤器查询 key 是否存在,如果不存在就直接返回。 布隆过滤器原理:布隆过滤器,这一篇给你讲的明明白白

7.2 缓存击穿

7.2.1 简述

Redis中某个key过期了(不是大量key过期),此时若有大量针对该过期的key发起请求,可能会瞬间把后端DB压垮,造成缓存击穿

7.2.2 缓存击穿的特点

  • 数据库访问压力瞬时增加
  • Redis里面没有出现大量key过期
  • Redis正常运行

7.2.3 解决方案

Redis中某个key可能会在某个时间点被超高并发的访问,是一种非常“热点”的数据。这个时候,需要考虑缓存“被击穿”的问题。

  • 预先设置热门数据: 在redis高峰访问之前,把一些热门数据提前存入到redis里面,加大这些热门数据key的时长。例如: 商品秒杀活动中,提前将秒杀的商品的key存入redis中并延长过期时间。
  • 实时调整: 现场监控哪些数据热门,实时调整key的过期时间
  • 使用锁:
    1. 就是在缓存失效的时候(判断拿出来的值为空),不是立即取load db。
    2. 先使用缓存工具的某些带成功操作返回值的操作(比如:Redis的SETNX)去set一个mutex key。
    3. 当操作返回成功时,再进行load db操作,并回设缓存,最后删除mutex key。
    4. 当操作返回失败,证明有线程再load db,当前线程睡眠一段时间再重试整个get缓存的方法。

7.3 缓存雪崩

7.3.1 简述

在极少时间段内,Redis中大量key出现集中过期的情况,当大量请求访问服务器时,由于key过期导致数据库压力增大,最终导致服务器崩溃,即缓存雪崩

7.3.2 解决方案

缓存失效时的雪崩效应对底层系统的冲击非常可怕。

  • 构建多级缓存架构: nginx缓存+Redis缓存+其他缓存(ehache等)
  • 使用锁或队列: 用加锁或队列的方式保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。不适用高并发情况。
  • 设置过期标志更新缓存: 记录缓存数据是否过期(设置提前量),如果过期会触发通知另外的线程在后台去更新实际Key的缓存。
  • 将缓存失效时间分散开: 比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

7.4 分布式锁

7.4.1 简述

随着业务发展的需要,原单体单机部署的系统被演化为分布式集群系统后,由于分布式系统多线程、多进程且分布在不同机器上,这将使得原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!

7.4.1 解决方案

分布式锁主流的实现方案有:

  • 基于数据库实现分布式锁
  • 基于缓存(Redis等)
  • 基于Zookeeper

性能方面: Redis最高。可靠性方面: Zookeeper最高。这里我们介绍基于Redis实现分布式锁。

7.4.2 设置锁和过期时间

上锁同时是设置key的过期时间:

set users(key) 10(value) nx(加锁) ex(设置过期时间) 12(设置12s后过期)
复制代码

7.4.3 UUID防止锁的误删

首先,创建锁的时候,所对应的value村的时随机的UUID值,然后当删除锁之前判断当前锁对应的value是否等于获得的UUID值,若相等则删除该锁,若不相等则不删除锁。

7.4.4 LUA脚本保证删除原子性

场景: 对a业务上锁后进行具体操作,在释放锁时,先比较锁对应的value值UUID(防止误删),发现UUID一样,准备进行删除操作时(此时正打算删除,但是还没删),锁到了过期时间,自动释放了。此时,对业务B进行上锁,然后执行业务B的相关操作,然后A的删除操作开始执行(之前打算删除但没有删除)即释放B的锁(原因:不具备原子性操作)。 **解决方案:**使用LUA脚本

Redis6.0新特性-IO多线程

IO多线程是指客户端交互部分的玩阿格里IO交互处理模块多线程(默认不开启),而非执行命令多线程。 Redis6.0执行命令依然是单线程。

おすすめ

転載: juejin.im/post/7075696797347217416