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数据库后不会回写进入缓存,但读取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还未更新时有线程读操作就会读到更新前的旧值
- 小总结
3. 缓存双写一致性案例
- 复习与面试题
- 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数据库增量日志解析,提供增量数据订阅和消费