使用 redis 缓存的经验

本贴最后更新于 2591 天前,其中的信息可能已经沧海桑田

问题:怎么保持缓存与数据库一致?

要解答这个问题,我们首先来看不一致的几种情况。我将不一致分为三种情况:

1. 数据库有数据,缓存没有数据;

2. 数据库有数据,缓存也有数据,数据不相等;

3. 数据库没有数据,缓存有数据。

在讨论这三种情况之前,先说明一下我使用缓存的策略,也是大多数人使用的策略,叫做 Cache Aside Pattern。酷壳里的 缓存更新的套路 一文,很值得一读,我的策略也是从他那学来的。

简而言之,就是

1. 首先尝试从缓存读取,读到数据则直接返回;如果读不到,就读数据库,并将数据会写到缓存,并返回。

2. 需要更新数据时,先更新数据库,然后把缓存里对应的数据失效掉(删掉)。

读的逻辑大家都很容易理解,谈谈更新。如果不采取我提到的这种更新方法,你还能想到什么更新方法呢?大概会是:先删除缓存,然后再更新数据库。这么做引发的问题是,如果 A,B 两个线程同时要更新数据,并且 A,B 已经都做完了删除缓存这一步,接下来,A 先更新了数据库,C 线程读取数据,由于缓存没有,则查数据库,并把 A 更新的数据,写入了缓存,最后 B 更新数据库。那么缓存和数据库的值就不一致了。

另外有人会问,如果采用你提到的方法,为什么最后是把缓存的数据删掉,而不是把更新的数据写到缓存里。这么做引发的问题是,如果 A,B 两个线程同时做数据更新,A 先更新了数据库,B 后更新数据库,则此时数据库里存的是 B 的数据。而更新缓存的时候,是 B 先更新了缓存,而 A 后更新了缓存,则缓存里是 A 的数据。这样缓存和数据库的数据也不一致。

按照我提到的这种更新缓存的策略,理论上也是有不一致的风险的,酷壳的文章有提到,只不过概率很小,我们暂时可以不考虑,后面我们有其他手段来补救。

讨论完使用缓存的策略,我们再来看这三种不一致的情况。

1. 对于第一种,在读数据的时候,会自动把数据库的数据写到缓存,因此不一致自动消除

2. 对于第二种,数据最终变成了不相等,但他们之前在某一个时间点一定是相等的(不管你使用懒加载还是预加载的方式,在缓存加载的那一刻,它一定和数据库一致)。这种不一致,一定是由于你更新数据所引发的。前面我们讲了更新数据的策略,先更新数据库,然后删除缓存。因此,不一致的原因,一定是数据库更新了,但是删除缓存失败了。

3. 对于第三种,情况和第二种类似,你把数据库的数据删了,但是删除缓存的时候失败了。

因此,最终的结论是,需要解决的不一致,产生的原因是更新数据库成功,但是删除缓存失败。

我想出的解决方案大概有以下几种:

1. 对删除缓存进行重试,数据的一致性要求越高,我越是重试得快。

2. 定期全量更新,简单地说,就是我定期把缓存全部清掉,然后再全量加载。

3. 给所有的缓存一个失效期。

第三种方案可以说是一个大杀器,任何不一致,都可以靠失效期解决,失效期越短,数据一致性越高。但是失效期越短,查数据库就会越频繁。因此失效期应该根据业务来定。

哪些数据需要放在缓存中?

首先,缓存的对象有三种:

1. 数据库中单条的的数据(以表名跟 id 作为 key 永久保存到 redis),在有更新的地方都要更新缓存(不适用于需要经常更新的数据);

2. 对于一些不分页,不需要实时(需要多表查询)的列表,我们可以将列表结果缓存到 redis 中,设定一定缓存时间作为该数据的存活时间。用获取该列表的方法名作为 key,列表结果为 value;这种情况只试用于不经常更新且不需要实时的情况下。

3. 不需要实时的,需要分页的列表:可以把分页的结果列表放到一个 map(key 为分页标识,value 为分页结果)中,然后将该 map 存到 redis 的 list 中(用该方法名为 key)。然后给该 list 设置一个缓存存活时间(用 expire)。这样通过方法名 lrange 出来就能获取存有分页列表的数据,遍历该 list,通过遍历 list 中 map 的 key 判断该分页数据是否在缓存内,是则返回,不存在则 rpush 进去。这种做法能解决比如 1-5 页的数据已经重新加载,而 6-10 页的数据依然是缓存的数据而导致脏数据的情况。

