缓存架构设计

image.png

多级缓存架构当中每一层的意义

ngx_lua 属于 nginx的一部分,它的执行指令都包含在 nginx 的11个步骤之中了,相应的处理阶段可以做插入式处理,即可插拔式架构,不过 ngx_lua 并不是所有阶段都会运行的;另外指令可以在 http、server、server if、location、location if几个范围进行配置

1. nginx本地缓存

nginx本地缓存,抗的是热数据的高并发访问,根据场景设置缓存时间商品的购买总是有热点的,比如每天购买的iphone,nike海尔等知名品牌的东西的人,总是比较多的,这些热数据,利用nginx本地缓存,由于经常被访问,所以可以锁定在nginx的本地缓存内大量的热数据的访问,就是经常会访问的那些数据,就会被保留在nginx本地缓存内,那么对这些热数据的大量访问,就直接走nginx就可以了,不需要走后续的各种网络开销了.

2. redis分布式缓存

如果nginx本地缓存不命中,则nginx会访问redis集群服务,redis集群服务,抗的是很高的离散访问,因为redis的性能问题,能够支撑海量的数据,高并发的访问,提供高可用的服务.
nginx本地内存有限,也就能cache住部分热数据,除了热点数据,其他相对不那么热的数据,可能流量会经常走到redis那里,利用redis cluster的多master写入,横向扩容,1T+以上海量数据支撑,几十万的读写并发,99.99%高可用性,那么就可以抗住大量的离散访问请求.

3.多级缓存架构设计需要解决的问题

这里列了一些

  1. 数据库+缓存双写一致性解决方案
    面临难题:高并发场景下,如何解决数据库与缓存双写的时候数据不一致的情况?
    2.缓存维度化拆分解决方案
    面临难题:如何解决大value缓存的全量更新效率低下问题?
    3.缓存命中率提升解决方案
    面临难题:如何提升缓存命中率?
    4.缓存并发重建冲突解决方案
    面临难题:如何解决高并发场景下,缓存重建时的分布式并发重建的冲突问题?
    5.缓存预热解决方案
    面临难题:如何解决高并发场景下,缓存冷启动导致MySQL负载过高,甚至瞬间被打死的问题?
    6.缓存雪崩解决方案
    面临难题:如何解决恐怖的缓存雪崩问题?避免给公司带来巨大的经济损失?
    7.缓存穿透解决方案
    面临难题:如何解决高并发场景下的缓存穿透问题?避免给Mysql带来过大的压力?
    8缓存失效解决方案
    面临难题:如何解决高并发场景下的缓存失效问题?避免给redis集群带来过大的压力?

多级缓存、缓存维度划分、数据聚合、动态渲染

1.多级缓存实现

一般来说,缓存有两个原则.

  • 越靠近用户的请求越好.比如,能用本地缓存的就不要发送HTTP请求,能用CDN缓存的就不要打到源站,能用OpenResty缓存的就不要打到数据库.
  • 尽量使用本进程和本机的缓存解决.因为跨了进程和机器甚至机房,缓存的网络开销就会非常大,这一点在高并发的时候会非常明显.

自然,在OpenResty中, 缓存的设计和使用也遵循这两个原则,OpenResty中有两个缓存的组件:shared dict缓存和lru缓存. 前者只能缓存字符串对象,缓存的数据有且仅有一份,每一个worker都可以进行访问,所以常用于worker之间的数据通信,后者则可以缓存所有的lua对象,但只能在单个worker进程内访问,有多少个worker,就会有多少份缓存数据.

shared dict 和 lru 缓存的区别


image.png

image.png

先查看本地是否存在,如果存在直接返回,如果不存在再去查询redis,如果redis当中还不存在,就请求到源服务器,这就是跨机器,跨网络的多级请求,本地实际上也可以利用字典及lru实现多级缓存

2.缓存击穿问题

缓存击穿,是指某个极度热点数据在某个时间点过期时,恰好在这个时间点对这个KEY有大量的并发请求过来,这些请求发现缓存过期一般都会从DB加载数据并回设到缓存,但是这个时候大并发的请求可能会瞬间压垮DB.
先来设想下面一个场景.数据源在MySQL数据库中,缓存的数据放在共享字典中,超时时间为1分钟.在这1分钟内的时间里,所有的请求都从缓存中获取数据,MySQL没有任何的压力.但是,一旦到达一分钟,也就是缓存数据失效的那一刻,如果正好有大量的并发请求进来,在缓存中没有查询到结果,就要出发查询数据源的函数,那么这些请求全部都将去查询Mysql数据库,直接造成数据库服务卡顿,甚至卡死.
对于一些设置了过期时间的KEY,如果这些KEY可能会在某些时间点被超高并发地访问,是一种非常热点的数据,这个时候,需要考虑这个问题.

