高并发场景设计(一)缓存设计

什么是缓存

缓存的定义:用于存储数据的硬件或软件的组成部分,以使得后续更快访问相应的数据。典型的应用场景:有cpu cache, 磁盘cache等。

缓存的使用

随着客户端请求量的增加,以及服务对响应时间要求的提升,单纯的依赖数据库DB已不能满足需求,尤其是对于多读写少的场景,可以将一部分数据存入缓存,利用内存读写的速度远远高于磁盘或者DB的读写速度,提升系统整体的吞吐量。

缓存的设计常遇到的问题

  • 缓存穿透大量不存在的key查询,越过缓存查询数据库,比如一些恶意攻击、爬虫等造成大量空命中。注意这里数据库也查询不到。

解决方案:1) 采用布隆过滤器或者bitmap,在访问和缓存之间加一层屏障,里面存储数据库目前存储的所有key。如果请求的key不再过滤器中,直接返回,不再查询数据库。2)缓存空的value。

  • 缓存击穿高并发情况下,某一key失效,导致大量的查询数据库的操作。

解决方案:double-check + 单节点同步锁,引入: 模块方法 + 回调设计模式

  • 缓存雪崩:大量的key同时失效

解决方案:区分冷热数据,设置不同的失效时间;同时再加锁,采用阻塞队列保证单线程访问数据库。

缓存回收策略

  1.  基于空间,即设置缓存的存储空间,如设置为10MB,当达到存储空间时,按照一定的策略移除数据。
  2. 基于容量,缓存设置了最大值,当缓存的条目超过最大值,则按照一定的策略将旧数据移除。
  3. 基于时间,TTL(Time To Live):存活期,即缓存数据从缓存中创建时间开始直到它到期的一个时间段(不管在这个时间段内有没有访问都将过期)。TTI(Time To Idle):空闲期,即缓存数据多久没被访问过将从缓存中移除的时间。
  4. 基于Java对象引用,软引用和弱引用

缓存类型

堆内缓存,即借助JVM自身的缓存,如goova提供的cache。

堆外缓存,比如Memory Cache, redis等。

堆外缓存又分为:单机缓存、分布式缓存和多级缓存等。

缓存一致性问题

数据库与缓存双写不一致,即需要更新数据库和缓存里的数据,

1、最初级的缓存不一致问题以及解决方案

问题:先修改数据库,再删除缓存,如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据出现不一致

解决思路:先删除缓存,再修改数据库。如果删除缓存成功了,但修改数据库失败了,那么数据库中是旧数据,缓存中是空的,数据不会不一致。

或者利用延时双删方案:执行数据库更新操作之前,删除缓存key,执行数据库更新,利用线程sleep,然后再次删除redis中的key。

2、比较复杂的数据不一致问题分析

问题:数据发生了变更,先删除了缓存,然后要去修改数据库,此时还没修改,有请求过来读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中,数据变更的程序完成了数据库的修改。

解决思路:更新数据的时候,根据数据的唯一标识,将操作路由发送到一个jvm内部的队列中;读取数据的时候,如果发现数据不在缓存中,那么将重新读取数据+更新缓存的操作,根据唯一标识路由之后,也发送同一个jvm内部的队列中。这样的话,一个数据变更的操作,先执行,删除缓存,然后再去更新数据库,但是还没完成更新,此时如果一个读请求过来,读到了空的缓存,那么可以先将缓存更新的请求发送到队列中,此时会在队列中积压,然后同步等待缓存更新完成。

或者利用Binlog异步更新缓存:监听binlog日志,异步刷新缓存数据。

3、更复杂的问题

问题:由于读请求进行了非常轻度的异步化,当数据更新很频繁,导致队列中积压了大量更新操作在里面,然后读请求会发生大量的超时,最后导致大量的请求直接走数据库。

解决思路:首先不能让所有的请求都放到一个JVM的队列中;其次,需要仔细估算TPS和QPS。

你可能感兴趣的:(高并发场景)