本人走过的一些弯路:

1. 对于数据缓存不是所有东西都缓存到 redis 就是好的,而是要针对一些改动不大或者访问率大的数据进行缓存来减少关系型数据库的压力。

2. 不要试图在拦截器或者过滤器中判断是否有缓存的存在,因为每个请求(不管该请求对应的方法是否做了缓存)它都会去 redis 中请求数据并判断,这样会浪费一定的内存资源跟响应时间。所以应该针对需要缓存的方法进行判断。

3. 一个方法中使用多个 get 或者 set 的方法,我们需要尽可能的减少去 jedispool 中获取 jedis 对象,所以在一个方法中应该只获取一次 jedis 对象,在方法结束的时候把该对象 return 还给连接池,这样才能做到尽可能的高效。

4. 在设置连接池中参数的时候要考虑到自身系统需求,不然会经常出现连接池中无可用对象获取,spring 时不时发起连接请求到 redis 等不必要的错误和资源浪费。

  • Redis

    Redis 是一个开源的使用 ANSI C 语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库,并提供多种语言的 API。从 2010 年 3 月 15 日起,Redis 的开发工作由 VMware 主持。从 2013 年 5 月开始,Redis 的开发由 Pivotal 赞助。

    286 引用 • 248 回帖 • 2 关注
  • 经验
    25 引用 • 160 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • hacpaixixi via macOS

    另外有人会问,如果采用你提到的方法,为什么最后是把缓存的数据删掉,而不是把更新的数据写到缓存里。这么做引发的问题是,如果 A,B 两个线程同时做数据更新,A 先更新了数据库,B 后更新数据库,则此时数据库里存的是 B 的数据。而更新缓存的时候,是 B 先更新了缓存,而 A 后更新了缓存,则缓存里是 A 的数据。这样缓存和数据库的数据也不一致。

    不是很明白:为什么不是 a 先更新缓存,a 不是先更新的数据库么

  • dafsic

    A,B 两个线程同时更新数据,数据库里最终应该是 A 的数据还是 B 的,还是看 cpu 对谁好?而且我数据库读写分离,主从同步,这缓存又要怎么弄?

Changer0914
做才是得到,苦才是人生,残酷才是青春。 杭州

