赞
踩
先说说两个很容易搞混的概念,就是集群和分布式。
先用简单的生活例子说明集群和分布式的差异。
问:什么是集群啊?什么是分布式啊?
答:
集群
小饭店原来是一个厨师,切菜洗菜备料炒菜全干。后来客人多了,厨房一个厨师忙不过来,又请了个厨师,两个厨师都能炒一样的菜,这两个厨师的关系就是集群。
分布式
为了让厨师专心炒菜,把菜做到极致,又请了个配菜师负责切菜,备菜,备料,厨师和配菜师的关系就是分布式的,一个配菜师也忙不过来,有请了个配菜师,这两个配菜师的关系就是集群了。所以说有分布式的架构中可能有集群,但集群不等于有分布式。
简单来说,集群就是一堆服务在做同一件事,实际上产品是单体形式,而分布式就是几堆不同的服务在做不同的事,产品是由几种服务组合而成的。
问:那我们平时开发的时候,会遇到不同的应用架构,能简单说一下市面上都有什么应用架构不?
答:软件应用架构,大概有8种,下面逐一说明。
也就是最简单的,程序和数据库都是在一台服务器上。
它的缺点是:任何风吹草动,都会导致服务不可用。
这是第一种的衍生版,就是把数据库放在另一个服务器运行,提高了单机的负载能力,数据读写能力。
3、应用集群
随着用户数量越来越多,需要使用负载均衡进行分流,提高程序的性能。
这里就是集群架构的样子,虽然服务器多了,但是每个服务器里面都是完整的产品,这里需要考虑的是请求由谁来转发,session如何管理的问题。
但是随着用户越来越多,系统的整体性能,都压在数据库的读写那里了,而数据库的读写性能是有上限的,这样就需要主写,从读的数据库模式,从而减轻数据库的读写压力。
数据库读写分离后会遇到的问题是:主从同步会有延迟
随着数据量越来越大,你会发现,用户对数据的各种查询,是一件常见的事情,此时数据库的从库就会压力大增,因为很可能出现模糊查询,全部查询等。
那如何解决?这里需要使用搜索引擎,例如ES引擎。
引入搜索引擎虽然解决了查询问题,但是这里又有个问题,就是查询的数据可能不是实时的,因为搜索引擎与数据库之间,是需要数据同步的。
随着数据越来越多,用户越来越多,业务越来越多,对某个商品的查询,在特殊时期,例如双11,会呈现井喷式的访问量,此时,无论是搜索引擎,从库,都会遇到极大的压力,此时,可以引入缓存机制缓解数据库压力,可以设置多层缓存:
负载均衡的代理层,通常是NGINX,配置静态页面数据缓存。
数据缓存层,通常就是Redis,可以将热点数据,不经常变动的数据,预先缓存在Redis,用户请求的时候,先在缓存层查,查不到再去数据库查。
应用服务层,通常就是实际的服务,可以把热点数据,不经常变动的数据,放到内存。
引入缓存层后,会遇到的问题是:缓存雪崩,缓存击穿,缓存穿透,缓存双写,数据一致性等问题。
就算第6步解决了访问的压力,但是随着数据库实际的数据越来越大,例如产品订单表达到百万记录,交易记录达到千万记录,此时一旦出现缓存雪崩,缓存击穿,缓存穿透的问题,系统就会直接崩溃,因为请求直接到达数据库,任何的简单查询,在这样的数据量里面都会非常慢。
此时,可以对数据库进行垂直拆分和水平拆分。
所谓的垂直拆分,就是把数据库的某些数据量很大表放到不同的数据库。
所谓的水平拆分,就是把一个表拆分到两个甚至多个数据库中,水平拆分举例:例如交易表,拆分成交易月表,交易周表,大客户交易表,普通客户交易表,最终的目的是把一个大表,按不同的维度拆分。
数据水平拆分会遇到的问题是:水平查分后的分页查询问题
随着业务的发展,业务越来越多,单个应用的压力越来越大,工程规模越来越大,改动起来非常麻烦,此时我们可以按照领域模型拆分应用,例如商品子系统,交易子系统,日志子系统等
此时,可以把一些通用的操作抽象出来,屏蔽底层的操作,例如数据查询,可以抽象成数据服务,封装了主从切换,缓存查询,数据库查询等,其他服务统一使用它作为数据访问的唯一途径,这样假如数据相关的改动,就不需要所有服务都跟着升级。
应用拆分会遇到的问题是:分布式事务的各种问题
问:分布式系统这么厉害,那它是不是无敌的啊?有什么地方需要注意的不?
答:分布式系统虽然厉害,它可以解决以前单机,集群解决不了的问题,是大项目大公司必须要使用的,但是它也有很多需要注意的地方,如果没有处理好,反而得不偿失。毕竟产品需要的是稳定,但是分布式要做的是既稳定,又高性能。
我会列出从用户开始使用到最终得到反馈的整个过程进行分析,每一步会遇到的问题,以及解决办法进行讲解。这里暂时不涉及分布式事务,分布式事务需要另开一篇详细说明。
具体的步骤有:
用户入口处理----请求之前的处理---负载均衡代理的处理---应用层处理---缓存层处理---数据层处理---消息层处理
以秒杀业务做分析,所谓的秒杀,是指一堆人在指定的时间段,使用App,浏览器,购买某一个商品,例如淘宝双11和12306的抢票,瞬时流量非常多,因为库存只有一份,读写冲突,就算使用了分布式锁也有压力。
办法: (1)、将请求尽量拦截在系统上游,目的是不要让锁冲突落到数据库上。
(2)、利用缓存。
办法的细节:
产品页面:点击查询,或者购买按钮后,按钮在X秒内置灰,禁止用户重复提交。
JS层面:限制用户在X秒内只能提交一次。
Node层面:使用去重机制,例如IP,Cookie-id,Uid等信息,进行统计,X秒内只能一个请求转到后端,其余请求则返回缓存。
服务层:使用请求队列,就是将并行的变成串行的,这样可以根据实际的商品数控制请求到达数据库的数量
业务规则:例如分时段售卖,分摊流量
问题1,我要调用业务接口,如何让系统知道:我就是我。。。?
答:
这里先说说从客户端到接入层这段,这里涉及路由请求分发,session管理,为何要处理路由请求分发?很简单,例如有个业务需要调用几次后端接口,这样在接入层:通常是NGINX,也可以是F5之类的,如果第一次请求去的是A服务器,session数据在A服务器,然后第二次请求去的是B服务器,这样B服务器就没有上一次调用的session信息,虽然,session可以同步,但是服务器数量多起来,同步就会压力很大,那我们是不是可以尽量让请求都落在同一个服务器?
答:可以的,在接入层,设置ip hash,url hash等配置,即可对请求进行hash计算,让请求落在相对统一的服务器。
这里我说了相对统一,原因是,服务器有可能会增加,有可能会减少,如果减少了服务器,那原先在那个服务器的session将会失效。这是让请求尽量落到一个服务器的办法。
问题2、就是在分库分表的情况下,系统如何知道我这个请求是我的?
答:
这里要使用分布式ID,为何要用分布式ID,而不是数据库自增ID,或者UUID?
数据库自增ID,首先性能问题,其次是分库后自增ID不能保证唯一性,然后自增ID表达不了业务信息。
UUID,首先是无法表达业务信息,其次是ID作为数据库主键后,MYSQL的聚集索引叶子存储的就主键本身,UUID是32字节的,数据量庞大,然后UUID无法排序。
分布式ID,首先ID生成是趋势时间递增,然后是全局唯一。
然后说一下为何要用分布式ID。
例如我需要查询最新的订单,可以这样查询。
select order-id order limit100
由于时间记录本身就能够按时间排序,所以就可以去掉time字段的索引查询,直接就可以查询最新的100条记录。
如何生成趋势时间递增的分布式ID,有以下方法:
1、Redis生成ID
主要是使用hash方式,使用increment累加生成有序ID,但是需要定期删除无用hash列
2、开源算法snowflake
就是雪花算法,结果是一个long型的ID,核心思想是:使用41bit作为毫秒数,10bit作为机器ID(5bit是数据中心,5bit是机器ID),12bit是毫秒内流水号,也就是说没毫秒可以生成4096个ID,一秒26万个ID。snowflake使用dataid和workid作为节点标识,它们的取值范围是1~31,也是说最多可以有961个节点服务器去生成分布式ID
用法:一般是做一个ID分发的服务,把dataid和workid存入redis,并设置24小时过期时间,目的是服务重启还能找回dataid和workid,24小时有效时间是避免抢占这资源不放(例如长期没有被取ID)
常见的基于snowflake实现的框架
百度uid-generator
但是它修改了机器ID部分,并且64位bit的分配支持配置。
美团ecp-uid项目
至此,分布式ID的生成问题解决。
拿到ID后,就开始获取数据了,但是在大规模访问的情况下,就要考虑限流问题,限流在负载均衡层处理。
负载均衡可以使用F5,HaProxy,Nginx,这里以Nginx说明,Nginx有两种限流方式,一是控制速率,二是控制并发数。
ngx_http_limit_req_module模块提供了控制速率的能力,使用漏桶算法。
在 nginx.conf http 中添加限流配置:
格式:limit_req_zone key zone rate
http {
limit_req_zone $binary_remote_addr zone=myRateLimit:10m rate=10r/s;
}
key :定义限流对象,binary_remote_addr 是一种key,表示基于 remote_addr(客户端IP) 来做限流,binary_ 的目的是压缩内存占用量。
zone:定义共享内存区来存储访问信息, myRateLimit:10m 表示一个大小为10M,名字为myRateLimit的内存区域。1M能存储16000 IP地址的访问信息,10M可以存储16W IP地址访问信息。
rate 用于设置最大访问速率,rate=10r/s 表示每秒最多处理10个请求。Nginx 实际上以毫秒为粒度来跟踪请求信息,因此 10r/s 实际上是限制:每100毫秒处理一个请求。这意味着,自上一个请求处理完后,若后续100毫秒内又有请求到达,将拒绝处理该请求。
处理突发流量
server {
location / {
limit_req zone=myRateLimit burst=20 nodelay;
proxy_pass http://my_upstream;
}
}
nodelay 针对的是 burst 参数,burst=20 nodelay 表示这20个请求立马处理,不能延迟,相当于特事特办。不过,即使这20个突发请求立马处理结束,后续来了请求也不会立马处理。burst=20 相当于缓存队列中占了20个坑,即使请求被处理了,这20个位置这只能按 100ms一个来释放。
这就达到了速率稳定,但突然流量也能正常处理的效果。
限制连接数
ngx_http_limit_conn_module 提供了限制连接数的能力,利用 limit_conn_zone 和 limit_conn 两个指令即可。下面是 Nginx 官方例子:
limit_conn_zone $binary_remote_addr zone=perip:10m;
limit_conn_zone $server_name zone=perserver:10m;
server {
limit_conn perip 10;
limit_conn perserver 100;
}
limit_conn perip 10 作用的key 是 $binary_remote_addr,表示限制单个IP同时最多能持有10个连接。
limit_conn perserver 100 作用的key是 $server_name,表示虚拟主机(server) 同时能处理并发连接的总数。
需要注意的是:只有当 request header 被后端server处理后,这个连接才进行计数。
如此,负载均衡层第一步的限流限速完成,负载均衡层的第二步是转发策略。
Nginx基本上有6种策略。
1、轮询策略(默认)
#动态服务器组
upstream dynamic_zuoyu {
server localhost:8080; #tomcat 7.0
server localhost:8081; #tomcat 8.0
server localhost:8082; #tomcat 8.5
server localhost:8083; #tomcat 9.0
}
#其他页面反向代理到tomcat容器
location ~ .*$ {
index index.jsp index.html;
proxy_pass http://dynamic_zuoyu;
}
有如下参数:
fail_timeout | 与max_fails结合使用。 |
max_fails | 设置在fail_timeout参数设置的时间内最大失败次数,如果在这个时间内,所有针对该服务器的请求都失败了,那么认为该服务器会被认为是停机了, |
fail_time | 服务器会被认为停机的时间长度,默认为10s。 |
backup | 标记该服务器为备用服务器。当主服务器停止时,请求会被发送到它这里。 |
down | 标记服务器永久停机了。 |
2、权重策略
#动态服务器组
upstream dynamic_zuoyu {
server localhost:8080 weight=2; #tomcat 7.0
server localhost:8081; #tomcat 8.0
server localhost:8082 backup; #tomcat 8.5
server localhost:8083 max_fails=3 fail_timeout=20s; #tomcat 9.0
}
weight参数用于指定轮询几率,weight的默认值为1,;weight的数值与访问比率成正比,比如Tomcat 7.0被访问的几率为其他服务器的两倍。
3、IP哈希策略
指定负载均衡器按照基于客户端IP的分配方式,这个方法确保了相同的客户端的请求一直发送到相同的服务器,以保证session会话。这样每个访客都固定访问一个后端服务器,可以解决session不能跨服务器的问题。
#动态服务器组 upstream dynamic_zuoyu { ip_hash; #保证每个访客固定访问一个后端服务器 server localhost:8080 weight=2; #tomcat 7.0 server localhost:8081; #tomcat 8.0 server localhost:8082; #tomcat 8.5 server localhost:8083 max_fails=3 fail_timeout=20s; #tomcat 9.0 }
ip_hash不能与backup同时使用。
此策略适合有状态服务,比如session。
当有服务器需要剔除,必须手动down掉。
4、最少连接策略
把请求转发给连接数较少的后端服务器。轮询算法是把请求平均的转发给各个后端,使它们的负载大致相同;但是,有些请求占用的时间很长,会导致其所在的后端负载较高。这种情况下,least_conn这种方式就可以达到更好的负载均衡效果。
#动态服务器组 upstream dynamic_zuoyu { least_conn; #把请求转发给连接数较少的后端服务器 server localhost:8080 weight=2; #tomcat 7.0 server localhost:8081; #tomcat 8.0 server localhost:8082 backup; #tomcat 8.5 server localhost:8083 max_fails=3 fail_timeout=20s; #tomcat 9.0 }
此负载均衡策略适合请求处理时间长短不一造成服务器过载的情况。
5、响应时间策略(fair)
按照服务器端的响应时间来分配请求,响应时间短的优先分配。
#动态服务器组 upstream dynamic_zuoyu { server localhost:8080; #tomcat 7.0 server localhost:8081; #tomcat 8.0 server localhost:8082; #tomcat 8.5 server localhost:8083; #tomcat 9.0 fair; #实现响应时间短的优先分配 }
6、URL哈希策略
按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,要配合缓存命中来使用。同一个资源多次请求,可能会到达不同的服务器上,导致不必要的多次下载,缓存命中率不高,以及一些资源时间的浪费。
而使用url_hash,可以使得同一个url(也就是同一个资源请求)会到达同一台服务器,一旦缓存住了资源,再此收到请求,就可以从缓存中读取。
#动态服务器组
upstream dynamic_zuoyu {
hash $request_uri; #实现每个url定向到同一个后端服务器
server localhost:8080; #tomcat 7.0
server localhost:8081; #tomcat 8.0
server localhost:8082; #tomcat 8.5
server localhost:8083; #tomcat 9.0
}
经过代理层的处理后,请求数据已经得到很好的负载处理,但是业务的实际处理还是需要应用层处理。
应用层整体的结构就是Http网关+RPC服务群。
应用层需要注意的地方有几个。
1、保证逻辑的隔离性,例如某个资源的读写,逻辑上只能一次一个人操作。
2、保证全链路可追踪。
3、保证数据可回滚。
第一点,使用分布式锁实现,而分布式锁有几种,Memcached,Redis,Zookeeper,Chubby方式。
其中Redis比较简单和常用。
setnx(idm,1)
如果返回1,说明key不存在,该线程得到锁,当返回0,说明key已经存在,抢锁失败。
del(id)
解锁
通常使用set(id,1,30,NX)防止死锁
这里有个极端场景,就是A获得锁,30秒过期,然后A的处理时间超过30秒,这时候锁过期,自动释放,B获得锁,然后A执行完成,接着释放锁,但是此时释放的是B的锁。
此时有两种处理方式。
1、在并发量不是很大的情况下,加锁时候,set(key,threadId,30,NX),就是使用线程ID做为值去使用锁,然后redis.get(key)获取线程ID,与当前线程ID做比较,如果一样,说明是自己的,否则,不能删除。
2、如果是在高并发场景下,可以开一个守护线程守护,当时间到了29秒,就执行expire,延长锁的时间。
第二点,使用微服务框架实现,例如go-zero框架,使用opentrace,elk,kafka实现全链路追踪并且UI界面显示。
第三点,使用分布式事务,例如seata框架,这个开新篇详细讨论。
高并发情况下,进行了前面的分流,分布式锁的处理后,接入层的压力已经不存在,此时压力都会在数据层,而数据库的IO是有限的,所以,这里要插入一层缓存层。
首先要明确,缓存解决的是什么问题,缓存的方案有哪些。
1、缓存解决什么问题。
在高并发情况下,请求直接到达数据库,会很快到达数据库的IO瓶颈,所以,使用缓存解决的问题是大量的数据读写情况下的高并发访问。
2、缓存的方案有哪些。
分布式缓存常见有Memcached和Redis。
Memcached适合做简单的K-V存储,内存使用率较高,由于是多核处理,对于较大的数据,性能较好。
缺点比较明显,Memcached没有集群机制,无法持久化。
Redis数据结构丰富,单线程处理所有请求,对于较大的数据,性能较差,Redis提供持久化功能,包括RDB全量持久化,AOF增量持久化,主从同步,故障切换。
缺点是全量持久化需要额外较大的内存,在高并发情况下,造成swap,会影响性能,增量持久化也涉及到写磁盘和fsync,也会拖慢处理速度。
3、缓存使用的场景
场景1、和数据库中的数据保持一致,原样缓存
这是最常用的场景,基本就是数据库长什么样,缓存就长什么样,数据库里面是全量商品信息,缓存里面是最热的商品信息。
场景2、列表排序分页场景
例如我们想获取点赞最多的评论,或最新的评论,然后列出来,一页一页的翻下去。使用Redis的列表方式存储ID的方式,在Redis中排序好,此时取列表分页就会很快,
可以使用一个异步线程初始化和刷新缓存,缓存里面保存一个时间戳,当有更新的时候,刷新时间戳,异步任务发现时间戳改变,就刷新缓存。
场景3、计数缓存
计数对于数据库来说,是非常繁重的工作,例如需要查询大量的行,最后得出计数的结论,当数据改变时候,要重新刷一遍,非常影响性能。
因此可以有一个计数服务,后端是一个缓存,将计数作为结果放到缓存,当数据有改变的时候,调用计数服务增加或减少计数,而非通过异步数据库的count来更新缓存,
计数服务可以使用Redis进行单个计数,或者hash表进行批量计算。
场景4、重构维度缓存
Memcached和Redis的缓存都是为了读多写少的情况下的,如果想读多写多的情况下保持高并发,有时候数据库里面保持的数据的维度是为了写入方便,而非为了查询方便的,
然而同时查询过程,也需要处理高并发,因而需要为了查询方便,将数据重新以另一个维度存储一遍,或者说将多个数据库的内容聚合一下,再存储一遍,从而不用每次查询的时候都重新聚合,
如果还是放在数据库,比较难维护,放在缓存就好一些。
例如一个商品的所有的帖子和帖子的用户,以及一个用户发表过的所有的帖子就是属于两个维度。
这需要写入一个维度的时候,同时异步通知,更新缓存中的另一个维度。
在这种场景下,数据量相对比较大,因而单纯用内存缓存memcached或者redis难以支撑,往往会选择使用levelDB进行存储,如果levelDB的性能跟不上,可以考虑在levelDB之前,再来一层memcached。
场景5、较大的详情数据缓存
例如评论的详情,或者帖子的详细内容,属于非结构化的,内容比较大,因而使用memcached比较好。
缓存的三大矛盾
1、缓存实时性与一致性问题:当有写入后咋办?
使用了缓存,数据就保存了多份,数据库一份,缓存中一份,当数据库因写入而产生新的数据,往往缓存不会和数据库放在一个事务里,如何将数据更新到缓存,什么时候更新到缓存,不同策略不一样。
2、缓存穿透问题:当没读到咋办?
为何会出现缓存读取不到的情况?
第一:可能读取的是冷数据,需要到数据库查询一下,然后放入缓存,再返回给客户。
第二:可能因为有数据写入,被实时从缓存中删除了,此时导致读取不到缓存,需要查询数据库然后再次放入缓存,再返回给客户。
第三:缓存的时效过了,这时候就会访问不到缓存,需要访问数据库再次放入缓存,再返回给客户。
第四:由于缓存长时间不访问,使用了LRU策略,缓存将会被删除,此时访问缓存将失败,需要访问数据库再次放入缓存,再返回给客户。
第五:缓存没有数据,数据库也没有数据,但是每次这种空的情况,都会面临一次数据库的访问,此时最好的办法是对空的结果也做一次缓存,降低数据库的实际访问次数。
3、缓存对数据库高并发访问:都来访问数据库咋办?
我们本来使用缓存,是来拦截直接访问数据库请求的,从而保证数据库大本营永远处于健康的状态。但是如果一遇到不命中,就访问数据库的话,平时没有什么问题,但是大促情况下,数据库是受不了的。
一种情况是多个客户端,并发状态下,都不命中了,于是并发的都来访问数据库,其实只需要访问一次就好,这种情况可以通过加锁,只有一个到后端来实现。
另外就是即便采取了上述的策略,依然并发量非常大,后端的数据库依然受不了,则需要通过降低实时性,将缓存拦在数据库前面,暂且撑住,来解决。
解决缓存三大矛盾的策略
1、实时策略
是最常用,保持实时性最好的策略。
读取的过程,先从cache读取数据,没有得到,则从数据库读取,成功后,放到缓存,如果命中,则从cache读取数据,取到后返回。
写如的过程,先把数据存到数据库,成功后,再让缓存失败,失效后下次读取的时候,会被先写入缓存。为何不立刻直接写缓存?那是因为并发写入,无法控制写缓存的顺序,有可能出现缓存中数据与数据库数据不一致的情况,
2、异步策略
读取不到的时候,不直接访问数据库,而是先返回一个fallback数据,然后往消息队列添加一个数据加载事件,然后背后有一个任务,收到事件后,会异步的读取数据库,由于有队列作用,可以实现消峰,缓冲对数据库的访问,甚至可以将队列中多个任务
合并请求,合并更新缓存,提高效率。
当更新的时候,异步策略总是先更新数据库或缓存其中一个,然后异步更新另一个。
一种方式是先更新数据库,然后异步更新缓存
,当数据库更新后,生成一个异步消息,放入消息队列,等待背后的任务通过消息进行缓存更新,缺点是实时性较差,要一段时间后才能看到更新,好处是数据库的持久化得到保证。
一种方式是先更新缓存,然后异步更新数据库,这种方式是读取和写入都用缓存,将缓存完全挡在数据库前面,把缓存当成数据库在使用,所以一般会使用持久化机制和主备redis,但是仍然无法保证缓存不丢,所以适合在并发量大,但数据没有那么关键的情况,
好处是实时性较好。
消息队列方案也有两套方式。
方案一:
流程如下所示
(1)更新数据库数据;
(2)缓存因为种种问题删除失败
(3)将需要删除的key发送至消息队列
(4)自己消费消息,获得需要删除的key
(5)继续重试删除操作,直到成功
然而,该方案有一个缺点,对业务线代码造成大量的侵入。于是有了方案二,在方案二中,启动一个订阅程序去订阅数据库的binlog,获得需要操作的数据。在应用程序中,另起一段程序,获得这个订阅程序传来的信息,进行删除缓存操作。
方案二:
流程如下图所示:
(1)更新数据库数据
(2)数据库会将操作信息写入binlog日志当中
(3)订阅程序提取出所需要的数据以及key
(4)另起一段非业务代码,获得该信息
(5)尝试删除缓存操作,发现删除失败
(6)将这些信息发送至消息队列
(7)重新从消息队列中获得该数据,重试操作。
备注说明:上述的订阅binlog程序在mysql中有现成的中间件叫canal,可以完成订阅binlog日志的功能。至于oracle中,博主目前不知道有没有现成中间件可以使用。另外,重试机制,博主是采用的是消息队列的方式。
如果对一致性要求不是很高,直接在程序中另起一个线程,每隔一段时间去重试即可,这些大家可以灵活自由发挥,只是提供一个思路。
3、定时策略
如果并发量实在太大,数据量也大的情况,异步都难以满足,可以降级为定时刷新的策略,这种情况下,应用只访问缓存,不访问数据库,更新频率也不高,而且用户要求也不高,例如详情,评论等。
这种情况下,由于数据量比较大,建议将一整块数据拆分成几部分进行缓存,而且区分更新频繁的和不频繁的,这样不用每次更新的时候,所有的都更新,只更新一部分。并且缓存的时候,可以进行数据的预整合,因为实时性不高,读取预整合的数据更快。
一、缓存穿透,意思是缓存和数据库中都没有数据,而用户不断发起请求,例如发起id为-1的数据或id为特别大,不存在的数据,此时的用户很可能是攻击者。
解决方案:
1、应用层加校验,例如用户鉴权校验,id做基础校验,id<=0的直接拦截。
2、从缓存取不到数据,在数据库也没取到,此时可以将k-v对写为k-null,缓存有效时间可以设置短一点,例如30秒,这样可以防止攻击者反复使用同一个id进行暴力攻击。
二、缓存击穿,意思是缓存中没数据,但是数据库中有数据(一般是缓存时间到期),此时由于并发特别多,同时去数据库取数据,引起数据库压力大增,造成崩溃。
解决方案:
1、设置热点数据永不过期
2、加互斥锁(分布式锁)
三、缓存雪崩,意思是缓存中大批量到过期时间,而查询数据量巨大,引起数据压力过大甚至崩溃,和缓存击穿不同,缓存击穿是并发查询同一条数据,而缓存雪崩是不同数据都过期,很多数据都查询不到而查数据库
解决方案:
1、缓存数据的过期时间随机设置,防止同一时间大量过期
2、缓存分布在不同的缓存数据库
3、设置热点数据永不过期
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。