Redis 缓存问题(13)

Stella981
• 阅读 322

缓存使用场景

针对读多写少的高并发场景,我们可以使用缓存来提升查询速度。

当我们使用Redis作为缓存的时候,一般流程是这样的:

Redis 缓存问题(13)

因为这些数据是很少修改的,所以在绝大部分的情况下可以命中缓存。但是,一旦被缓存的数据发生变化的时候,我们既要操作数据库的数据,也要操作Redis的数据,所以问题来了。现在我们有两种选择:

  1. 先操作Redis的数据再操作数据库的数据
  2. 先操作数据库的数据再操作Redis的数据到

底选哪一种?首先需要明确的是,不管选择哪一种方案,我们肯定是希望两个操作要么都成功,要么都一个都不成功。不然就会发生Redis跟数据库的数据不一致的问题。

但是,Redis的数据和数据库的数据是不可能通过事务达到统一的,我们只能根据相应的场景和所需要付出的代价来采取一些措施降低数据不一致的问题出现的概率,在数据一致性和性能之间取得一个权衡。

对于数据库的实时性一致性要求不是特别高的场合,比如T+1的报表,可以采用定时任务查询数据库数据同步到Redis的方案。

由于我们是以数据库的数据为准的,所以给缓存设置一个过期时间,是保证最终一致性的解决方案。

选择方案

一、先更新数据库,再删除缓存

正常情况:

 更新数据库,成功。
 删除缓存,成功。

异常情况:

1.更新数据库失败,程序捕获异常,不会走到下一步,所以数据不会出现不一致。

2.更新数据库成功,删除缓存失败。数据库是新数据,缓存是旧数据,发生了不一致的情况。

这种情况我可以提供一个“重试机制”来解决。比如:如果删除缓存失败,我们捕获这个异常,把需要删除的 key 发送到消息队列。然后自己创建一个消费者消费,尝试再次删除这个 key。这种方式会对业务代码造成入侵。

所以我们使用另外一种方案“异步更新缓存” 因为更新数据库时会产生binlog日志,所以我们可以通过一个服务来监听binlog的变化(如:maxwell 或 canal ),然后在客户端完成删除 key 的操作。如果删除失败的话,再发送到消息队列。

总之,对于后删除缓存失败的情况,我们的做法是不断地重试删除,直到成功。无论是重试还是异步删除,都是最终一致性的思想。

二、先删除缓存,再更新数据库

正常情况:

 删除缓存,成功。
 更新数据库,成功。

异常情况: 1、删除缓存,程序捕获异常,不会走到下一步,所以数据不会出现不一致。

2、删除缓存成功,更新数据库失败。 因为以数据库的数据为准,所以不存在数据不一致的情况。看起来好像没问题,但是如果有程序并发操作的情况下:

 a.  线程 A 需要更新数据,首先删除了 Redis 缓存
 b.  线程 B 查询数据,发现缓存不存在,到数据库查询旧值,写入 Redis,返回
 c.  线程 A 更新了数据库

这个时候,Redis 是旧的值,数据库是新的值,发生了数据不一致的情况。

那问题就变成了:能不能让对同一条数据的访问串行化呢?代码肯定保证不了,因为有多个线程,即使做了任务队列也可能有多个服务实例。数据库也保证不了,因为会有多个数据库的连接。只有一个数据库只提供一个连接的情况下,才能保证读写的操作是串行的,或者我们把所有的读写请求放到同一个内存队列当中,但是这种情况吞吐量太低了。

所以我们可以使用“延时双删”的策略,在写入数据之后,再删除一次缓存。

A 线程:
  1)删除缓存
  2)更新数据库
  3)休眠 500ms(这个时间,依据读取数据的耗时而定)
  4)再次删除缓存

高并发问题

在 Redis 存储的所有数据中,有一部分是被频繁访问的。有两种情况可能会导致热点问题的产生,一个是用户集中访问的数据,比如抢购的商品,明星结婚和明星出轨的微博。还有一种就是在数据进行分片的情况下,负载不均衡,超过了单个服务器的承受能力。热点问题可能引起缓存服务的不可用,最终造成压力堆积到数据库。

出于存储和流量优化的角度,我们必须要找到这些热点数据。

热点数据发现

除了自动的缓存淘汰机制之外,怎么找出那些访问频率高的 key 呢?或者说,我们可以在哪里记录 key 被访问的情况呢?

1. 客户端

我们可以在所有调用get、set方法的地方加上Key的计数。但这样的话但是这样的话,每一个地方都要修改,重复的代码也多。如果我们用的是 Jedis 的客户端,我们可以在 Jedis 的 Connection 类sendCommand()里面,用一个 HashMap 进行 key 的计数。

但有这种方式有几个问题:

 1.不知道要存多少个Key,如果数量较大,可能会发生内存泄漏的问题。
 2.会对客户端代码造成侵入。
 3.只能统计当前客户端的热点。

