Redis学习:BigKey、缓存双写一致性更新策略和案例
文章目录
1. BigKey
- 面试题
- MoreKey不可以使用keys * ,要使用SCAN基于游标来查询所有的key
- 通过在redis.conf配置文件中修改SECURITY配置项,将这些指令重命名或者禁用
- MEMORY USAGE key 来查询指定key所占用的字节数,也可以使用redis-cli –bigkeys 来查看每个类型占用字节最多的key,以及key的个数和平均大小
- BigKey是指某个key的value太大,string类型超过10k,其他类型hash\list\set\zset超过5000个元素就是bigkey,string类型可以直接使用del阻塞删除,其他类型不可以使用del,会造成阻塞,而是用渐进式删除,一部分一部分进行删除,通过HSCAN\SSCAN\ZSCAN来获取对应类型key中value中的数据,然后一部分一部分进行删除
- BigKey调优,因为redis有del阻塞删除和unlink非阻塞删除,而默认使用del阻塞删除,故可以在redis.conf配置文件中修改LAZYFREE配置项实现非阻塞删除,以进行优化
- MoreKey时,不可以直接使用Keys * 进行遍历,会严重阻塞,要使用SCAN 游标 模式 个数 命令基于游标进行遍历所有的key,每次会返回下一次遍历开始的游标以及近似个数的匹配的key
- MoreKey是指redis中key的个数过多,而BigKey是指某个Key的value过大
- SCAN是遍历Key,HSCAN\SSCAN\ZSCAN是遍历指定类型key中的value
- MoreKey案例(简历加分)
- 向Redis中插入大量数据
- 先使用linux操作创建一个文件,里面存放了一百万条set指令,使用管道指令将所有的指令在Redis中运行
- 使用DBSIZE可以查看Redis数据库中的key的个数
- 禁用keys * 等指令
- 数据量很大时,一定要避免使用keys * ,可以直接在配置文件中禁止该指令
- 当数据量很大时,不要使用keys * 和flushdb,因为Redis只有主线程串行执行命令,且keys * 是遍历实现的,故会非常耗费时间,造成Redis主线程卡顿,则此时后面的所有请求的命令均会卡顿
- 通过在redis.conf配置文件中设置SECURITY配置项禁止这些命令,或者重命名,更改配置文件后一定要先重启服务端
- 使用SCAN遍历Morekey
- SCAN是从游标开始遍历指定个数的匹配的key,而HSCAN\SSCAN\ZSCAN是遍历指定类型key中value的元素,是删除BigKey时遍历当前key中的元素进行删除
- 当redis数据库中key很多时,要禁止使用keys * 、FLUSHDB等危险指令,可以在redis.conf的SECURITY配置项中进行配置,此时如果要遍历数据库中的key,要使用SCAN指令基于游标进行遍历,SCAN指令是遍历所有的key,可以指定匹配的模式,以及遍历的大概数目,且返回下一次遍历的游标,当游标为0时说明遍历结束
- SCAN 游标 匹配模式 个数:SCAN是基于游标的迭代,每次要指定一个游标,返回下一次开始的游标以及近似数量的key,当游标返回0时说明匹配的key遍历结束
- 使用scan命令来查找指定的key(不使用keys * )使用SCAN遍历匹配的key,使用HSCAN/SSCAN/ZSCAN遍历指定 key(bigkey) 中的元素
- Scan命令用于迭代数据库中的数据库键,是基于游标的迭代器,当前遍历要基于上一次遍历的游标
- SCAN命令基于游标来遍历数据库中的所有key,游标从0开始,每次返回下一次遍历的游标和当前遍历的key(不保证数量),不是顺序遍历,而是高位进位加法遍历
- SCAN命令的意思就是对所有满足条件的key进行非顺序遍历,而是基于游标遍历,每次返回模糊数量的key,以及下一次遍历游标,直到所有的遍历结束返回游标0
- SCAN遍历所有的key,不是顺序遍历,而是基于游标进行遍历,高位加法遍历
- 向Redis中插入大量数据
- BigKey案例(key的value非常大)
- 多大的key算big
- MoreKey是指key非常多,BigKey是指某个key的value非常大
- BigKey是指value非常大,不是key大
- 非字符串的bigkey,不要使用del删除,而且要注意过期时间,当过期后会自动使用del进行删除,字符串的bigKey使用del进行阻塞删除,而非string的bigKey要使用渐进式删除,一部分一部分删除,或者使用lazyfree,要在配置文件中进行配置,默认是关闭的
- string类型的key的value最大是512M,但超过10K就是bigkey
- list\hash\set\zset类型的key,最多可以2^32 - 1 个元素,但数量超过5000就是bigkey
- 哪些危害:内存不均、超时删除、网络阻塞
- 如何产生:社交粉丝、报表积累
- 如何发现
- 使用redis-cli --bigkeys可以返回每个数据类型的最大的bigkey,以及该类型key的数目和平均大小,此时可以对最大的bigkey进行删除,string类型可以直接阻塞del删除,但非string不要使用del,而是渐进式删除,且对于key,可以通过MEMORY USAGE key,查看某个key所占用的内存字节数
- redis-cli –bigkeys
- 给出每个数据结构最大的bigkey,同时给出每个数据类型的个数 + 平均大小
- 但只给出TOP1的bigkey,无法查询所有的bigkey,要使用SCAN查询所有的key,并使用MEMORY USAGE key查看某个key占用内存
- –bigkeys底层使用SCAN进行扫描,可以使用 -i time 添加每100个SCAN的时间间隔
- MEMORY USAGE key
- 给出指定的key所占用的字节数
- 如何删除bigkey
- 非string类型先使用HSCAN/SSCAN/ZSCAN遍历指定key中的部分value,然后删除继续遍历,一部分一部分渐进式删除
- 使用HSCAN\ZSCAN\SSCAN遍历的是指定类型key中value的元素,然后再渐进式删除,遍历指定key中的value,再使用渐进式删除一部分一部分删除,字符串类型直接阻塞del删除,非string使用渐进式删除,且所有的key过期后均会默认使用del阻塞删除,要通过配置lazyfree配置项来开启非阻塞删除,key到期后默认使用del删除
- 非字符串的bigkey,不要使用del(del会直接全部删除),要使用渐进式删除,一部分一部分的删除,不要直接全部删除,且key过期后会自动默认使用del进行删除造成阻塞
- string类型使用del进行删除,或者使用unlink异步删除
- 非string类型不要用del直接删除,因为太大时会造成网络阻塞,要使用渐进式删除,一部分一部分删除
- hash类型使用渐进式删除,一部分一部分删除,通过HSCAN key查看当前hash类型的bigkey中的部分value,然后删除,继续遍历,一部分一部分渐进式删除,不要一下使用del全部删除
- list类型使用 LTRIM start end 进行修建,不在区间内的将被删除,list类型使用LTRIM修建,不在区间内的全部删除,故每次可以只修建前面指定个数的元素,直到末尾
- set类型使用SREM
- 多大的key算big
- BigKey生产调优 LazyFree配置项
- key过期默认是使用del阻塞删除的,如果对于string可以,但对于非string不要使用del进行删除,此时就应该修改配置文件,对lazyfree配置项进行配置,已开启非阻塞删除
- 默认是使用del阻塞删除的,故可以通过在配置文件中修改FLAZYFREE配置使其非阻塞删除进行优化
- redis提供两种删除方法:del(阻塞)、unlink(非阻塞),默认是使用del阻塞删除,在配置文件中修改lazyfree配置项
2. 缓存双写一致性更新策略
- 面试题:写策略
- 缓存和数据库一致性问题:更新数据时要保证MySQL数据库的准确性,故先更新MySQL再删除缓存,此时会出现脏读,但可以保证最终一致性;读取数据时,当缓存不存在则要从MySQL读取,如果高并发必须加锁,否则会全打到MySQL上,而且加锁后要再检查是否已经写入缓存,此时就可以一次MySQL操作实现缓存回写,即双检加锁;如果先删除缓存再更新数据库,则为了保证最后的一致性,在MySQL更新前如果有读取缓存,则会从旧的MySQL中读出数据并写回缓存,此时MySQL更新后因为不会重新删除缓存,就会导致不一致,故要使用双删延迟,即先删除缓存,然后更新数据库,然后延迟一个读操作保证之间的读操作已经写回缓存,最后再删除缓存,此时就算有读操作也是从更新后的数据库读取数据写入缓存
- 为了保证MySQL和redis缓存的一致性,可以使用canal中间件对MySQL的增量文件进行解析订阅和消费,其实时监控MySQL的binlog,可以在canal客户端连接canal服务端然后subscribe订阅指定数据库的库和表,此时就可以获得增量文件中未确认的数据,即变动的数据,然后对这些数据打印并进行相应的处理,以实现和Redis缓存的一致性
- 缓存双写一致性
- 一致性,如果redis中有数据则必须与数据库一致;如果redis无数据,则保证数据库中为最新值,且要准备写入redis
- 缓存可以分为只读缓存和读写缓存,对于只读缓存不需要回写操作,对于读写缓存必须保证回写的一致性
- 对于读写缓存,当redis中无数据时,有两种回写策略:同步直写和异步缓写,同步直写是将数据库数据直接写入redis缓存,异步缓写是先不写入缓存
- 如何实现:当读取redis无数据时,要使用双检加锁来防止MySQL击穿
- 双检加锁策略
- 检查两次缓存是否存在,第一次不存在时加锁访问MySQL将数据写入缓存,且在锁里再进行一次检查,保证只有一次查询MySQL,防止击穿MySQL
- 当同时查询缓存时,如果存在则直接返回,否则对第一个查询数据上加锁,由一个线程去查询并写入缓存,且在锁里面再做一次查询,此时后边等待的进程进入锁后会直接返回,保证只进行一次MySQL查询
- 当并发量很大时,缓存不存在时可能全部去MySQL查找造成缓存击穿,且可能会redis数据覆盖,故必须使用双检加锁策略,读取redis数据时必须使用双检加锁,第一次检查缓存无数据时,加锁去从MySQL中读取数据,且在锁中再检查一次,不存在时再去读取MySQL,此时就可以保证只有一次访问MySQL;更新数据时,使用先更新数据库再删除缓存策略,或者双删延迟
- 当并发量很小时,可以如下写法,可以不加锁,但并发量很大时,必须加锁访问MySQL
- 双检加锁策略
- 数据库和缓存一致性的几种更新策略
- 以MySQL写入库的数据为准,要保证最终一致性
- 可以停机的情况
- 直接停机后保证数据的一致性
- 不可以停机更新策略
没有完美的方案,都不可避免的会出现脏读,但要保证最终数据的一致性
只有缓存不存在时才会从MySQL中读取数据并回写,当更新MySQL后不会进行回写,更新MySQL数据库后不会回写进入缓存,但读取redis不存在时会双检加锁读取MySQL再写入redis,要保证以MySQL为准,故必须先更新MySQL- 先更新数据库再更新缓存×
- 如果缓存更新失败则会导致不一致,更新缓存时可能导致不一致
- 先更新数据库再删除缓存不会造成最终的数据不一致,但要是更新缓存则会造成数据不一致
- 且并发执行时,缓存更新结果可能不一致
- 先更新缓存再更新数据库×
- 要保证以MySQL为准,故必须先更新MySQL
- 不推荐,因为要以MySQL为准,故要保证MySQL数据库始终正确
- 且高并发下也会出现顺序错误,有快有慢使得更新顺序错误
- 先删除缓存再更新数据库
- 因为更新后不会修改redis,故可能会造成最终数据的不一致,故必须使用双删延迟
- 延时双删:就是更新前先删除缓存的值,然后更新,更新后再删除缓存的值,第二次删除要延时一个读操作的时间,保证读操作已经回写进入缓存后再进行删除,此时不可避免的会出现有一次读操作会读到脏数据,但可以保证最终数据一致性,但是一次读操作时间不好估计
- 如果不使用延时双删,则可能会导致缓存和数据库始终不一致,如还未 更新MySQL就读缓存,将旧数据放入缓存中,此时MySQL更新完成,则此时的缓存和MySQL就不一致,更新完MySQL后不会再更新缓存,故必须延时双删
- 当缓存中不存在时,就回去MySQL数据库中查找值
- 可能出现A更新MySQL还未完成时,B读缓存不存在然后读MySQL并回写缓存,此时A再更新MySQL就会出现不一致,可以使用延时双删来解决
- 通过回写来保证缓存的一致性,当缓存没有时会从数据库回写进入缓存
- 但如果删除缓存后正在更新MySQL但还未完成,此时访问数据就会读到MySQL中的旧数据,且还会把旧值写回缓存
- 先更新数据库再删除缓存
- 要保证MySQL为准,故必须先更新MySQL,然后删除缓存可以保证最终数据一致性
- 不可避免的会出现脏读,但要保证最终数据的一致性
- 此时会存在幻读,当MySQL还未更新时有线程读操作就会读到更新前的旧值
- 先更新数据库再更新缓存×
- 小总结
- 读取缓存使用双检加锁,更新数据时使用先更新数据库再删除缓存
- 缓存双写一致性,要保证最终数据一致性,当缓存数据不存在时使用双检加锁来从MySQL中回写入缓存中,读取缓存时要使用双检加锁,更新数据时要先更新数据库再删除缓存,此时读取读到不存在时使用双检加锁
- 更新数据时优先使用先更新数据库再删除缓存策略来保证缓存和数据库最终数据的一致性
- 如果使用先删除缓存再更新数据库,则要使用双删延迟,先删除缓存然后更新数据库,然后延迟一段时间后再删除缓存,此时延迟的时间不好估计,如果不使用双删延迟,则可能会导致最终不一致,因为更新完数据库后不会进行回写
3. 缓存双写一致性案例
- 复习与面试题
- 要保证最终一致性,且以MySQL为准
- 故更新时使用先更新数据库再删除缓存,读取时使用双检加锁
- 如何将MySQL的改变同步反应到缓存中,使用canal中间件监听binlog日志去监控MySQL的改变,并在客户端实时写入Redis
- Canal:监听MySQL并通知Redis
- 是什么:基于MySQL数据库增量日志解析,提供增量数据订阅和消费
- 类似于Redis的AOF持久化,将MySQL的增量数据保存并发布给其他组件
- 基于MySQL数据库增量日志解析,提供增量数据订阅和消费
- 工作原理
- MySQL主从复制原理,MySQL有一个binlog日志,记录增量
- Canal工作原理,基于MySQL增量日志解析,提供订阅和消费
- MySQL-canal-Redis双写一致性
- 用canal实时监控MySQL的更新,并同步给Redis,基于MySQL数据库增量日志解析,提供增量数据订阅和消费
- MySQL要开启binlog并配置canal账户
- 要配置MySQL开启binlog二进制文件,并配置一个canal账户授予权限
- 开启MySQL的binlog写入功能,MySQL8默认开启
- 授权canal连接MySQL账号,默认MySQL中没有canal用户,此时要新建+授权
- Canal服务端
- 下载:在github上下载alibaba的canal开发版本
- 解压
- 配置
- 客户端会读取默认的配置文件,当设置了则进行覆盖
- 要保证canal能够连接到MySQL的服务端,故要配置MySQL的IP和Port,并换成自己在MySQL上创建的canal用户
- Canal客户端
使用Java编写canal客户端来监控MySQL数据库的变动并通知给Redis- 建项目
- 改POM:要引入canal有关的依赖,去github的wiki操作文档
- 写YML
- 主启动
- 业务类
- RedisUtils
- 返回jedis连接的工具类,内部创建的jedis连接池,可以返回一个jedis连接
- RedisCanalClientExample
- 去github的官网上找example
- 在main中先创建canal连接,然后订阅指定库表的变动(不要订阅全部库的全部表),然后获得MySQL中监听到的未确认的数据,如果为0说明没有数据改变,此时继续监听,如果不为0说明有数据变动,则打印printEntry获得的数据变动,然后对这些变动进行确认
- 在printEntry函数中,此时传入了数据变动的Entry,要对每个entry进行自定义的业务处理以及打印,for循环遍历所有的entry,然后判断每个变动的类型,根据类型调用相应的方法对Redis进行实现相应的操作,原始操作只是打印,如果要实现同步数据到Redis,要自己创建相应的函数来实现Redis数据的更新
- 通过canal客户端和服务端可以实现对MySQL数据库指定库表数据的实现监控,并得到变动的数据根据业务需求将变动数据更新到redis中,可以在canal客户端中编写相应的功能实现对Redis数据的一致性更新,相应操作根据业务需求自己编写
- 在main函数中获得canal连接后,要通过**connector.subscribe(“库.表(正则表达式)”)**订阅指定库表的变动,此时不要订阅所有的库表,否则会监控所有变动造成很大开销,也可以配置黑名单来指定不监控的库和表
- 当canal客户端某个配置未显式配置时,会自动读取配置文件中的配置,当配置了会覆盖配置文件中的配置,而默认是订阅所有的库和表
- try-with-resources释放资源,在try指令内的部分对象会自动关闭,不需要自己释放
- RedisUtils
- 小总结
- 通过使用canal中间件,监控MySQL数据库的变动并实时回写进入Redis实现强一致性
- canal服务端会自动监听MySQL的数据变动,然后创建canal客户端来获得监听数据并回写到指定的Redis中
- 是什么:基于MySQL数据库增量日志解析,提供增量数据订阅和消费