今天我们就来说说在大规模分布式系统中应该如何使用缓存。毕业已经三年多了,我们经历了几十个大大小小的系统。今天我们就从各个角度来探讨一下。让我们看看我们不同的缓存应该如何使用,以便能够有效地使用它们。
我们团队目前正在研发直播产品。以抖音为例。比如我们要开发一个顶哥、二哥等排行榜,怎么开发呢?对于抖音对于这个亿级产品来说,缓存设计一定是家常便饭。
对于百万级、千万级的接口调用,如果没有缓存设计,直接命中数据持久层,那将是一场毁灭性的灾难。我每天都会经历数百万次的接口调用。由于缓存设计不严谨,缓存失效后,瞬间冲击到数据库层。幸好报警及时修复,差点就遭遇了p0故障。
一般来说,缓存分为客户端缓存和服务器缓存。最常见的客户端缓存是浏览器缓存,它是通过http控制的缓存。
基于请求响应模式,大多数场景下客户端通过https协议请求后台获取数据。如果高频接口每天被调用数百万次,即使是短期的客户端缓存也会带来高效的收益。
由于客户端到服务器要经过较长的网络链路以及多变的网络环境,数据包小到几十K,大到几十M,使得复杂多变的网络得救了。要求的时间。
客户端缓存减少了客户端和服务器之间的通信次数和成本。只要缓存可用,就可以及时响应数据。
最常见的客户端缓存是浏览器缓存。简而言之,就是http缓存。不知道大家在实际开发过程中是否使用过这段代码:
ResponseEntity.ok().cacheControl(CacheControl.maxAge(3, TimeUnit.SECONDS)).body()
看他的包,是springframework框架下的http包下的工具类。
导入org.springframework.http.CacheControl;导入org.springframework.http.ResponseEntity;
在浏览器缓存中,http协议头有这样一个键值字段进行控制,称为Cache-Control:max-age=30,max-age标记了资源缓存在多少上客户 第二。
如果max-age=0,则表示不缓存数据。除了max-age可以控制数据的缓存状态之外,还有以下三个属性可以控制缓存状态no_store、no_cache、must-revalidate。
除了Cache-Control可以使用客户端缓存之外,http中还有一个条件请求头可以更智能地使用客户端缓存。
条件请求是根据响应消息中返回的“Last-modified”和“ETag”来实现的。 Last-modified 资源的最后修改时间,ETag 代表资源的唯一标识。你可以理解,只要修改资源,就会不一样。
再次请求时,请求头中会包含“If-None-Match:ETag 返回的值”,以验证资源是否有效。
有效,则会返回“304 Not Modified”,表示缓存资源有效,可以继续使用。
不过我很少用这个方法。基本上,使用Cache-Control就足够了。控制实际效果的时间。一般场景下,短期的不一致是允许的。
客户端除了可以发送Cache-Control之外,还可以发送Cache-Control来协商客户端缓存的使用。
比如我在浏览器中访问一个连接,在输入框中按回车,请求头中有Cache-Control: max-age = 0,表示不使用缓存,数据是直接从后台获取。
所以Cache-Control不太容易控制客户端缓存。这需要两者之间的协商。然而,Cache-Control 的优点之一是它可以控制 CDN 缓存。
CDN服务一般是第三方提供的内容分发网络服务。主要用于缓存静态数据,如图片、音频、视频等。这些数据并不是恒定的,所以命中率非常高。 。
无需回源获取数据,效率高。毕竟使用CDN的成本很高。一般小公司不会用,但大公司可能会采用。
CDN厂商花费大量资金在全国各地建立CDN服务站点,供用户就近访问,减少响应时间。
所以这是应用层的零开发。一般情况下,您只需要针对您的服务管理平台的某个接口进行配置即可。
除了缓存静态数据之外,想想一些动态数据,但是不经常变化的数据也可以通过CDN来缓存。不过缓存时间可能会设置的比较短,所以在高并发下能够在地下获得的好处也比较大。
好了,CDN就不多说了。我没有接触过CDN开发,但是在项目中使用过。这只是一个简单的配置。等我接触到开发了再和大家详细聊聊。 CDN实际上只是充当源站缓存数据的代理。
服务器端缓存中Redis缓存可能是我们最常见的缓存了。可以说Redis已经是各大公司普遍使用的缓存中间件的首选。这并不夸张。
我们的项目中也在使用Redis,但是是在Redis层面进行封装,包括Redis Sentinel、Redis Sharding,这些都是根据我们自己的业务情况进行二次开发,然后提供给自己的商业用途。
Redis的基本数据类型中,有String、List、Set、SortedSet、Hash五种主要类型供您选择。这五种数据类型可以解决95%的场景,并且在这五种基本数据类型的底层都采用了高效且节省空间的数据结构。因此,Redis的高性能之一也是得益于这些数据结构作为支撑。
例如:实行排行榜,突出直播间顶哥和榜单顶哥的财富和实力。
所以这显然是按照某个字段排序的,比如对抖音硬币进行排序。在redis中,List和Sorted Set都可以实现有序缓存。
List按照List写入的顺序存储,而Sorted Set则是按照某个字段的权重排序,可以查询权重范围内的数据。
List 可能不适合我们的场景,因为数据每次都是新生成的,并且是按时间顺序写入的,所以 List 集合更适合。
我们的场景是,名单上的某个兄弟一直在买礼物,所以他需要重新排序。该数字将基于老大哥购买的抖音硬币,而不是按照每个新添加到缓存的顺序获取数字。多少可以作为权重来排序,非常适合我们的场景。排序集还可以用于根据时间对场景进行排序。
还有一些聚合统计的场景。例如,如果要统计两个关键数据集的交集、并集、差集,可以使用集合集合。
假设有一天你的老板要求你开发一个功能来统计每天新增的用户数据。事实上,只有两组差异。一组设置用户保存所有用户的ID,另一组设置用户保存当天用户的ID。设置,然后当天的用户集与所有用户集的差值就是新的用户集。这可以使用集合集合中的 SDIFSTORE 命令来实现。
还有一些二进制统计场景,即基于redis的Bitmap进行统计。它并不记录数据本身,而只能判断数据是否存在。 Bitmap节省了比特,因此数十亿级的存储只需要M级的存储单元,因此Bitmap非常节省空间。
Bitmap 提供了SETBIT 来设置位,以及GETBIT 来获取某个位的值。您还可以使用BITCOUNT来统计第1位的值,例如可以统计某月的签到场景。
Redis的高性能缓存给我们的系统带来了很大的性能提升,但同时也存在一些类型的问题,比如数据一致性问题以及三大缓存问题(击穿、穿透传输) 、雪崩),与Redis的网络通信接口超时,更多的数据缓存在Redis中,操作时间的复杂性导致Redis变慢。
数据一致性问题是指缓存和数据库之间的一致性问题。只要使用了缓存,就会存在一致性问题。现在市场不需要强一致性。他们都追求最终一致性。
缓存根据是否可写分为读写缓存和只读缓存。其中大多数是只读缓存。现在我们来讨论只读缓存一致性问题。
只读缓存的一致性问题包括以下两种场景:
但这两种场景在高并发场景下都会出现问题。我们看第一个场景:先更新数据库,然后删除缓存。
这种场景也会存在一致性问题。当我们更新数据库然后删除缓存后,缓存删除失败。此时请求读取的缓存仍然是旧值。
这种情况的解决方案是重试。可以在应用层重试,也可以放到消息队列中重试。当重试次数达到最大限制时,需要发送警报。进行了人工检查。
第二种场景:先删除缓存,再更新数据库。高并发场景下也可能出现数据不一致的情况。 如果线程A删除了缓存,但还没有更新数据库,然后线程B读取缓存发现缓存缺失,则从数据库中读取旧值并缓存到Redis中,后续请求会读取从 Redis 获取旧值。 但是,这种一般的等待时间无法正确估计,而且在高并发场景下,让线程等待无疑会降低性能,而这通常是不允许的。 所以一般建议采用第一种方案,即先更新数据库,再删除缓存。 我们的项目中也会用到读写缓存。之前我们遇到的一个需求是,直播时,主播公屏的码流中要显示用户中奖的横幅,即“恭喜某某某直播。我随机赢得了XXX礼物。” 礼物抽奖流程之前已经发送到消息队列了,所以只需要监听对应的抽奖流程Topic,然后按照排序规则将中奖流程放入Redis即可。然后客户端从Redis中读取,其实很简单。管道中不需要存储,只需显示即可。 所以Redis的应用场景还是很多的,几乎可以覆盖开发中95%的需求 缓存击穿、渗透、雪崩 使用分布式缓存还会涉及三大缓存问题,即缓存击穿、缓存穿透、缓存雪崩。 缓存穿透有两种解决方案: 缓存空对象:代码维护比较简单,但效果不好。 布隆过滤器:代码维护复杂,但非常有效。 缓存空对象是指当请求进来时,所请求的数据在缓存中或数据库中不存在。第一个请求会跳过缓存访问数据库,访问数据库后结果为空。这时候,Cache这个空对象。 如果再次访问空对象,它会直接命中缓存,而不是再次命中数据库。缓存空对象示意图如下: ? rediscache;公共用户finduser(integer id){object = rediscache.get(integer.tostring(id);// caches存在,并直接返回到(对象) != NULL) { // 测试该对象是否为缓存空对象,如果是则直接返回null。return null; (用户)对象; } else { use using using using using using 必须这样做 SIf (user!= Null) { rediscache.put (integer.tostring (ID)) , user); 对象存储在缓存中 redisCache.put(Integer.toString(id), new NullValueResultDO()); > 该结构体主要用于判断某个元素是否在集合中。它具有运行速度快(时间效率)、内存占用小(空间效率)的优点,但存在一定的误识别率和删除困难。它只能告诉你一个元素肯定不在集合中或者可能在集合中。 计算机科学中有一个思想:空间换时间,时间换空间。一般两者不能同时实现,但布隆过滤器却实现了运行效率和空间大小的兼顾。它是如何实现这一目标的? 布隆过滤器引用了一个误报率的概念,即它可能认为不属于这个集合的元素可能属于这个集合,但不会认为属于这个集合的元素不属于这个集合这一套。长过滤器的特点如下: 一个非常大的二进制位数组(数组中只有0和1) 几个哈希函数 空间效率和查询效率高 不存在误报(False Negative):如果某个元素在某个集合中,那么肯定可以被报告。 可能存在误报(False Positive):某个元素不在某个集合中,也可能被爆炸。 不提供删除方法,代码维护困难。 位数组初始化为0。它不存储元素的具体值。当哈希函数哈希后的元素值(即数组下标)时,对应的数组位置值变为1。 布隆过滤器存储数据和查询数据的实际示意图如下: 缓存击穿意味着某个key非常热,并且不断承载着大并发。大并发重点关注访问这一点。当key失效时,持续的大并发会突破缓存,直接请求数据库瞬间增加数据库的访问压力。 这里的缓存细分强调并发性。缓存崩溃的原因有两个: 没有人查询过该数据,第一次被并发访问。 (冷门数据) 被添加到缓存中,reids设置了数据过期时间,这条数据刚刚过期,大并发访问(热点数据) 缓存崩溃的解决方案: 锁定。当用户并发访问量较大时,在查询缓存时和查询数据库的过程中都会加锁。只能执行第一个传入的请求。当第一个请求将数据放入缓存时,后续访问将直接缓存,防止缓存崩溃。 请勿设置热点密钥过期时间 缓存雪崩是指缓存在一定时间内集中过期。此时此刻,无数的请求直接绕过缓存,直接请求数据库。 缓存雪崩的可能原因有: 雷德倒下了 大部分数据无效 缓存雪崩的解决方案有两种: 搭建高可用集群,防止单机redis宕机。 设置不同的过期时间,防止大量key同时失效。 接口超时,操作时间复杂度高 Redis数据第三方缓存中间件必须通过网络与Redis通信,因此经过网络后可能会出现网络超时的情况。 之前我们也看到过,由于网络波动,某机房出现一系列Redis查询网络超时报警。 所以为了解决网络临时超时的问题,我们可能还需要实现接口重试机制,以提高接口的可用性。 以及熟悉Redis五种基本数据类型底层数据结构、Redis中集合类型的操作HGETALL、SMEMBERS以及集合的聚合统计等,时间复杂度为 O(N) Redis 中存储的数据越多,N 越大,操作的复杂度越高。这就是所谓的bidkey现象,出现了查询阻塞。 当然,出现这种问题时,可以按照一定的规则拆分bigkey。这样就可以分成多个key来存储,查询效率会更高。 当然,也可以采用Redis数据分片方案,将所有数据存储在原实例中,根据 16384 crc16(key) % 16384确定数据存储在哪个槽位。 这种方式也具有较好的可扩展性,但一般建议拆分密钥方案,实现成本低,实现简单。 缓存消息队列玩法 在某些场景下,消息队列还可以用来更新缓存。用户更新数据并异步发送消息队列。消费者可以监控消息队列并在消费完消息后更新缓存。 因为有些数据更新需要发送到消息队列并被其他消费者监听,所以只需要监听消息队列即可。 并且消息队列中的消息是有消息队列保证的,包括生产者对消息队列的可靠发送,有ack和重试保证,消息队列本身有持久化机制保证,消费者也有保证由消费后手动ack来确认消息消费。 计划任务 计划任务实际上是本地缓存。在分布式系统中,计划任务会缓存在每个服务中,这会增加数据的不一致。 但在某些场景下,它带来的好处也是非常可观的。例如,在某种场景下,需要查询一些安全中间平台的白名单/黑名单,而这些列表不会经常变化。需求上线后,只需配置即可,后续变更频率也很低。 但是你的界面可能是一个高流量的界面。用户每次进来都会请求一次,进行判断。而如果有几千万的用户,那么一天可能会有几百万的请求。 那么你请求安全中心的白名单/黑名单有两种选择,要么实时请求,要么定时任务请求本地缓存,然后查询只需要本地获取即可。 这样的话,一定是计划任务请求,这样会带来更大的好处。在SprngBoot项目中启用计划任务非常简单。你只需要在你的主启动类中添加这个注解即可:**@EnableScheduling* * 然后在需要定时任务的执行类的方法上添加这个注解:**@Scheduled(cron = “0 0 2 * * ?”)**,其实就是一个cron表达式。执行模式是执行的频率。一次。 只要你的时间配置足够短,数据就会接近实时,不会相差太远。您可以将其配置为每 30 秒、数十秒或每几分钟执行一次。这可以与产品协商,看看产品可以容忍多少延迟。 然后将查询到的中心的列表数据缓存到本地地图中,并以用户的uid作为地图的key。那么后面需要查询的时候就直接从地图中获取。 这样就不需要每次请求进来的时候都调用中心的http/rpc接口实时查询数据,直接从本地获取,提高效率。这也是用空间换时间的想法。 接口超时 这里需要注意的是,您需要提高接口调用的可用性。毕竟中心属于另一个服务,所以服务之间如果涉及到远程调用,可能会出现超时的情况。 那么你必须确保你的接口99.9%可用。对于接口超时,可以设置接口重试。 因为有时候可能是网络原因导致的暂时超时。如果被叫方因网络抖动而设置暂时超时,那么重试成功的概率可能会更高。 一般重试次数设置为2-3比较合理,除非网络故障或者接口无法调整。这种情况需要及时报警,通知开发商并及时检查。无论哪里出现问题,都要确保对接口有彻底的解决方案。 而且每次还需要设置超时时间。设置超时时间也很重要。如果超时时间设置太短,还没检测到就超时了,会导致频繁超时,造成资源浪费。 如果超时时间设置得太长,线程会一直阻塞等待调用结果返回。这样,在高并发场景下,就会出现资源耗尽、系统崩溃的情况。 所以我给你的建议是结合在线服务所在服务器的配置和qps来配置,配置合理的超时时间,这样可以在合理的时间内超时返回,而不会造成资源耗尽。 重试机制涉及到很多中间件思想,比如:分布式事务中的2PC和3PC。 2PC第二阶段提交失败,只能重试,直到所有参与者都成功(回滚或者提交成功)。 因为除了重试,没有更好的办法了。你只能不断重试,直到成功为止。大多数情况下,可能是暂时的网络抖动造成的,所以重试成功的概率非常高。 批量查询数据 计划任务缓存实际上是一种集中式缓存。如果缓存的数据量比较大,那么调用接口的时候需要批量获取,但是不能一次查询太多,所以一般都是严谨的。中端设计时,所有参数都经过参数验证。 因为它对调用者完全透明且不可信任,所以任何参数都可以传递。如果调用者一次性检查几万、几万个数据集,也不是接口会爆炸。 。 因此,必须批量调用。主叫方批量、寻呼呼叫。中台一次只能查询几百个参数。这样做是为了保证接口。可用性。 调用方伪代码如下:布尔结束 = true;int page = 1;int pageSize = 500;while(结束){ // 设置超时时间, 失败重试 Data data = getData(page, pageSize); 地图 map =data.getDataMap(); // data 中的字段 hashMore 代表查询中是否还有数据下一页? end = Objects.equals(data.getHasMore(),1); page++;} 本地缓存@Cacheable @Cacheable是springframework下提供的缓存注解类。 spring 中定义了 org.springframework.cache.Cache 和 org.springframework.cache.CacheManager 接口,统一实现。缓存。 除了@Cacheable用户缓存数据之外,@CachePut还可以用于缓存更新。这两个比较常用。 它们缓存的数据也是缓存在本地的,就像定时任务一样。除了使用@Cacheable之外,还可以使用Google开发的缓存工具类LoadingCache。它也是本地缓存的一种,可以设置缓存大小、重新刷新时间。 与Cacheable相比,会更方便,因为你发现Cacheable还是缺少缓存时间和缓存更新的属性配置实现。你可能需要自己做二次开发,比如添加缓存过期时间,多少秒后自动更新缓存。这样Cacheable就可以更加完美。 private最终LoadingCache> tagCache = CacheBuilder.newBuilder() .concurrencyLevel(4) .expireAfterWrite(5, TimeUnit.SECON DS) .initialCapacity(8) .maximumSize (10000).build(新的cacheloader>(){@overridepublic list load load(string cachekey){returnget(cachekey) ); } @Override 公众听听ableFuture> 重新加载(String key, List oldValue) { ListenableFutureTask> 任务 = ListenableFutureTask.create( () -> get(key)); executorService.execute(task); 返回 任务; } } ); 与@Cacheable相比,代码冗余较多,注解方式更加直观简洁,但LoadingCache更加灵活。 我们自己扩展了Cacheable,添加了生效时间和自动更新解决方案。这种Cacheable比较适合我们的业务。 总结 在一个项目中,可能会并行使用多个缓存。使用不同的缓存必须基于业务考虑,包括成本、数据一致性、性能问题等。不同的缓存方式有不同的特点。 Redis在分布式系统中缓存共享数据,性能高效,可扩展性强。 Redis可以基于数据分片和哨兵模式进行扩展,但是需要额外的运维成本,并且引入第三方中间件,使得系统复杂度也很高,排查困难,每次都是通过网络调用,可能会出现网络超时和数据丢失的情况,因此必须做好数据兼容性和安全预案。 本地缓存使用简单方便,成本低廉。每个服务实例都会有数据的冗余副本,增加了一致性问题,但效率很高,不需要通过网络传输获取数据。 一般我们的项目都会分配6-8G的内存,所以本地缓存一般就够用了。因此,如果本地缓存一般都可以使用,则可以优先使用本地缓存。 有些场景不得不使用分布式缓存,也就是使用Redis缓存来共享数据,使用不同的缓存来解决项目中的问题。 从上面几种缓存方案中,我们可以看到重试方案。重试是解决很多问题的重要手段之一,但也必须控制重试次数和重试超时时间,防止资源耗尽。大多数情况下,重试就可以解决问题。如果重试次数达到限制但仍失败,则可能是网络故障或接口问题。这种情况下,应用程序需要发送警报,通知开发者排除故障。这是一个万无一失的解决方案。 客户端缓存和CDN缓存在服务器端相对较少使用。大多数公司不使用它们。您可以专注于服务器端缓存。 好了,今天就聊这么多。我是李杜,一名从非技术班到大厂的java bug 工程师。我挖坑,然后把它们全部填满。我们下期再聊吧。我们来谈谈如何在大型分布式系统中创建高效的通信架构。 精彩推荐 最全Java面试题库 高仿“微信”开源版太棒了! 而不是在网上搜索问题? 还不赶快关注我们吧~
第二种场景:先删除缓存,再更新数据库。高并发场景下也可能出现数据不一致的情况。
如果线程A删除了缓存,但还没有更新数据库,然后线程B读取缓存发现缓存缺失,则从数据库中读取旧值并缓存到Redis中,后续请求会读取从 Redis 获取旧值。
但是,这种一般的等待时间无法正确估计,而且在高并发场景下,让线程等待无疑会降低性能,而这通常是不允许的。 所以一般建议采用第一种方案,即先更新数据库,再删除缓存。 我们的项目中也会用到读写缓存。之前我们遇到的一个需求是,直播时,主播公屏的码流中要显示用户中奖的横幅,即“恭喜某某某直播。我随机赢得了XXX礼物。” 礼物抽奖流程之前已经发送到消息队列了,所以只需要监听对应的抽奖流程Topic,然后按照排序规则将中奖流程放入Redis即可。然后客户端从Redis中读取,其实很简单。管道中不需要存储,只需显示即可。 所以Redis的应用场景还是很多的,几乎可以覆盖开发中95%的需求 缓存击穿、渗透、雪崩 使用分布式缓存还会涉及三大缓存问题,即缓存击穿、缓存穿透、缓存雪崩。 缓存穿透有两种解决方案: 缓存空对象:代码维护比较简单,但效果不好。 布隆过滤器:代码维护复杂,但非常有效。 缓存空对象是指当请求进来时,所请求的数据在缓存中或数据库中不存在。第一个请求会跳过缓存访问数据库,访问数据库后结果为空。这时候,Cache这个空对象。 如果再次访问空对象,它会直接命中缓存,而不是再次命中数据库。缓存空对象示意图如下: ? rediscache;公共用户finduser(integer id){object = rediscache.get(integer.tostring(id);// caches存在,并直接返回到(对象) != NULL) { // 测试该对象是否为缓存空对象,如果是则直接返回null。return null; (用户)对象; } else { use using using using using using 必须这样做 SIf (user!= Null) { rediscache.put (integer.tostring (ID)) , user); 对象存储在缓存中 redisCache.put(Integer.toString(id), new NullValueResultDO()); > 该结构体主要用于判断某个元素是否在集合中。它具有运行速度快(时间效率)、内存占用小(空间效率)的优点,但存在一定的误识别率和删除困难。它只能告诉你一个元素肯定不在集合中或者可能在集合中。 计算机科学中有一个思想:空间换时间,时间换空间。一般两者不能同时实现,但布隆过滤器却实现了运行效率和空间大小的兼顾。它是如何实现这一目标的? 布隆过滤器引用了一个误报率的概念,即它可能认为不属于这个集合的元素可能属于这个集合,但不会认为属于这个集合的元素不属于这个集合这一套。长过滤器的特点如下: 一个非常大的二进制位数组(数组中只有0和1) 几个哈希函数 空间效率和查询效率高 不存在误报(False Negative):如果某个元素在某个集合中,那么肯定可以被报告。 可能存在误报(False Positive):某个元素不在某个集合中,也可能被爆炸。 不提供删除方法,代码维护困难。 位数组初始化为0。它不存储元素的具体值。当哈希函数哈希后的元素值(即数组下标)时,对应的数组位置值变为1。 布隆过滤器存储数据和查询数据的实际示意图如下: 缓存击穿意味着某个key非常热,并且不断承载着大并发。大并发重点关注访问这一点。当key失效时,持续的大并发会突破缓存,直接请求数据库瞬间增加数据库的访问压力。 这里的缓存细分强调并发性。缓存崩溃的原因有两个: 没有人查询过该数据,第一次被并发访问。 (冷门数据) 被添加到缓存中,reids设置了数据过期时间,这条数据刚刚过期,大并发访问(热点数据) 缓存崩溃的解决方案: 锁定。当用户并发访问量较大时,在查询缓存时和查询数据库的过程中都会加锁。只能执行第一个传入的请求。当第一个请求将数据放入缓存时,后续访问将直接缓存,防止缓存崩溃。 请勿设置热点密钥过期时间 缓存雪崩是指缓存在一定时间内集中过期。此时此刻,无数的请求直接绕过缓存,直接请求数据库。 缓存雪崩的可能原因有: 雷德倒下了 大部分数据无效 缓存雪崩的解决方案有两种: 搭建高可用集群,防止单机redis宕机。 设置不同的过期时间,防止大量key同时失效。 接口超时,操作时间复杂度高 Redis数据第三方缓存中间件必须通过网络与Redis通信,因此经过网络后可能会出现网络超时的情况。 之前我们也看到过,由于网络波动,某机房出现一系列Redis查询网络超时报警。 所以为了解决网络临时超时的问题,我们可能还需要实现接口重试机制,以提高接口的可用性。 以及熟悉Redis五种基本数据类型底层数据结构、Redis中集合类型的操作HGETALL、SMEMBERS以及集合的聚合统计等,时间复杂度为 O(N) Redis 中存储的数据越多,N 越大,操作的复杂度越高。这就是所谓的bidkey现象,出现了查询阻塞。 当然,出现这种问题时,可以按照一定的规则拆分bigkey。这样就可以分成多个key来存储,查询效率会更高。 当然,也可以采用Redis数据分片方案,将所有数据存储在原实例中,根据 16384 crc16(key) % 16384确定数据存储在哪个槽位。 这种方式也具有较好的可扩展性,但一般建议拆分密钥方案,实现成本低,实现简单。 缓存消息队列玩法 在某些场景下,消息队列还可以用来更新缓存。用户更新数据并异步发送消息队列。消费者可以监控消息队列并在消费完消息后更新缓存。 因为有些数据更新需要发送到消息队列并被其他消费者监听,所以只需要监听消息队列即可。 并且消息队列中的消息是有消息队列保证的,包括生产者对消息队列的可靠发送,有ack和重试保证,消息队列本身有持久化机制保证,消费者也有保证由消费后手动ack来确认消息消费。 计划任务 计划任务实际上是本地缓存。在分布式系统中,计划任务会缓存在每个服务中,这会增加数据的不一致。 但在某些场景下,它带来的好处也是非常可观的。例如,在某种场景下,需要查询一些安全中间平台的白名单/黑名单,而这些列表不会经常变化。需求上线后,只需配置即可,后续变更频率也很低。 但是你的界面可能是一个高流量的界面。用户每次进来都会请求一次,进行判断。而如果有几千万的用户,那么一天可能会有几百万的请求。 那么你请求安全中心的白名单/黑名单有两种选择,要么实时请求,要么定时任务请求本地缓存,然后查询只需要本地获取即可。 这样的话,一定是计划任务请求,这样会带来更大的好处。在SprngBoot项目中启用计划任务非常简单。你只需要在你的主启动类中添加这个注解即可:**@EnableScheduling* * 然后在需要定时任务的执行类的方法上添加这个注解:**@Scheduled(cron = “0 0 2 * * ?”)**,其实就是一个cron表达式。执行模式是执行的频率。一次。 只要你的时间配置足够短,数据就会接近实时,不会相差太远。您可以将其配置为每 30 秒、数十秒或每几分钟执行一次。这可以与产品协商,看看产品可以容忍多少延迟。 然后将查询到的中心的列表数据缓存到本地地图中,并以用户的uid作为地图的key。那么后面需要查询的时候就直接从地图中获取。 这样就不需要每次请求进来的时候都调用中心的http/rpc接口实时查询数据,直接从本地获取,提高效率。这也是用空间换时间的想法。 接口超时 这里需要注意的是,您需要提高接口调用的可用性。毕竟中心属于另一个服务,所以服务之间如果涉及到远程调用,可能会出现超时的情况。 那么你必须确保你的接口99.9%可用。对于接口超时,可以设置接口重试。 因为有时候可能是网络原因导致的暂时超时。如果被叫方因网络抖动而设置暂时超时,那么重试成功的概率可能会更高。 一般重试次数设置为2-3比较合理,除非网络故障或者接口无法调整。这种情况需要及时报警,通知开发商并及时检查。无论哪里出现问题,都要确保对接口有彻底的解决方案。 而且每次还需要设置超时时间。设置超时时间也很重要。如果超时时间设置太短,还没检测到就超时了,会导致频繁超时,造成资源浪费。 如果超时时间设置得太长,线程会一直阻塞等待调用结果返回。这样,在高并发场景下,就会出现资源耗尽、系统崩溃的情况。 所以我给你的建议是结合在线服务所在服务器的配置和qps来配置,配置合理的超时时间,这样可以在合理的时间内超时返回,而不会造成资源耗尽。 重试机制涉及到很多中间件思想,比如:分布式事务中的2PC和3PC。 2PC第二阶段提交失败,只能重试,直到所有参与者都成功(回滚或者提交成功)。 因为除了重试,没有更好的办法了。你只能不断重试,直到成功为止。大多数情况下,可能是暂时的网络抖动造成的,所以重试成功的概率非常高。 批量查询数据 计划任务缓存实际上是一种集中式缓存。如果缓存的数据量比较大,那么调用接口的时候需要批量获取,但是不能一次查询太多,所以一般都是严谨的。中端设计时,所有参数都经过参数验证。 因为它对调用者完全透明且不可信任,所以任何参数都可以传递。如果调用者一次性检查几万、几万个数据集,也不是接口会爆炸。 。 因此,必须批量调用。主叫方批量、寻呼呼叫。中台一次只能查询几百个参数。这样做是为了保证接口。可用性。 调用方伪代码如下:布尔结束 = true;int page = 1;int pageSize = 500;while(结束){ // 设置超时时间, 失败重试 Data data = getData(page, pageSize); 地图 map =data.getDataMap(); // data 中的字段 hashMore 代表查询中是否还有数据下一页? end = Objects.equals(data.getHasMore(),1); page++;} 本地缓存@Cacheable @Cacheable是springframework下提供的缓存注解类。 spring 中定义了 org.springframework.cache.Cache 和 org.springframework.cache.CacheManager 接口,统一实现。缓存。 除了@Cacheable用户缓存数据之外,@CachePut还可以用于缓存更新。这两个比较常用。 它们缓存的数据也是缓存在本地的,就像定时任务一样。除了使用@Cacheable之外,还可以使用Google开发的缓存工具类LoadingCache。它也是本地缓存的一种,可以设置缓存大小、重新刷新时间。 与Cacheable相比,会更方便,因为你发现Cacheable还是缺少缓存时间和缓存更新的属性配置实现。你可能需要自己做二次开发,比如添加缓存过期时间,多少秒后自动更新缓存。这样Cacheable就可以更加完美。 private最终LoadingCache> tagCache = CacheBuilder.newBuilder() .concurrencyLevel(4) .expireAfterWrite(5, TimeUnit.SECON DS) .initialCapacity(8) .maximumSize (10000).build(新的cacheloader>(){@overridepublic list load load(string cachekey){returnget(cachekey) ); } @Override 公众听听ableFuture> 重新加载(String key, List oldValue) { ListenableFutureTask> 任务 = ListenableFutureTask.create( () -> get(key)); executorService.execute(task); 返回 任务; } } ); 与@Cacheable相比,代码冗余较多,注解方式更加直观简洁,但LoadingCache更加灵活。 我们自己扩展了Cacheable,添加了生效时间和自动更新解决方案。这种Cacheable比较适合我们的业务。 总结 在一个项目中,可能会并行使用多个缓存。使用不同的缓存必须基于业务考虑,包括成本、数据一致性、性能问题等。不同的缓存方式有不同的特点。 Redis在分布式系统中缓存共享数据,性能高效,可扩展性强。 Redis可以基于数据分片和哨兵模式进行扩展,但是需要额外的运维成本,并且引入第三方中间件,使得系统复杂度也很高,排查困难,每次都是通过网络调用,可能会出现网络超时和数据丢失的情况,因此必须做好数据兼容性和安全预案。 本地缓存使用简单方便,成本低廉。每个服务实例都会有数据的冗余副本,增加了一致性问题,但效率很高,不需要通过网络传输获取数据。 一般我们的项目都会分配6-8G的内存,所以本地缓存一般就够用了。因此,如果本地缓存一般都可以使用,则可以优先使用本地缓存。 有些场景不得不使用分布式缓存,也就是使用Redis缓存来共享数据,使用不同的缓存来解决项目中的问题。 从上面几种缓存方案中,我们可以看到重试方案。重试是解决很多问题的重要手段之一,但也必须控制重试次数和重试超时时间,防止资源耗尽。大多数情况下,重试就可以解决问题。如果重试次数达到限制但仍失败,则可能是网络故障或接口问题。这种情况下,应用程序需要发送警报,通知开发者排除故障。这是一个万无一失的解决方案。 客户端缓存和CDN缓存在服务器端相对较少使用。大多数公司不使用它们。您可以专注于服务器端缓存。 好了,今天就聊这么多。我是李杜,一名从非技术班到大厂的java bug 工程师。我挖坑,然后把它们全部填满。我们下期再聊吧。我们来谈谈如何在大型分布式系统中创建高效的通信架构。 精彩推荐 最全Java面试题库 高仿“微信”开源版太棒了! 而不是在网上搜索问题? 还不赶快关注我们吧~
但是,这种一般的等待时间无法正确估计,而且在高并发场景下,让线程等待无疑会降低性能,而这通常是不允许的。
所以一般建议采用第一种方案,即先更新数据库,再删除缓存。
我们的项目中也会用到读写缓存。之前我们遇到的一个需求是,直播时,主播公屏的码流中要显示用户中奖的横幅,即“恭喜某某某直播。我随机赢得了XXX礼物。”
礼物抽奖流程之前已经发送到消息队列了,所以只需要监听对应的抽奖流程Topic,然后按照排序规则将中奖流程放入Redis即可。然后客户端从Redis中读取,其实很简单。管道中不需要存储,只需显示即可。
所以Redis的应用场景还是很多的,几乎可以覆盖开发中95%的需求
使用分布式缓存还会涉及三大缓存问题,即缓存击穿、缓存穿透、缓存雪崩。
缓存穿透有两种解决方案:
缓存空对象是指当请求进来时,所请求的数据在缓存中或数据库中不存在。第一个请求会跳过缓存访问数据库,访问数据库后结果为空。这时候,Cache这个空对象。
如果再次访问空对象,它会直接命中缓存,而不是再次命中数据库。缓存空对象示意图如下:
? rediscache;公共用户finduser(integer id){object = rediscache.get(integer.tostring(id);// caches存在,并直接返回到(对象) != NULL) { // 测试该对象是否为缓存空对象,如果是则直接返回null。return null; (用户)对象; } else { use using using using using using 必须这样做 SIf (user!= Null) { rediscache.put (integer.tostring (ID)) , user); 对象存储在缓存中 redisCache.put(Integer.toString(id), new NullValueResultDO()); > 该结构体主要用于判断某个元素是否在集合中。它具有运行速度快(时间效率)、内存占用小(空间效率)的优点,但存在一定的误识别率和删除困难。它只能告诉你一个元素肯定不在集合中或者可能在集合中。
计算机科学中有一个思想:空间换时间,时间换空间。一般两者不能同时实现,但布隆过滤器却实现了运行效率和空间大小的兼顾。它是如何实现这一目标的?
布隆过滤器引用了一个误报率的概念,即它可能认为不属于这个集合的元素可能属于这个集合,但不会认为属于这个集合的元素不属于这个集合这一套。长过滤器的特点如下:
布隆过滤器存储数据和查询数据的实际示意图如下:
缓存击穿意味着某个key非常热,并且不断承载着大并发。大并发重点关注访问这一点。当key失效时,持续的大并发会突破缓存,直接请求数据库瞬间增加数据库的访问压力。
这里的缓存细分强调并发性。缓存崩溃的原因有两个:
缓存崩溃的解决方案:
缓存雪崩是指缓存在一定时间内集中过期。此时此刻,无数的请求直接绕过缓存,直接请求数据库。
缓存雪崩的可能原因有:
缓存雪崩的解决方案有两种:
Redis数据第三方缓存中间件必须通过网络与Redis通信,因此经过网络后可能会出现网络超时的情况。
之前我们也看到过,由于网络波动,某机房出现一系列Redis查询网络超时报警。
所以为了解决网络临时超时的问题,我们可能还需要实现接口重试机制,以提高接口的可用性。
以及熟悉Redis五种基本数据类型底层数据结构、Redis中集合类型的操作HGETALL、SMEMBERS以及集合的聚合统计等,时间复杂度为 O(N)
Redis 中存储的数据越多,N 越大,操作的复杂度越高。这就是所谓的bidkey现象,出现了查询阻塞。
当然,出现这种问题时,可以按照一定的规则拆分bigkey。这样就可以分成多个key来存储,查询效率会更高。
当然,也可以采用Redis数据分片方案,将所有数据存储在原实例中,根据 16384 crc16(key) % 16384确定数据存储在哪个槽位。
这种方式也具有较好的可扩展性,但一般建议拆分密钥方案,实现成本低,实现简单。
在某些场景下,消息队列还可以用来更新缓存。用户更新数据并异步发送消息队列。消费者可以监控消息队列并在消费完消息后更新缓存。
因为有些数据更新需要发送到消息队列并被其他消费者监听,所以只需要监听消息队列即可。
并且消息队列中的消息是有消息队列保证的,包括生产者对消息队列的可靠发送,有ack和重试保证,消息队列本身有持久化机制保证,消费者也有保证由消费后手动ack来确认消息消费。
计划任务实际上是本地缓存。在分布式系统中,计划任务会缓存在每个服务中,这会增加数据的不一致。
但在某些场景下,它带来的好处也是非常可观的。例如,在某种场景下,需要查询一些安全中间平台的白名单/黑名单,而这些列表不会经常变化。需求上线后,只需配置即可,后续变更频率也很低。
但是你的界面可能是一个高流量的界面。用户每次进来都会请求一次,进行判断。而如果有几千万的用户,那么一天可能会有几百万的请求。
那么你请求安全中心的白名单/黑名单有两种选择,要么实时请求,要么定时任务请求本地缓存,然后查询只需要本地获取即可。
这样的话,一定是计划任务请求,这样会带来更大的好处。在SprngBoot项目中启用计划任务非常简单。你只需要在你的主启动类中添加这个注解即可:**@EnableScheduling* *
然后在需要定时任务的执行类的方法上添加这个注解:**@Scheduled(cron = “0 0 2 * * ?”)**,其实就是一个cron表达式。执行模式是执行的频率。一次。
只要你的时间配置足够短,数据就会接近实时,不会相差太远。您可以将其配置为每 30 秒、数十秒或每几分钟执行一次。这可以与产品协商,看看产品可以容忍多少延迟。
然后将查询到的中心的列表数据缓存到本地地图中,并以用户的uid作为地图的key。那么后面需要查询的时候就直接从地图中获取。
这样就不需要每次请求进来的时候都调用中心的http/rpc接口实时查询数据,直接从本地获取,提高效率。这也是用空间换时间的想法。
这里需要注意的是,您需要提高接口调用的可用性。毕竟中心属于另一个服务,所以服务之间如果涉及到远程调用,可能会出现超时的情况。
那么你必须确保你的接口99.9%可用。对于接口超时,可以设置接口重试。
因为有时候可能是网络原因导致的暂时超时。如果被叫方因网络抖动而设置暂时超时,那么重试成功的概率可能会更高。
一般重试次数设置为2-3比较合理,除非网络故障或者接口无法调整。这种情况需要及时报警,通知开发商并及时检查。无论哪里出现问题,都要确保对接口有彻底的解决方案。
而且每次还需要设置超时时间。设置超时时间也很重要。如果超时时间设置太短,还没检测到就超时了,会导致频繁超时,造成资源浪费。
如果超时时间设置得太长,线程会一直阻塞等待调用结果返回。这样,在高并发场景下,就会出现资源耗尽、系统崩溃的情况。
所以我给你的建议是结合在线服务所在服务器的配置和qps来配置,配置合理的超时时间,这样可以在合理的时间内超时返回,而不会造成资源耗尽。
重试机制涉及到很多中间件思想,比如:分布式事务中的2PC和3PC。
2PC第二阶段提交失败,只能重试,直到所有参与者都成功(回滚或者提交成功)。
因为除了重试,没有更好的办法了。你只能不断重试,直到成功为止。大多数情况下,可能是暂时的网络抖动造成的,所以重试成功的概率非常高。
计划任务缓存实际上是一种集中式缓存。如果缓存的数据量比较大,那么调用接口的时候需要批量获取,但是不能一次查询太多,所以一般都是严谨的。中端设计时,所有参数都经过参数验证。
因为它对调用者完全透明且不可信任,所以任何参数都可以传递。如果调用者一次性检查几万、几万个数据集,也不是接口会爆炸。 。
因此,必须批量调用。主叫方批量、寻呼呼叫。中台一次只能查询几百个参数。这样做是为了保证接口。可用性。
调用方伪代码如下:
布尔结束 = true;int page = 1;int pageSize = 500;while(结束){ // 设置超时时间, 失败重试 Data data = getData(page, pageSize); 地图 map =data.getDataMap(); // data 中的字段 hashMore 代表查询中是否还有数据下一页? end = Objects.equals(data.getHasMore(),1); page++;}
@Cacheable是springframework下提供的缓存注解类。 spring 中定义了 org.springframework.cache.Cache 和 org.springframework.cache.CacheManager 接口,统一实现。缓存。
除了@Cacheable用户缓存数据之外,@CachePut还可以用于缓存更新。这两个比较常用。
它们缓存的数据也是缓存在本地的,就像定时任务一样。除了使用@Cacheable之外,还可以使用Google开发的缓存工具类LoadingCache。它也是本地缓存的一种,可以设置缓存大小、重新刷新时间。
与Cacheable相比,会更方便,因为你发现Cacheable还是缺少缓存时间和缓存更新的属性配置实现。你可能需要自己做二次开发,比如添加缓存过期时间,多少秒后自动更新缓存。这样Cacheable就可以更加完美。
private最终LoadingCache> tagCache = CacheBuilder.newBuilder() .concurrencyLevel(4) .expireAfterWrite(5, TimeUnit.SECON DS) .initialCapacity(8) .maximumSize (10000).build(新的cacheloader>(){@overridepublic list load load(string cachekey){returnget(cachekey) ); } @Override 公众听听ableFuture> 重新加载(String key, List oldValue) { ListenableFutureTask> 任务 = ListenableFutureTask.create( () -> get(key)); executorService.execute(task); 返回 任务; } } );
与@Cacheable相比,代码冗余较多,注解方式更加直观简洁,但LoadingCache更加灵活。
我们自己扩展了Cacheable,添加了生效时间和自动更新解决方案。这种Cacheable比较适合我们的业务。 总结 在一个项目中,可能会并行使用多个缓存。使用不同的缓存必须基于业务考虑,包括成本、数据一致性、性能问题等。不同的缓存方式有不同的特点。 Redis在分布式系统中缓存共享数据,性能高效,可扩展性强。 Redis可以基于数据分片和哨兵模式进行扩展,但是需要额外的运维成本,并且引入第三方中间件,使得系统复杂度也很高,排查困难,每次都是通过网络调用,可能会出现网络超时和数据丢失的情况,因此必须做好数据兼容性和安全预案。 本地缓存使用简单方便,成本低廉。每个服务实例都会有数据的冗余副本,增加了一致性问题,但效率很高,不需要通过网络传输获取数据。 一般我们的项目都会分配6-8G的内存,所以本地缓存一般就够用了。因此,如果本地缓存一般都可以使用,则可以优先使用本地缓存。 有些场景不得不使用分布式缓存,也就是使用Redis缓存来共享数据,使用不同的缓存来解决项目中的问题。 从上面几种缓存方案中,我们可以看到重试方案。重试是解决很多问题的重要手段之一,但也必须控制重试次数和重试超时时间,防止资源耗尽。大多数情况下,重试就可以解决问题。如果重试次数达到限制但仍失败,则可能是网络故障或接口问题。这种情况下,应用程序需要发送警报,通知开发者排除故障。这是一个万无一失的解决方案。 客户端缓存和CDN缓存在服务器端相对较少使用。大多数公司不使用它们。您可以专注于服务器端缓存。 好了,今天就聊这么多。我是李杜,一名从非技术班到大厂的java bug 工程师。我挖坑,然后把它们全部填满。我们下期再聊吧。我们来谈谈如何在大型分布式系统中创建高效的通信架构。 精彩推荐 最全Java面试题库 高仿“微信”开源版太棒了! 而不是在网上搜索问题? 还不赶快关注我们吧~
在一个项目中,可能会并行使用多个缓存。使用不同的缓存必须基于业务考虑,包括成本、数据一致性、性能问题等。不同的缓存方式有不同的特点。
Redis在分布式系统中缓存共享数据,性能高效,可扩展性强。 Redis可以基于数据分片和哨兵模式进行扩展,但是需要额外的运维成本,并且引入第三方中间件,使得系统复杂度也很高,排查困难,每次都是通过网络调用,可能会出现网络超时和数据丢失的情况,因此必须做好数据兼容性和安全预案。
本地缓存使用简单方便,成本低廉。每个服务实例都会有数据的冗余副本,增加了一致性问题,但效率很高,不需要通过网络传输获取数据。
一般我们的项目都会分配6-8G的内存,所以本地缓存一般就够用了。因此,如果本地缓存一般都可以使用,则可以优先使用本地缓存。
有些场景不得不使用分布式缓存,也就是使用Redis缓存来共享数据,使用不同的缓存来解决项目中的问题。
从上面几种缓存方案中,我们可以看到重试方案。重试是解决很多问题的重要手段之一,但也必须控制重试次数和重试超时时间,防止资源耗尽。大多数情况下,重试就可以解决问题。如果重试次数达到限制但仍失败,则可能是网络故障或接口问题。这种情况下,应用程序需要发送警报,通知开发者排除故障。这是一个万无一失的解决方案。
客户端缓存和CDN缓存在服务器端相对较少使用。大多数公司不使用它们。您可以专注于服务器端缓存。
好了,今天就聊这么多。我是李杜,一名从非技术班到大厂的java bug 工程师。我挖坑,然后把它们全部填满。我们下期再聊吧。我们来谈谈如何在大型分布式系统中创建高效的通信架构。
精彩推荐 最全Java面试题库 高仿“微信”开源版太棒了! 而不是在网上搜索问题? 还不赶快关注我们吧~
精彩推荐
最全Java面试题库
高仿“微信”开源版太棒了!
而不是在网上搜索问题? 还不赶快关注我们吧~