2. 代理层

第二种方式就是在代理端实现,比如 TwemProxy 或者 Codis,但是不是所有的项目都使用了代理的架构。

3. 服务端

第三种就是在服务端统计,Redis 有一个 monitor 的命令,可以监控到所有 Redis执行的命令。如:

jedis.monitor(new JedisMonitor() {
    @Override
    public void onCommand(String command) {
        System.out.println("#monitor: " + command);
    }
});

Facebook 的 开 源 项 目 redis-faina就是基于这个原理实现的。它是一个 python 脚本,可以分析 monitor 的数据。

redis-cli -p 6379 monitor | head -n 100000 | ./redis-faina.py

这种方法也会有两个问题:

 1)monitor 命令在高并发的场景下,会影响性能,所以不适合长时间使用。
 2)只能统计一个 Redis 节点的热点 key。

4.机器层面

还有一种方法就是机器层面的,通过对 TCP 协议进行抓包,也有一些开源的方案,比如 ELK 的 packetbeat 插件。

当我们发现了热点 key 之后,我们来看下热点数据在高并发的场景下可能会出现的问题,以及怎么去解决。

缓存雪崩

缓存雪崩就是 Redis 的大量热点数据同时过期(失效),因为设置了相同的过期时间,刚好这个时候 Redis 请求的并发量又很大,就会导致所有的请求落到数据库。

解决办法:

 1. 加互斥锁或者使用队列,针对同一个 key 只允许一个线程到数据库查询
 2. 缓存定时预先更新,避免同时失效
 3. 通过加随机数,使 key 在不同的时间过期
 4. 缓存永不过期
 5. 事中可使用本地缓存(ehcache)+ 限流&降级(降级的目的是保证核心服务可用即使有损)

缓存穿透

Redis 缓存问题(13)

还有一种情况,数据在数据库和 Redis 里面都不存在,可能是一次条件错误的查询。在这种情况下,因为数据库值不存在,所以肯定不会写入 Redis,那么下一次查询相同的key 的时候,肯定还是会再到数据库查一次。那么这种循环查询数据库中不存在的值,并且每次使用的是相同的 key 的情况,我们有没有什么办法避免应用到数据库查询呢?

(1)缓存空数据 
(2)缓存特殊字符串,比如&&

我们可以在数据库缓存一个空字符串,或者缓存一个特殊的字符串,那么在应用里面拿到这个特殊字符串的时候,就知道数据库没有值了,也没有必要再到数据库查询了。

但是这里需要设置一个过期时间,不然的话数据库已经新增了这一条记录,应用也还是拿不到值。

这个是应用重复查询同一个不存在的值的情况,如果应用每一次查询的不存在的值是不一样的呢?即使你每次都缓存特殊字符串也没用,因为它的值不一样,比如我们的用户系统登录的场景,如果是恶意的请求,它每次都生成了一个符合 ID 规则的账号,但是这个账号在我们的数据库是不存在的,那 Redis 就完全失去了作用。

这种因为每次查询的值都不存在导致的 Redis 失效的情况,我们就把它叫做缓存穿透。这个问题我们应该怎么去解决呢?

经典面试题

其实它也是一个通用的问题,关键就在于我们怎么知道请求的 key 在我们的数据库里面是否存在,如果数据量特别大的话,我们怎么去快速判断。

如何在海量元素中(例如 10 亿无序、不定长、不重复)快速判断一个元素是否存在?

如果是缓存穿透的这个问题,我们要避免到数据库查询不存的数据,肯定要把这 10亿放在别的地方。这些数据在 Redis 里面也是没有的,为了加快检索速度,我们要把数据放到内存里面来判断,问题来了:

如果我们直接把这些元素的值放到基本的数据结构(List、Map、Tree)里面,比如一个元素 1 字节的字段,10 亿的数据大概需要 900G 的内存空间,这个对于普通的服务器来说是承受不了的。

所以,我们存储这几十亿个元素,不能直接存值,我们应该找到一种最简单的最节省空间的数据结构,用来标记这个元素有没有出现。

这个东西我们就把它叫做位图,他是一个有序的数组,只有两个值,0 和 1。0 代表不存在,1 代表存在。

具体的关于BitMap与布隆过滤器的内容,可以转到另外一片文章: 《布隆过滤器》

缓存预热

缓存预热顾名思义,就是在系统上线后,将相关缓存数据直接加载到缓存系统中。

这样就可以避免用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接获取被预热的缓存数据。

方案:

 1. 写一个缓存刷新页面,上线后人工请求一下
 2. 数据量不大,可以在项目启动时自动进行加载(要考虑集群部署多服务,重复初始化问题)
点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Stella981 Stella981
2年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Easter79 Easter79
2年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
2年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
2年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
3个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这