SERVICE PHONE
363050.com发布时间:2026-01-04 15:43:02 点击量:
哈希交易所官网入口,哈希交易所注册,哈希交易所登录,哈希交易所下载,哈希交易所APP/哈希交易所官网入口为用户提供官方注册、登录、APP下载与币种交易服务,安全合规,快速充值提现,全面满足数字资产投资需求。
从上一篇我们已经了解了选择缓存需要考虑缓存吞吐量、缓存命中率、缓存扩展机制、缓存是否支持分布式几个维度, 还分别了解了几种Java 本地缓存。进程本地缓存因为就在进程的内存里面,不需要网络和对象拷贝的开销所以性能非常高,不过也正因为数据保存在进程的内存中也有了很多限制。
2、不支持技术异构:另外进程级的缓存一般都和对应的开发语言绑定,无法提供给不同的开发语言使用。
3、无法扩展:缓存是绑定着进程共生共死的,其资源的分配和使用都限制其应用程序,也无法进行节点扩展。
4、没有持久化机制:同时因为数据都保存在内存中的不会进行持久化,所以一旦进程停止了缓存数据就都没有了。
所以综合来看,我们需要有一种支持多进程共享、数据持久化、技术异构的缓存,这种缓存也就是我们所熟知的分布式缓存。
本地缓存虽然性能会更高,但其短板也很明显,为了解决这些问题,以Jboss cache、memcache、redis为代表的分布式缓存也开始出现,它们以不同的方式实现了分布式缓存的特性。
通过同步数据达到共享:如果想让多个进程可以共享缓存数据,那么是不是可以通过一种方式,当一个进程的缓存变化时,把变化的内容通知给其它进程呢,从而使得任何一个进程缓存的数据都能同步到其他进程缓存去,从而达到类似共享的效果,这也就是Jboss Cache的思想,JBoss Cache 会部署到应用服务器中,当某一个应用的缓存有变更的时候就会把变更的信息同步到其他应用服务器的Jboss Cache中。 这种方案的确也解决了共享的问题,这种缓存同步的操作如果太多那么本身就会成为一个很大的性能问题。
独立部署实现共享:把缓存独立出来单独进行部署和访问,所有的进程都通过网络请求从缓存服务器存取数据,MemCache和Redis就是独立开来的,不过独立出来的缓存在读取数据的时候就会引入额外的网络开销,相比于直接在本地就能获取的数据,自然会有额外的性能开销。
是否具备持久化机制也是使用分布式缓存需要关注的,这取决于存放到缓存的数据安全,如果你想缓存当做直接存储数据的地方,那么缓存就必须支持持久化机制,在redis 中提供了RDB 和AOF两种形式的持久化策略,通过不同的配置可以让缓存数据实时或定时同步到磁盘中以保证数据的安全,不过为此也要牺牲部分性能来保证数据的持久化。在Memcache中数据是不持久化的,数据全部放在内存中,一旦进程停止那么数据就都没有了。如果你想直接把缓存当数据最后存储的容器,那么需要可以支持持久化的缓存。如果数据都最终会保存到数据库中,那么持久化对你来说就无关紧要了。
单台服务器的内存资源总是有局限的,当缓存数据量规模很大时,那么单台服务器的内存是无法容纳的,那么此时就需要支持缓存的节点扩展,需要把缓存的数据分配到多个节点的能力。最简单的做法就是在保存数据时对缓存的KEY进行哈希,得到一个哈希值后对节点的数量进行取余,得到一个服务器编号,然后对应KEY的缓存数据就会保存到该服务器,读取数据的时候逻辑也类似。
简单的取余的确可以把数据分配到不同的缓存节点中解决数据分配和读取问题,但是这种方式在动态增加或删除节点后,需要整体对缓存进行一次重新分配,这个数据重新分配和迁移的过程对运行中的系统影响很大。 另外一方面这种方式也容易出现数据倾斜导致很多数据都集中在一个节点的情况,而一致性哈希算法可以很好的缓解这个问题,关于一致性哈希参考:440059。
如果你已经选好了适合项目场景的缓存已经开始使用了,虽然系统的性能得到了提升,不过随之而来的也出现了很多缓存使用的问题。
一般情况下缓存都只是为了我们提升数据检索性能的一个媒介,它不会作为最后的数据存储归宿,通常我们会把最终的数据保存到数据库中去。这样的情况下我们的数据就会分别存在于缓存和数据库中,当我们操作两个数据库和缓存的时候就会存在修改了数据库但是没更新缓存数据、又或者更新了缓存数据库的数据又没同步修改,像这种数据库的数据和缓存数据不同步的问题就属于缓存数据一致性问题。
缓存一致性问题主要原因是在于修改数据库的同时还需要把最新的数据更新到缓存中去,但是这个过程可能出现一些问题从而导致缓存数据与数据库的数据不一致,如果A、B请求对同一条数据进行变更,A先把用户年龄修改为18、B请求后把年龄修改为20,这个过程可能是以下两种情况。
A先修改完数据库--B修改数据--B修改缓存--A修改缓存,从下图我们也可以看见这种情况会造成数据库和缓存数据的不一致问题。
读取数据时:先从缓存读取、如果缓存没有则从数据库读取数据,最后把读取到的数据保存到缓存。
只要保证数据变更时缓存一定会删除,那么查询缓存的时候每次都会从数据库加载数据,所以也就避免了修改缓存数据导致的不一致问题。
Read/Write Through策略就是以缓存为主,应用程序不和下游的数据存储打交道,由缓存服务负责和持久化服务打交道,用户对数据的读取和变更都只操作缓存,由缓存负责把变更同步给下游的持久化服务。
Write Back 回写策略,这种策略应用的代表就是CPU缓存,CPU为了减少和主内存打交道的次数(因为CPU和主存打交道需要通过总线,总线是个公共的资源,具备独占性会产生锁,影响性能),比如有现在CPU1和CPU2 按下面的思路操作缓存,CPU1在变更数据的时候只会修改当前CPU1缓存行的数据,然后把此缓存行标记为“脏页”,此时并不会把修改的缓存同步到主内存中去, 下次如果还是CPU1对同一个缓存数据行进行修改时,那么也只需要修改自己内存的数据即可。只有当CP2需要对该缓存行数据读取或者修改时,才会触发CPU1把该缓存行的数据同步到主内存中去。 这种回写的机制其核心就是为了解决写入性能瓶颈问题。通过把多次写入操作合并,达到性能优化的目的。
既然是并发性修改导致的问题,那么我们也可以通过加锁的方式,A在修改数据之前先加锁,等数据库和缓存的数据都修改完毕之后再释放锁,B在A没有释放锁之前是没办法对数据进行操作的,所以也就避免了并发修改数据的可能,从而避免了缓存不一致问题。
为了避免极端情况下导致的缓存不一致问题,我们可以通过设置过期时间来避免不一致的数据一直被错误的使用,我们设置一个比较短的过期时间,只要数据一过期,然后程序就会从数据库同步数据到缓存,这种方式虽然不是直接解决了缓存不一致性的问题,但是可以控制不一致数据存在的时长。
因为缓存本身就是为了保存一些经常使用的活跃数据,对于那些经常不用的冷数据我们也必要长久的保存在内存中,毕竟内存资源是有限的,所以在缓存数据的时候我们也会设置一定的失效时间,当缓存失效了之后,我们的请求发现缓存没有数据就从数据库取读取数据再更新到缓存。
缓存击穿就是在缓存失效后,查询数据的请求不得不进入到数据库进行查询数据的,这个过程中如果并发请求量非常大,大量热点数据过期的时候瞬间会有大量请求进入到数据库,有可能数据库因为承受不了大量的请求而崩溃。
我们可以在热点数据上设置不同的过期时间,避免大量的热点数据集中过期,从而导致大量并发请求同时进入数据库。
也可以把过期时间控制在系统低流量的时间段,比如凌晨三四点,避过流量的高峰期。
当然不设置过期时间,这种可以避免热点数据失效的问题,不过如果数据有频繁的变更,这种方式通常不建议,不设置过期时间也很容易导致缓存保存很多无用的数据。
这种方式就是在查询请求未命中缓存时,查询数据库操作前进行加锁,加锁后后面的请求就会阻塞,避免了大量的请求集中进入到数据库查询数据了。
我们可以不设置过期时间来保证缓存永远不会失效,然后通过后台的线程来定时把最新的数据同步到缓存里去。
缓存穿透通常是说,请求的数据在缓存里面不存在,同时在数据库也不存在。因为系统里面根本不存在该数据,所以请求总是无法命中缓存,每次都会进入数据库,从而会导致数据库承受不来大量的请求导致崩溃的风险。比如我们通过文章的ID来查询文章信息,那么用户就可能通过修改文章的ID来查询一些根本不存在的数据。
明知道搜索的数据根本不存在还不断的请求,那么这种就属于恶意的攻击了,这种恶意的请求通常是有目的性的通过频繁的随机请求系统不存在的数据,造成请求穿透到数据库,从而导致系统风险,这个情况我们可以通过几种方式应对。
这种情况下的解决方案是如果数据不存在的情况下,缓存一个对应key的null值,当请求下次进来的时候发现缓存对应的null就直接返回结果,不再穿透到数据库查询,不过这种方案只能应对一下可预知的一定范围的请求,如果是有意为之的恶意攻击,那么通常都是随机一些参数进行请求的,如果缓存null来应对,那么这样就会造成缓存大量null数据,那么也会导致缓存的风险。
以ID查询文章为例,如果我们要知道数据库是否存在对应的文章,那么最简单的方式就是我们把所有数据库存在的ID都保存到缓存去,这个时候当请求过进入系统,先从这个缓存数据里判断系统是否存在对应的数据ID,如果不存在的话直接返回出去,避免请求进入到数据库层,存在的话再从获取文章的信息。
但是这并不是一个好方案,毕竟内存的资源是宝贵的,数据库数据量太大我们根本不可能把所有数据都放到缓存里去,所以我们需要一种以最节省内存资源的方式来得到相同的效果,这就是布隆过滤器要做的事情。
布隆过滤器的核心思想就是不保存实际数据,而是在内存中创建一个一定长度的位图来用0和1来标记对应的数据是否存在系统。对照下面的图,假如我们创建了了一个长度为14的位图,现在我们需要把数据1和数据2标记到位图里去,首先根据多个哈希函数计算出不同的哈希值,然后用哈希值对位图的长度进行取模,最后得到位图的下标位,然后在对应的下标位上进行标记。
然后在验证数据是否存在系统的时候也是一样,先通过多个哈希函数得到哈希值,然后哈希值与位图的长度进行取模得到多个下标。如果多个下标都被标记成1了,那么说明数据存在于系统,不过只要有一个下标为0那么就说明该数据肯定不存在于系统中,
缓存的确提升了系统的性能,但是使用缓存也会伴随着风险,当我们系统的大量数据都保存在缓存中,大部分请求都已经是走缓存了,那么此时缓存自身的可用性就显得尤为重要了,如果缓存出现了问题这时候大量请求都穿透到数据库,导致数据库也崩溃,从而造成整个系统不可用,这个就是是缓存雪崩。
所以对于这种情况我们需要做两方面的事情来避免发生雪崩情况,第一方面,我们首先着眼于缓存本身的可用性,尽可能保证缓存本身不出问题从而避免发生雪崩问题。另外一方面是着眼于整个系统,如果缓存或者其他服务中间键出现了问题,那么我们还需要一种托底的保护机制来保证这些问题不会蔓延到到整个系统。
保证缓存可用性手段分为两种,一种是尽量避免缓存出现可用性问题,我们可以通过集群部署多个节点尽量减少单个缓存节点的压力。另一种是在缓存出现意外挂掉之后,有冗余备份的节点顶替上来提供服务。
缓存分片主要目的是分流,把缓存的数据拆分到多个节点分别存储,每个节点会存储一部分的缓存数据,通过某种算法把请求分发到对应的节点,减轻了单个缓存节点的访问压力达到分流效果,而且就算一个节点出现故障也不会像一台节点一点造成整个缓存的不可一共,通过分片既能减轻节点的请求压力也避免了整个缓存不可用的风险。缓存分片的算法通常会在哈希算法、一致性哈希哈希算法中选择。
哈希算法:哈希算法就是简单将缓存key的哈希Code和分片的数量进行一次取余的操作,取余的出来的数就是分配到对应编号的节点,哈希算法的优势在于简单,但是在增加或减少节点时会麻烦一点,因为计算值产生了变化,从而从新路由导致需要重数据库重新加载缓存,如果说缓存的节点数量基本预估不会变更了就适合使用哈希算法。
一致性哈希算法:一致性哈希算法优势在于可以动态的扩容缩容,但是会比哈希算法复杂一些,缓存数据也可能会保存的不均匀,如果对于未来节点数量不确定的情况下,适合使用一致性哈希算法。
这种方式是通过冗余多个备用节点,当主节点发生故障时,通过一种算法从备用节点中重新选举出一个主节点来对外提供服务,拿redis来说就是通过sentinel来监控每个redis节点的状态,定时发送心跳机制,当一定时间没有收到主节点的响应心跳时,就认为主节点已经挂了,然后从备选节点里面选举出一个新的主节点来对外提供服务。
限流是通过事先预防的机制来保护系统,我们通过事先对系统可承受的流量进行估算,从而设定一个阈值,超过这个阈值后的请求 不再向下层发送,而是直接拒绝超过阈值的请求。。
限流是事先预防,而熔断更多的是事后保护机制,当某些功能出现延时或者故障后,系统会触发熔断机制,直接拒绝处理请求,从而故障进一步的扩散,引发雪崩效应。
当系统流量太大,超出系统负荷时,避免大负荷量拖垮我们整个系统,这个时候我们就有必要对业务的功能进行取舍了,为了保证核心业务能正常运转,从而暂时关闭一些非核心的业务,根据我们的系统业务等级划分来保护关键业务的正常运行,比如说一个电商网站 关键业务就是选购商品、下单、支付,至于其他的功能资讯、活动、消息、个人信息都可以先关闭,从而保证系统能进行最基础的购物功能。