如何避免缓存击穿?

  1. 主动更新缓存:默认缓存是被动更新的.只有在中端请求发现缓存失效时,它才会去数据库查询新的数据.那么,如果我们把缓存的更新,从被动改为主动,也就可以直接绕开缓存风暴的问题了,在OpenResty中,我们可以使用ngx.timer.every来创建一个定时器去定时更新.
    缺点:每一个缓存都要对应一个周期性的任务:而且缓存过期时间和计划任务的周期时间还要对应好,如果这中间出现了什么纰漏,终端就可能一直获取到的都是空数据.
  2. 使用互斥锁:请求发现缓存不存在后,去查询DB前,使用锁,保证有且仅有一个请求去查询DB,并更新到缓存.流程如下:
  • 获取锁,直到成功或超时.如果超时,则抛出异常,返回.如果成功,则继续向下执行.
  • 再去缓存中.如果存在值,则直接返回;如果不存在,则继续往下执行,如果成功获取到锁的话,就可以保证只有一个请求去数据源更新数据,并更新到缓存中了.
  • 查询DB,并更新到缓存中,返回值.

OpenResty可以利用lua-resty-lock加锁,利用的是OpenResty自带的resty库,它底层是基于共享字典,提供非阻塞的lock API
不过,在上面lua-resty-lock的实现中,你需要自己来处理加锁,解锁,获取过期数据,重试,异常处理等各种问题,还是相当繁琐的,我们可以使用lua-resty-mlcache,看下图

image.png

Redis缓存穿透

缓存穿透是指查询一个一定不存在的数据,由于缓存不命中,并且处于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次都请求到要到存储层去查询,失去了缓存的意义.

  • 危害:对底层数据源(mysql,hbase,http接口,rpc调用等等)压力过大,有些底层数据源不具备高并发性.
  • 原因: 可能是代码本身或者数据存在的问题造成的,也很有可能是一些恶意攻击,爬虫等等(因为http读接口都是开放的)
  • 如何发现:可以分别记录cache命中数,以及总调用量,如果发现空命中(cache都没有命中)较多,则可能就会是缓存穿透问题.

解决思路:


image.png