推荐标签 标签

  • RabbitMQ

    RabbitMQ 是一个开源的 AMQP 实现,服务器端用 Erlang 语言编写,支持多种语言客户端,如:Python、Ruby、.NET、Java、C、PHP、ActionScript 等。用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。

    49 引用 • 60 回帖 • 348 关注
  • SOHO

    为成为自由职业者在家办公而努力吧!

    7 引用 • 55 回帖
  • CSDN

    CSDN (Chinese Software Developer Network) 创立于 1999 年,是中国的 IT 社区和服务平台,为中国的软件开发者和 IT 从业者提供知识传播、职业发展、软件开发等全生命周期服务,满足他们在职业发展中学习及共享知识和信息、建立职业发展社交圈、通过软件开发实现技术商业化等刚性需求。

    14 引用 • 155 回帖
  • 微信

    腾讯公司 2011 年 1 月 21 日推出的一款手机通讯软件。用户可以通过摇一摇、搜索号码、扫描二维码等添加好友和关注公众平台,同时可以将自己看到的精彩内容分享到微信朋友圈。

    133 引用 • 796 回帖
  • Swift

    Swift 是苹果于 2014 年 WWDC(苹果开发者大会)发布的开发语言,可与 Objective-C 共同运行于 Mac OS 和 iOS 平台,用于搭建基于苹果平台的应用程序。

    36 引用 • 37 回帖 • 551 关注
  • WebSocket

    WebSocket 是 HTML5 中定义的一种新协议,它实现了浏览器与服务器之间的全双工通信(full-duplex)。

    48 引用 • 206 回帖 • 289 关注
  • Kafka

    Kafka 是一种高吞吐量的分布式发布订阅消息系统,它可以处理消费者规模的网站中的所有动作流数据。 这种动作(网页浏览,搜索和其他用户的行动)是现代系统中许多功能的基础。 这些数据通常是由于吞吐量的要求而通过处理日志和日志聚合来解决。

    36 引用 • 35 回帖 • 7 关注
  • App

    App(应用程序,Application 的缩写)一般指手机软件。

    91 引用 • 384 回帖
  • B3log

    B3log 是一个开源组织,名字来源于“Bulletin Board Blog”缩写,目标是将独立博客与论坛结合,形成一种新的网络社区体验,详细请看 B3log 构思。目前 B3log 已经开源了多款产品:SymSoloVditor思源笔记

    1063 引用 • 3455 回帖 • 154 关注
  • 正则表达式

    正则表达式(Regular Expression)使用单个字符串来描述、匹配一系列遵循某个句法规则的字符串。

    31 引用 • 94 回帖 • 1 关注
  • AngularJS

    AngularJS 诞生于 2009 年,由 Misko Hevery 等人创建,后为 Google 所收购。是一款优秀的前端 JS 框架,已经被用于 Google 的多款产品当中。AngularJS 有着诸多特性,最为核心的是:MVC、模块化、自动化双向数据绑定、语义化标签、依赖注入等。2.0 版本后已经改名为 Angular。

    12 引用 • 50 回帖 • 511 关注
  • Sublime

    Sublime Text 是一款可以用来写代码、写文章的文本编辑器。支持代码高亮、自动完成,还支持通过插件进行扩展。

    10 引用 • 5 回帖 • 1 关注
  • RYMCU

    RYMCU 致力于打造一个即严谨又活泼、专业又不失有趣,为数百万人服务的开源嵌入式知识学习交流平台。

    4 引用 • 6 回帖 • 58 关注
  • Visio
    1 引用 • 2 回帖
  • 音乐

    你听到信仰的声音了么?

    62 引用 • 512 回帖 • 3 关注
  • Openfire

    Openfire 是开源的、基于可拓展通讯和表示协议 (XMPP)、采用 Java 编程语言开发的实时协作服务器。Openfire 的效率很高,单台服务器可支持上万并发用户。

    6 引用 • 7 回帖 • 116 关注
  • sts
    2 引用 • 2 回帖 • 234 关注
  • Scala

    Scala 是一门多范式的编程语言,集成面向对象编程和函数式编程的各种特性。

    13 引用 • 11 回帖 • 159 关注
  • Hadoop

    Hadoop 是由 Apache 基金会所开发的一个分布式系统基础架构。用户可以在不了解分布式底层细节的情况下,开发分布式程序。充分利用集群的威力进行高速运算和存储。

    90 引用 • 122 回帖 • 624 关注
  • 叶归
    9 引用 • 41 回帖 • 20 关注
  • 小薇

    小薇是一个用 Java 写的 QQ 聊天机器人 Web 服务,可以用于社群互动。

    由于 Smart QQ 从 2019 年 1 月 1 日起停止服务,所以该项目也已经停止维护了!

    35 引用 • 468 回帖 • 761 关注
  • OpenResty

    OpenResty 是一个基于 NGINX 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。

    17 引用 • 53 关注
  • Node.js

    Node.js 是一个基于 Chrome JavaScript 运行时建立的平台, 用于方便地搭建响应速度快、易于扩展的网络应用。Node.js 使用事件驱动, 非阻塞 I/O 模型而得以轻量和高效。

    139 引用 • 269 回帖 • 1 关注
  • AWS
    11 引用 • 28 回帖 • 2 关注
  • 前端

    前端技术一般分为前端设计和前端开发,前端设计可以理解为网站的视觉设计,前端开发则是网站的前台代码实现,包括 HTML、CSS 以及 JavaScript 等。

    246 引用 • 1338 回帖
  • SMTP

    SMTP(Simple Mail Transfer Protocol)即简单邮件传输协议,它是一组用于由源地址到目的地址传送邮件的规则,由它来控制信件的中转方式。SMTP 协议属于 TCP/IP 协议簇,它帮助每台计算机在发送或中转信件时找到下一个目的地。

    4 引用 • 18 回帖 • 634 关注
  • 反馈

    Communication channel for makers and users.

    121 引用 • 907 回帖 • 278 关注