文章目录

  • 缓存更新
  • 缓存穿透、缓存雪崩、热点key
  • 一致性哈希算法
  • Redis为什么快
  • redis-server --test-memory内存检测


缓存更新

三种更新策略

  • 先更新数据库,再更新缓存
  • 先删除缓存,再更新数据库
  • 先更新数据库,再更新缓存(最常用)

1、先更新数据库,再更新缓存

  • 如果你是一个写数据库场景比较多,而读数据场景比较少的业务需求,采用这种方案就会导致,数据压根还没读到,缓存就被频繁的更新,浪费性能。
  • 如果你写入数据库的值,并不是直接写入缓存的,而是要经过一系列复杂的计算再写入缓存。那么,每次写入数据库后,都再次计算写入缓存的值,无疑是浪费性能的。显然,删除缓存最为合适。

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

  • 该方案会导致不一致的原因是。同时有一个请求A进行更新操作,另一个请求B进行查询操作。那么就会出现以下情形:
  1. 请求A进行写操作,删除缓存;
  1. 请求B查询发现缓存不存在;
  1. 请求B去数据库查询得到旧值;
  1. 请求B将旧值写入缓存;
  1. 请求A将新值写入数据库。

解决方案:

  • 1.更新完数据库后再删除一次缓存。
  • 2.如果是读写分离架构,采用双删异步策略。

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

  • 假设这会有两个请求,一个请求A做查询操作,一个请求B做更新操作,那么会有如下情形产生:
  1. 缓存刚好失效;
  1. 请求A查询数据库,得一个旧值;
  1. 请求B将新值写入数据库;
  1. 请求B删除缓存;
  1. 请求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个存储节点,请设计一种算法合理地将他们存储在这些节点上。

普通哈希算法的原理:

redis更新list中某一条数据 redis 设置更新删除顺序_缓存


由于该算法使用节点数取余的方法,强依赖node的数量,因此,当node数量发生变化的时候,item对应的node发生剧烈变化,而变化发生的成本就是我们需要在node数发生变化的时候,数据需要迁移。

一致性哈希算法

当增加或者删除节点时,对大多数item,保证原来分配的某个node,现在仍然应该分配到那个node,将数据迁移量降到最低。

原理

redis更新list中某一条数据 redis 设置更新删除顺序_服务器_02


简单来说,一致性哈希将整个哈希值空间组织成一个虚拟的圆环,如假设某哈希函数H的值空间为0-(2^32 - 1)(即哈希值是一个32位无符号整数)整个空间按顺时针方向组织。0和2^32-1在零点方向重合。

redis更新list中某一条数据 redis 设置更新删除顺序_缓存_03


下一步将各个服务器使用Hash进行一个哈希,具体可以选择服务器的ip或主机名作为关键字进行哈希,这样每台机器就能确定其在哈希环上的位置。

redis更新list中某一条数据 redis 设置更新删除顺序_redis_04


接下来使用如下算法定位数据访问到相应服务器:将数据key使用相同的函数Hash计算出哈希值,并确定此数据在环上的位置,从此位置沿环顺时针“行走”,第一台遇到的服务器就是其应该定位到的服务器。例如我们有Object A、Object B、Object C、Object D四个数据对象,经过哈希计算后,在环空间上的位置如下:

redis更新list中某一条数据 redis 设置更新删除顺序_服务器_05


根据一致性哈希算法,数据A会被定位到Node A上,B定位到Node B上,C到Node C,D到Node D上。下面分析一致性哈希算法的容错性和可扩展性。现假设Node C不幸宕机,可以看到此时对象A、B、D不受影响,只有对象C被重定位到Node D。一般的,在一致性哈希算法中,如果一台服务器不可以用,则受影响的仅仅是此服务器到其环空间中前一台服务器(即沿着逆时针方向行走遇到的第一台服务器)之间数据,其他不会受到影响。一致性哈希算法在服务节点太少时,容易因为节点分布不均而造成数据倾斜问题。例如系统中只有两台服务器,其环分布如下:

redis更新list中某一条数据 redis 设置更新删除顺序_服务器_06


这就可能大量数据分布在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更新list中某一条数据 redis 设置更新删除顺序_缓存_07

Redis为什么快

  1. 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度就是O(1);
  2. 数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;
  3. 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
  4. 使用多路I/O复用模型,非阻塞IO;
  5. 使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,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时说明内存检测完毕。