1.缓存空对象
如果一个查询返回的数据为空,(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存(有一个比较巧妙的做法是,可以将这个不存在的key预先设定一个特定值.)但它的过期时间会很短,最长不超过5分钟.

  • 适用场景: 数据命中不高,数据频繁变化实时性高
  • 维护成本: 代码比较简单,但是有两个问题:
    第一:空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间(如果是攻击,问题更严重),比较有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除.
    第二:缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有一定影响.例如过期时间设置为5分钟,如果此时存储层添加了这个数据,那此段时间就会出现缓存层和存储数据的不一致,此时可以利用消息系统或者其他方式清除掉缓存层中的空对象.
  1. bloomfilter提前拦截
    通常如果想判断一个元素是不是一个集合里,一般想到的是将集合中所有元素保存起来,然后通过比较确定.链表,树,散列表(又叫哈希表,Hash table)等等数据结构都是这种思路.但是随着集合中元素的增加,我们需要的存储空间越来越大.同时检索速度也越来越慢.
    布隆过滤器原理
    布隆过滤器的原理是,当一个元素被加入集合时,通过K个Hash函数将这个元素映射成一个位数组中的K个点,把它们置为1.检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在,如果都是1,则被检元素很可能在.这就是布隆过滤器的基本思想.
    image.png

    数组的容量即使再大,也是有限的.那么随着元素的增加,插入的元素就会越来越多,位数组中被置为1的位置因此也越来越多,这就会造成一种情况:当一个不在布隆过滤器中的元素,经过同样规则的哈希计算之后,得到的值在位数组中查询,有可能这些位置因为之前其它元素的操作先被置为1了,所以,有可能一个不存在布隆过滤器中的会被误判断成在布隆过滤器中,这就是布隆过滤器的一个缺陷,但是,如果布隆过滤器判断某个元素不在布隆过滤器中,那么这个值就一定不在布隆过滤器中.
    总结就是:
    布隆过滤器说某个元素在,可能会被误判, 布隆过滤器说某个元素不在,那么一定不在
    对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃.它的优点是空间效率和查询时间都比较一般的算法要好的多,缺点是有一定的误判问题.
    适用场景:数据命中不高,数据相对固定实时性低(通常是数据集较大),比如爬虫URL去重,邮箱系统的垃圾邮件过滤,消息推送系统的用户推送过滤

Redis中的布隆过滤器:

https://github.com/RedisBloom/RedisBloom
Redis官方提供的布隆过滤器到了Redis4.0提供了插件功能之后才正式登场.布隆过滤器作为一个插件加载到Redis Server中,给Redis提供了强大的布隆去重功能.
布隆过滤器有两个基本指令,bf.add添加元素,bf.exists查询元素是否存在,它的用法和set集合的sadd和sismember差不多.注意bf.add只能一次添加一个元素,如果想要一次添加多个,就需要用到bf.madd指令.同样如果需要一次查询多个元素是否存在,就需要用到bf.mexists指令.
Redis还提供了自定义参数的布隆过滤器,需要在add之前使用bf.reserve指令显式创建,否则会使用默认配置
bf.reserve过滤器名error_rate initial_size
布隆过滤器存在误判的情况,在Redis中有两个值决定布隆过滤器的准确率:

  • error_rate: 允许布隆过滤器的错误率,这个值越低过滤器的位数组的大小越大,占用空间也就越大
  • initial_size: 布隆过滤器可以储存的元素个数,当实际存储的元素个数超过这个值之后,过滤器的准确率会下降

Redis集群当中使用hash Tag让key分配在某个节点

Hash Tag原理是:当一个key包含{}的时候,不对整个key做hash,而仅对{}包括的字符串做hash.hash Tag可以让不同的key拥有相同的hash值,从而分配在同一个槽里;这样针对不同key的批量操作(mget/mset等);以及事务,Lua脚本等都可以支持.

分布式重建缓存的并发冲突问题

现在我们的逻辑是Lua代码里会先去worker内存中取数据,再到redis中取数据,如果redis取不到,就会去源站获取,如果还是取不到,就需要重建缓存了.但是重建缓存有一个问题,因为我们的服务可能是多实例的,虽然在nginx层我们通过流量分发将请求通过id分发到了不同的nginx应用层上.
那么到了接口服务层,可能多次请求访问的是不同的实例,那么可能会导致多个机器去重建读取相同的数据,然后写入缓存中,这就有了分布式重建缓存的并发冲突问题.
问题:可能2个实例获取到的数据快照不一样,但是新数据先写入缓存,如果这个时候另外一个实例的缓存后写入,就有问题了.

解决方案:(使用分布式锁)

  • 变更缓存重建更新redis之前,都需要先对获取对应商品id的分布式锁
  • 拿到分布式锁之后,需要根据时间版本去比较一下,如果自己的版本新于redis中的版本,那么就更新,否则就不更新
  • 如果拿不到分布式锁,那么就等待,不断轮询等待,直到自己获取到分布式锁

分布式锁的基本条件

首先, 为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下几个条件:

  • 1.互斥性.在任意时刻,只有一个客户端能持有锁.
    1. 不会发生死锁.即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁.
    1. 解铃还需系铃人.加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了,即不能误解锁.

缓存维度化拆分解决方案

面临难题:如何解决大value缓存的全量更新效率低下问题?
为什么需要维度化?
一般的业务场景,会对一些页面的数据全量打包为一个kv,比如说商品详情页面,会把商品的基本信息,店家商铺信息,商品分类信息等这些信息组成的json全部塞到一个kv中,然后,当我们对其中的某项数据进行修改的时候(比如说修改了商品的分类),需要把这个kv取出来,修改里面的数据,然后才是放到redis中,很明显这样操作是对redis造成很大的性能影响的,每一次小更新,都需要去操作这个比较大的kv值,对redis造成了一定的压力,就会出现以下问题

  • 网络耗费的资源大,每次需要把真个缓存获取,修改,网络传输耗时为主要消耗
  • redis的性能和吞吐量能够支撑到多大,基本跟数据本身的大小有很大关系
    下图时redis在大value下的表现


    image.png

    前端展示可以分为这么几个维度: 商品维度(标题、图片、属性等)、 主商品维度(商品介绍、规格参数)、 分类维度、 商家维度、 店铺维度等; 另外还有一
    些实时性要求比较高的如实时价格、实时促销、广告词、配送至、预售等是通过异步加载。

Redis缓存和DB的一致性问题

1)产生原因
主要有两种情况,会导致缓存和 DB 的一致性问题:
缓存和 DB 的操作,不在一个事务中,可能只有一个操作成功,而另一个操作失败,导致不一致。
我们讨论二种更新策略:

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

1)先更新数据库,再更新缓存 这套方案,我们不考虑
问题:同时有请求A和请求B进行更新操作,那么会出现
(1)A更新了数据库
(2)B更新了数据库
(3)B更新了缓存
(4)A更新了缓存
这就出现请求A更新缓存应该比请求B更新缓存早才对,但是因为网络等原因,B却比A更早更新了缓存。这就导致了脏数据

  1. 先删缓存,再更新数据库
    我们会基于这个方案去实现缓存更新,但是不代表这个方案在并发情况下没问题
    该方案会导致不一致的原因是。同时有一个请求A进行更新操作,另一个请求B进行查询操作。那么会出现如下情形:
    (1)请求A进行写操作,删除缓存
    (2)请求B查询发现缓存不存在
    (3)请求B去数据库查询得到旧值
    (4)请求B将旧值写入缓存
    (5)请求A将新值写入数据库
    上述情况就会导致不一致的情形出现
  • 解决方案(数据库与缓存更新与读取操作进行异步串行化)
    更新数据的时候,将操作任务,发送到一个队列中。读取数据的时候,如果发现数据不在缓存中,那么将重新读取数据+更新缓存的操作任务,也发送同一个队列中。 每个个队列可以对应多个消费者,每个队列拿到对应的消费者,然后一条一条的执行。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349