Redis 缓存处理策略——基于商铺查询
Preword
文章基于黑马点评中商铺查询功能, 来讲解Redis做缓存时要采用什么样的处理策略解决一致性/穿透/雪崩/击穿等问题.
并且每种处理策略都有对应的java代码实现. 当掌握了不同的处理策略代码如何编写后, 可以将代码封装为工具类, 来提高代码复用性.
引子
众所周知, Redis相较于SQL具有高效读写的特点, 在处理大量的请求时有更好的性能. 因此非常适合作为缓存, 来降低数据库的压力, 可以大大提高后端性能.
但是Redis作为缓存会存在一系列问题需要我们解决.
Q1: Redis中的缓存如何确保和数据库中的一致性?
有时我们需要数据库与缓存具备高一致性, 如果数据库更新了, 结果缓存还是老数据, 怎么办?
第一: 我们可以给缓存加入一个有效期, 当到期之后必须去数据库拿新数据来作为新缓存.
第二: 我们可以主动更新. 即数据库更新时, 把缓存也更新. 但是每次数据库更新都要更新缓存吗? 如果数据库更新了100次都无人访问, 那缓存更新100次只是浪费资源, 没有意义. 那么我们绝对不是数据库更新的时候顺便更新缓存, 而是直接数据库更新时直接删除掉缓存, 当用户来访问时再从数据库中建立缓存. 这样就可以防止资源浪费.
还有另一个问题, 既然已经知道是删除缓存, 那么到底是先更新数据库再删缓存, 还是先删缓存再更新数据库?
两种操作都会发生异常, 我们肯定选择发生异常概率低的情况. 若先删缓存再改库, 发生异常为线程一删除缓存后, 线程二进来查库又写缓存, 然后线程一改数据库内容. 因为删缓存极快, 改库操作较慢, 此时中间插入线程二操作概率会比较大. 若先改库再删缓存, 则异常情况为缓存首先失效, 此时线程一查询数据需要读数据库, 而在查询完数据库后线程二又改数据库, 删除缓存, 然后线程一又将读到的数据库旧数据写入缓存, 此时会造成数据库数据为新, 而缓存数据为旧数据. 此时异常发生条件非常严苛, 需要缓存失效时发生读取操作, 读取操作过程中又发生数据库改数据, 发生概率非常小. 因此先改库再删缓存.
- so, 代码如何实现?
总结一下, 我们可以采用主动更新 + 超时剔除
- 删除缓存 or 更新缓存? -> 删除缓存, 等有查询了再更新, 若更新缓存但无查询, 则更新没有意义.
- 先操作数据库, 再删缓存 or 反 -> 两种操作都会发生异常, 我们肯定选择发生异常概率低的情况. 若先删缓存再改库, 发生异常为线程一删除缓存后, 线程二进来查库又写缓存, 然后线程一改数据库内容. 因为删缓存极快, 改库操作较慢, 此时中间插入线程二操作概率会比较大. 若先改库再删缓存, 则异常情况为缓存首先失效, 此时线程一查询数据需要读数据库, 而在查询完数据库后线程二又改数据库, 删除缓存, 然后线程一又将读到的数据库旧数据写入缓存, 此时会造成数据库数据为新, 而缓存数据为旧数据. 此时异常发生条件非常严苛, 需要缓存失效时发生读取操作, 读取操作过程中又发生数据库改数据, 发生概率非常小. 因此先改库再删缓存.
Q2: 缓存穿透
缓存穿透: 即用户访问的参数根本不存在, 缓存肯定没有, 请求肯定就直接打到数据库. 如果大量请求都这样, 数据库肯定直接gg了, 那怎么解决?
- 缓存置空 -> 将redis中添加空缓存, 并设置有效期
- 布隆过滤器 -> 添加布隆过滤器, 若无一定无, 若有不一定有.
Q3: 缓存雪崩
即大量的缓存同时过期/失效了, 这时大量的请求直接打到数据库, 肯定也会直接gg.
Q4: 缓存击穿
大量请求访问并且重建耗时较长的key过期以后, 大量请求访问到数据库, 给数据库造成巨大压力.
- 解决方案在一致性和高性能之间做抉择
- 互斥锁: key过期后一个线程进行恢复, 并且上锁, 此时其他线程只能等待, 导致性能变差. 但所有线程拿到的都是新数据, 一致性高.
- 逻辑过期: key不设置TTL, 而是在value中写入TTL. 线程对TTL进行校验, 如果过期则单开线程进行更新. 此时所有线程拿到旧数据返回, 一致性较差, 但不会像互斥锁那样等待, 因此效率高.
封装工具类
因为里面的泛型啊, 还有function参数马尿还没掌握的非常熟练, 先留坑吧. 难受了.