redis缓存问题

本文关注redis缓存的几个主要问题以及解决方法。关键词包括:缓存穿透、缓存雪崩、缓存击穿、布隆算法、hash一致性算法、分布式锁。

1.数据库瓶颈

公司里基本上都是BS架构。tomcat(java)服务器会挂着mysql服务器。用户发请求给web服务器tomcat,tomcat会向后台数据库mysql中查数。随着客户端连接增多,tomcat服务器和数据库都可能发生瓶颈(数据库相比服务器更容易也更先发生瓶颈:如相应时间变长)。

 

数据库瓶颈解决方法:(1)减少连接:数据库连接池(提前创建数据库连接,需要连接直接从池里拿,用完又丢到池里,防止频繁创建以及销毁连接)。(2)分库分表;(3)分布式集群;(4)加缓存层(使用redis)。

 

加redis缓存层:在服务器和mysql中间加redis,redis存放热门数据(经常被访问的数据)。web服务器先去redis中查,redis是基于内存的,响应速度很快,并且支持海量查询。redis中没有再去mysql查询。

2.缓存穿透

问题:服务器在redis中没找到,再去mysql中找,这个现象叫缓存穿透(只要加了缓存层,就一定会发生)。不能避免缓存穿透(除非将mysql数据全部同步到redis中,就算同步也不能保证实时),只能避免高频的缓存穿透

例子:

黑客攻击数据库,比如查id=-1,redis中肯定没有,然后缓存穿透到mysql。连续的发起id=-1的请求,就出现高频缓存穿透问题,造成mysql宕机。

解决办法:把查询的数据往redis再缓存一份。比如第一次查穿透了,查id=-1在mysql中查出来是null值,把这条数据缓存到redis中,下次查就从redis中找了。

但如果黑客发送id=uuid(每次请求都不一样的),上面的方法就适得其反(redis中存了一堆null值。redis自带的淘汰策略LRU、LFU就会把有价值的数据淘汰掉)。

解决方法:采用布隆过滤器。在redis和mysql中间加上布隆过滤器。过滤器保存mysql中经常被查询字段的值(id是uuid,经过过滤器(存了id值)发现没有这个值,就直接阻挡了)。

但是也可能通过多个字段(不仅是id)攻击,那么过滤器也要保存这么多,而过滤器在内存中,就会导致内存紧张

解决过滤器内存紧张问题:布隆算法。

3.布隆算法

布隆算法:通过一定的错误率来换取空间

布隆算法原理:通过一个bit数组来标识数据。bit数组初始化为0.将数据(id=10)传给hash函数(要求:(1)hash值的范围必须在数组的长度范围之内(2)hash值要足够的散列),得到一个hash值,这个值是数组的下标,将数组对应的数改为1(bit数组这个位置为1就代表这个数据).

但是查值时,布隆过滤器由于hash冲突会产生错误。因为bit数组的长度是有限的,而数据是无限的。

降低hash碰撞的概率(布隆过滤器的错误率):(1)增加hash函数个数(要多个位置都一样才存在,都一样的概率低了);(2)加大数组长度。

hash函数的个数并不是越多越好,需要参考数组的长度和数据的个数。(ln2*数组的长度/数据的个数)

hash错误率体现在布隆算法说数据存在,那么实际可能不存在(hash冲突),布隆算法说数据不存在,那么一定不存在。

因此布隆过滤器解决黑客攻击发送不同字段导致内存紧张问题。

布隆算法弊端:删除数据。bit数组中某个位置的1可能代表好几个数据。解决办法:计数器:如果该位置的计数器的值为0,就可以放心的把这个位置的1变为0.

3.缓存雪崩

概念:redis中缓存大量热门数据,突然在某个时刻集中失效,导致大量的请求打向mysql。

原因:
(1)redis中缓存的数据的有效期是一致的导致。解决:给每条数据随机有效期。(不要突然同时失效)

(2)redis数据库挂掉了。解决:分布式缓存。把热门数据存在多台redis中。集群采取切片集群模式(把数据切成一小堆一小堆的,放在不同的redis中),其中一台挂了,只丢失一部分热门数据,其他还活着。如果热门数据量不大的话,还可以采取副本集群模式(每个redis都存相同的一份),一个挂了,其他都还有一样的数据。

4.hash一致性算法

redis集群的hash一致性算法:hash一致性算法主要应用在redis集群中。

问题1:将数据data平均分到两台redis中存储,采取什么策略?hash取模。计算每条数据key的hash值,然后模上2,0存在第一台,1存在第二台。

问题2:如果现在又新增一台redis,是模上3了,那么原来存在前两台redis的一部分数据,应该存在redis3了,就又要拿出来(新增一台,会将原来的数据拿出来重新分发一次,不利于集群的扩展),增加扩容成本,解决办法:hash一致性算法:解决集群扩展问题的。

hash一致性算法原理:有个hash环,环上分布很多,每个点有个编号。把两台数据库映射到环上的点。映射方法:拿到每台redis的ip或者host,计算个hash,然后模上点的个数,这样就将redis和环上的点映射了。同样将每条数据也根据同样的映射方法(对数据的key取模)映射到环的点上。那么那个数据存在哪个redis中?有个存储策略:让每个数据在环上按顺时针找,先找到哪个redis,就存在哪个redis中。

hash一致性算法怎么解决集群扩展问题?现新增一台redis,也映射到环上,但此时只需要新增redis和新增redis右边的一台redis发生数据传输,其他都没关系。(不管多少个集群,也都只有两台有数据迁移。大量集群,hash一致性算法优势更加突出)

