Redis是一种性能很高的NoSQL数据库. 它的优点是增查改删时的高性能, 但缺点也很明显: 它是基于内存读写实现的. 这是它能够实现高性能的秘诀, 但也导致它一旦崩溃, 数据也会丢失.
当然, 我们可以开启Redis自带的持久化功能, 将数据定期保存回磁盘. 但即便如此, 一旦遇到突发情况, 仍有少部分数据会丢失. 因此, 如何将Redis的数据持久化回后边的基于磁盘的SQL数据库, 并保证数据的一致性, 是必须讨论的课题.
-
传统方案(多为旁路缓存策略) 传统方案(旁路缓存)中, Redis作为缓存一般设置了有效期, 以保证数据最新. 具体的实现大概分为四种:
- 先写缓存, 再写数据库
- 先写数据库, 再写缓存
- 先删缓存, 再写数据库
- 先写数据库, 再删缓存
我们来逐条分析:
- 第一种思路问题最典型. 写缓存后, 若写数据库因为网络等原因失败了, 缓存中的数据便成了错误数据. 即便可以通过多次重试减小数据不一致的概率, 但是很长一段时间内缓存有数据而数据库无数据一般是不可接受的, 应当尽量避免, 除非没有更佳的方案或任务本身就采用延迟写入数据库的策略(write-back).
- 第二种思路则存在数据库是新数据, 而缓存是旧数据, 两侧数据不一致的情况. 并发时更是有旧请求写缓存比新请求晚的情况, 导致缓存了旧值, 数据库却正常. 避免起来比较麻烦, 因为控制不了数据库的写入速度. 纠正起来也比较麻烦, 你怎知新写的缓存到底是对是错? 这种方案因为后写缓存, 相对于读请求而言生效慢, 而且完全无法用在延迟写入数据库策略中.
- 第三种思路的问题很浅显. 先删了缓存, 但数据库还未写入的时间段内, 若有请求打来, 会有几率从数据库访问到旧值, 并更新缓存为旧值. 同时, 因为删了缓存, 同样存在缓存生效较慢且无法用在延迟写入策略中的问题.
- 第四种思路在旁路缓存策略下看上去很靠谱. 但是仍存在一个小小的问题: 若在数据库写完前, 缓存过期, 去数据库访问到了旧值, 并在删掉缓存后把缓存更新为旧值, 该如何应对? 这个问题实际上其他思路也会遇到, 只不过其他思路彼得问题更大. 解决起来其实蛮方便的, 在删除缓存后过一段时间再删一次即可. 这就是传说中的缓存双删. 只删第二次是不够的, 因为不能让旧值存活如此之长的时间. 但是同样, 该方案存在缓存生效较慢且无法用在延迟写入策略中的问题.
-
我就是想使用延迟写入策略, 而且是bitmap点赞这样的大key, 该怎么设计缓存逻辑? (本人遇到的实际情况)
这种复杂情况确实多出现在点赞这样密集读写的业务中. 延迟写入增强了SQL数据库的写性能, bitmap点赞节约Redis内存. 因为value过大, 不适用于设置过期策略, 且bitmap一位只能表示有无点赞, 不能表示过期/未知等信息, 故本人设计在读取时以固定概率随机不信任Redis, 去访问数据库以同步信息. 这样一来, 时间久了, 缓存将会无限贴近数据库的真实值. 因为缓存永不过期+延迟写入策略, 写入时当然是先写缓存后写数据库.
新问题来了, 既然是随机不信任Redis, 那么在更新缓存后但数据库还未被写入这个时间段内, 若有请求命中随机值而不信任缓存, 不就会把缓存更新为旧值吗? 而且, 与上述思路相似, 在写入请求前若有随机不信任缓存的请求得到了旧值, 在写入请求的更新缓存后将缓存再次更新为旧值, 该怎么办呢? 本人提出的方案是可以参考缓存双删, 做缓存双写, 并且第一次写入使用带有过期时间的新key作为缓存的缓存兼随机不信任机制的锁, 每次读取都先查找这个新key是否存在, 若存在则优先读取它并不触发随机同步. 这样一来, 写入请求会先更新新key, 在它的过期时间前对于被写入者的读取皆会打在这个新key上(因为新key要被写回数据库, 一定是正确的). 如此, 这一段时间内的读取请求都有了保障, 一定是正确的, 并且不会触发随机同步. 而写入请求以前若有触发随机同步, 可以随意使用其返回的旧值更新被写入者, 反正过一段时间后的第二次写入会将被写入者修改为正确的新值. 由于随机双写策略的第二次写入延时是固定的, 若有多个写入请求, 只会按顺序将新key更新为最新的值, 保证读取请求的正确性, 且第二次写入也是按顺序执行的, 保证被写入者在所有流程结束后本身的正确性.