文章目录
- 缓存更新
- 缓存穿透、缓存雪崩、热点key
- 一致性哈希算法
- Redis为什么快
- redis-server --test-memory内存检测
缓存更新
三种更新策略
- 先更新数据库,再更新缓存
- 先删除缓存,再更新数据库
- 先更新数据库,再更新缓存(最常用)
1、先更新数据库,再更新缓存
- 如果你是一个写数据库场景比较多,而读数据场景比较少的业务需求,采用这种方案就会导致,数据压根还没读到,缓存就被频繁的更新,浪费性能。
- 如果你写入数据库的值,并不是直接写入缓存的,而是要经过一系列复杂的计算再写入缓存。那么,每次写入数据库后,都再次计算写入缓存的值,无疑是浪费性能的。显然,删除缓存最为合适。
2、先删缓存,再更新数据库
- 该方案会导致不一致的原因是。同时有一个请求A进行更新操作,另一个请求B进行查询操作。那么就会出现以下情形:
- 请求A进行写操作,删除缓存;
- 请求B查询发现缓存不存在;
- 请求B去数据库查询得到旧值;
- 请求B将旧值写入缓存;
- 请求A将新值写入数据库。
解决方案:
- 1.更新完数据库后再删除一次缓存。
- 2.如果是读写分离架构,采用双删异步策略。
3、先更新数据库,再删缓存
- 假设这会有两个请求,一个请求A做查询操作,一个请求B做更新操作,那么会有如下情形产生:
- 缓存刚好失效;
- 请求A查询数据库,得一个旧值;
- 请求B将新值写入数据库;
- 请求B删除缓存;
- 请求A将查到的旧值写入缓存。
还有其他造成不一致的原因吗?
有的,这也是缓存更新策略2和缓存更新策略3都存在的一个问题,如果删缓存失败了怎么办,那不是会有不一致的情况出现吗。比如一个写数据请求,然后写入数据库了,删缓存失败,这就会出现不一致的情况。
解决方案:
- 1.更新数据库数据;
- 2.缓存因为种种原因删除失败;
- 3.将需要删除的key发送至消息队列;
- 4.自己消费消息,获得需要删除的key;
- 5.继续重试删除操作,直到成功。
该方案有一个缺点,对业务线代码造成大量的侵入。
改进方案:
1.更新数据库数据;
2.数据库会将操作信息写入binlog日志当中;
3.订阅程序提取出所需要的数据以及key;
4.另起一段非业务代码,获得该信息;
5.尝试删除缓存操作,发现删除失败;
6.将这些信息发送至消息队列;
7.重新从消息队列中获得该数据,重试操作。
缓存穿透、缓存雪崩、热点key
缓存击穿
缓存系统,按照key去查询value,当key对应的value一定不存在的时候并对key并发请求量很大的时候,就会对后端造成很大的压力。
由于缓存不命中,每次都要查询持久层。从而失去缓存的意义。
解决方案
- 1、缓存层缓存空值
缓存太多空值,占用更多空间(优化:给个空值过期时间)
存储层更新代码了,缓存层还是空值(优化:后台设置时主动删除空值,并更新为新值)- 2、将数据库中所有的查询条件,放到布隆过滤器中。当一个查询请求来临的时候,先经过布隆过滤器进行检查,如果请求存在这个条件中,那么继续执行,如果不在,直接丢弃。
备注
比如数据库中有10000个条件,那么布隆过滤器的容量size设置的要稍微比10000大一些,比如12000。对于预判率的设置,根据实际项目,以及硬件设施来具体决定。但是一定不能设置为0,并且误判率设置的越小,哈希函数跟数据长度都会更多跟更长,那么对硬件,内存等的要求就会相应的高。
private static BloomFilter bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size, 0.0001);
有了size跟误判率,那么布隆过滤器就会产生相应的哈希函数跟数组。
综上:我们可以利用布隆过滤器,将redis缓存击穿控制在一个可容忍的范围内。
缓存雪崩(缓存失效)
如果缓存集中在一段时间内失效,发生大量的缓存穿透,所有的查询都落在数据库上,就会造成缓存雪崩。
解决方案
- 在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
- 可以通过缓存reload机制,预先去更新缓存,再即将发生大并发访问前手动触发加载机制。
- 不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
- 做二级缓存,或者双缓存策略。A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期。
热点key
一个key的访问量非常大即为热点key。
因为缓存的构建是需要一定时间的。于是就出现一个致命问题:在缓存失效的瞬间,有大量线程来构建缓存,造成后端负载加大,甚至可能会让系统崩溃。
解决方案
- a. 使用互斥锁:这种解决方案思路比较简单,就是只让一个线程构建缓存,其他线程等待构建缓存的线程执行完,重新从缓存存取数据就可以了。
- b. “提前”使用互斥锁:在value内部设置1个超时值(timeout1),timeout1比实际的memcache timeout(timeout2)小。当从cache读取到timeout1发现它已经过期时候,马上延长timeout1并重新设置到cache。然后再从数据库加载数据并设置到cache中。
- c. “永远不过期”
这里的永远不过期包含两层意思:
(1)从redis上看,确实没有设置过期时间,这就保证了不会出现热点key过期问题,也就是“物理”上不过期。
(2)从功能上看,如果不过期,那不就成了静态的了么?所以我们将过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期客户端热点key缓存:将热点key对应value并缓存在客户端本地,并且设置一个失效时间。对于每次读请求,将首先检查key是否存在于本地缓存中,如果存在则直接返回,如果不存在再去访问分布式缓存的机器。- d. 将热点key分散为多个子key,然后存储到缓存集群的不同机器上,这些子key对应的value都和热点key是一样的。当通过热点key去查询数据时,通过某种hash算法随机选择一个子key,然后再去访问缓存机器,将热点分散到了多个子key上。
一致性哈希算法
假设有1000w个数据项,100个存储节点,请设计一种算法合理地将他们存储在这些节点上。
普通哈希算法的原理:
由于该算法使用节点数取余的方法,强依赖node的数量,因此,当node数量发生变化的时候,item对应的node发生剧烈变化,而变化发生的成本就是我们需要在node数发生变化的时候,数据需要迁移。
一致性哈希算法
当增加或者删除节点时,对大多数item,保证原来分配的某个node,现在仍然应该分配到那个node,将数据迁移量降到最低。
原理
简单来说,一致性哈希将整个哈希值空间组织成一个虚拟的圆环,如假设某哈希函数H的值空间为0-(2^32 - 1)(即哈希值是一个32位无符号整数)整个空间按顺时针方向组织。0和2^32-1在零点方向重合。
下一步将各个服务器使用Hash进行一个哈希,具体可以选择服务器的ip或主机名作为关键字进行哈希,这样每台机器就能确定其在哈希环上的位置。
接下来使用如下算法定位数据访问到相应服务器:将数据key使用相同的函数Hash计算出哈希值,并确定此数据在环上的位置,从此位置沿环顺时针“行走”,第一台遇到的服务器就是其应该定位到的服务器。例如我们有Object A、Object B、Object C、Object D四个数据对象,经过哈希计算后,在环空间上的位置如下:
根据一致性哈希算法,数据A会被定位到Node A上,B定位到Node B上,C到Node C,D到Node D上。下面分析一致性哈希算法的容错性和可扩展性。现假设Node C不幸宕机,可以看到此时对象A、B、D不受影响,只有对象C被重定位到Node D。一般的,在一致性哈希算法中,如果一台服务器不可以用,则受影响的仅仅是此服务器到其环空间中前一台服务器(即沿着逆时针方向行走遇到的第一台服务器)之间数据,其他不会受到影响。一致性哈希算法在服务节点太少时,容易因为节点分布不均而造成数据倾斜问题。例如系统中只有两台服务器,其环分布如下:
这就可能大量数据分布在Node A,少数数据分布在Node B,为了解决这种数据倾斜问题,一致性哈希算法引入了虚拟节点机制,即对每一个服务节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点。具体做法可以在服务器ip或主机名后面增加编号。例如上面的情况,可以为每台服务器计算三个虚拟节点,于是可以分别计算“Node A#1”、“Node A#2”、“Node A#3”、“Node B#1”、“Node B#2”、“Node B#3”的哈希值,于是形成六个虚拟节点。同时数据定位算法不变,只是多了一步虚拟节点到实际节点的映射,例如定位到“Node A#1”、“Node A#2”、“Node A#3”三个虚拟节点的数据均定位到Node A上。这样就解决了服务节点少时数据倾斜的问题。在实际应用中,通常将虚拟节点数设置为32甚至更大,因此即使很少的服务节点也能做到相对均匀的数据分布。
Redis为什么快
- 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度就是O(1);
- 数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;
- 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
- 使用多路I/O复用模型,非阻塞IO;
- 使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM机制,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
redis-server --test-memory内存检测
redis-server
除了启动Redis外,还有一个--test-memory
选项。redis-server --test-memory
可以用来检测当前操作系统能够稳定地分配内存给Redis,通过这种检测可以有效避免因为内存问题造成Redis崩溃,例如下面操作检测当前操作系统能否提供1G的内存给Redis:
redis-server --test-memory 1024
整个内存检测的时间比较长,当输出passed this test
时说明内存检测完毕。