hash一致性算法弊端:容易发生数据倾斜(大量数据存在少量结点上)。比如redis1和redis2在环上离得很近,就会产生。解决:加虚拟结点。(每个结点都多虚拟几个,在环上变得均匀)。

通过hash槽,实现hash一致性。hash槽有16384个

5.缓冲击穿

概念:redis中只存储一条数据,某一时刻这条数据失效。

 缓冲击穿和缓冲雪崩实质都是缓存穿透,是缓冲穿透的一种特殊表现形式。

 一般中小型公司不需要解决缓存击穿(小公司没有一条热门数据击穿会使mysql扛不住)。大公司使用分布式锁解决。

 解决方法:(1)使用分布式锁(保证只有一个线程打向mysql,但有效率问题:其他服务器阻塞);(2)将查询的数据,再次缓存到redis中。(不严谨,但简单,解决了效率问题)

6.分布式锁

分布式锁三个条件:有共享资源、共享资源互斥、多任务场景(有这个三个条件就要上锁)

 分布式锁名字由来:不同应用程序分布在不同结点服务器上(多台服务器叫分布式),锁控制多台服务器上多个进程的排队问题,叫分布式锁。

 基本流程:多线程可以使用锁机制排队实现。但如果多任务是不同应用程序(独立个体),就要用到外部的锁(一个值或者数据),比如去三个客户端到数据库抢着插入数据,谁先插入数据谁就先获取公共资源、获取完后,就将数据库中的值删除释放锁,其他再去抢。

 mysql作为锁会存在问题:死锁:当有锁的一个结点宕机了,就能删除数据库的值释放锁,其他结点也抢不到锁。

 基于redis实现分布式锁:redis天生具备有效期特性,但存在有效期度的问题(太长则等待时间太长,太短则存在线程安全问题(如一票多卖))。而redisession可以延长有效期,在一定长度上可以解决有效期度的问题。

 因此,一般会选择zookeeper作为分布式锁(最常用的)。

zookeeper

概念:zookeeper是一个分布式一致性服务框架。分布式表明zookeeper可以部署到多个结点上。客户端往zookeeper一个结点插入一条数据,它会将数据同步到其他结点上。这种一致性是最终一致性(弱一致性)。最终一致性:现在客户端1对结点1插入数据,结点1把数据同步到其他数据,该同步过程中,客户端2读结点5的数据,此时结点5还没有同步完,那么客户端2发送一个命令,让结点5先把数据同步完,再返回数据。强一致性就是必须把所有结点都同步完,其他客户端才能去读。

 特点:

 (1)可以存储数据,类似文件系统(有目录、层级)去存。

 (2)可以创建4种类型的目录:持久目录、临时目录、持久有序目录和临时有序目录。

 持久目录:当客户端连接结点1,创建一个持久目录,当客户端断开连接,这个目录会依然存在。create /msb/qqq 000

 临时目录:当客户端连接结点1,创建一个持久目录(创建时要给初始值),当客户端断开连接,这个目录会删除(其他结点都会删除)。 create -e  /msb/qqq 000。

 临时目录删除需要一段时间,因为zookeeper中客户端和结点的长连接是通过心跳机制维持的,就是客户端会在每个一段时间会发送一个消息给结点告诉自己还活着,心跳周期内结点不知道客户端是否存活。

 持久有序:生成的目录有序号,并且是有序的。不同结点针对这一个目录创建,是递增序号的。因此zookeeper内部会维护一个全局有序唯一序号。create -s  /msb/qqq 000

 临时有序:create -e -s  /msb/qqq 000

(3)具备事件回调机制

 回调机制:客户端给结点上的目录注册事件(比如目录下的目录被删除),事件发生就会触发客户端上的回调函数。

 基于zookeeper集群实现分布式锁:

 3个客户端client1、client2、client3分别去zookeeper不同结点上去抢锁(创建临时有序的lock目录),比如client1先抢到,就创建lock01,client2后创建就叫lock02(创建的序号不可能相同:因为全局有序的唯一序号)、最后client3就创建lock03,这些lock1~3会同步到各个结点上。此外,还需注册事件。客户端2在创建结点时发现还有比它更小的序号,就再前面那个序号的目录(lock01)上注册事件,client3也在lock02上注册事件。然后开始释放:lock01最小,最先执行业务,执行完后将lock01删掉,此时注册在lock01上的事件就会回调lock02(client2)的回调函数,就是去执行业务。同样执行完后lock02也会删除,回调lock03的回调函数。

 在zookeeper目录是设置成临时有序的:当正在执行的客户端挂掉后,由于是临时的,所有当过了心跳周期后,这个结点对应的lock目录会被删除,同样会触发后面的回调函数。因此当结点宕机后不会影响其他结点运行,zookeeper解决了死锁问题。

 基于zookeepe的分布式锁没有使用有效期、超时时间等,就解决了mysql和redis的问题(有效时间 度的问题)。基于回调机制的。

 zookeeper节省了大量的机器资源。

7.zookeeper分布式锁解决缓存击穿:

 如果到达必须要解决缓存击穿的程度,说明用户数已经很高了,此时web服务器(tomcat)肯定有多台,还在多个tomcat前面挂个ngix。当redis中没有数据,会到分布式集群中抢锁(互斥锁),抢到锁的web服务器就会去mysql中查询,再将查询到的数据缓存到redis中,剩下的web服务器在抢锁之前要先去redis中看数据是否存在,如果存在就不用抢锁了。(如果不使用分布式锁,这些请求都会打到mysql,使用了只需要1个去请求,其他从redis中拿)。

原文地址:https://www.cnblogs.com/yq055783/p/13043680.html