浅谈缓存最终一致性的解决方案
Posted 腾讯技术工程
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅谈缓存最终一致性的解决方案相关的知识,希望对你有一定的参考价值。
作者:clareguo,腾讯 CSIG 后台开发工程师
到底是更新缓存还是删除缓存? 到底是先更新数据库,再删除缓存,还是先删除缓存,再更新数据库?
放入消息队列中,在对应的消费者中获取删除失败的 key
,异步重试删除。这种方法在实现上相对简单,但由于删除失败后的逻辑需要基于业务代码的 trigger 来触发 ,对业务代码具有一定入侵性。时间进行控制,二是将请求暂存在消息队列中顺序消费。在下面这种并发执行场景下,来自线程 1 的写请求更新了数据库,接着来自线程 2 的读请求命中缓存,接着线程 1 才更新缓存,这样便会导致线程 2 读取到的缓存落后于数据库。同理,先更新缓存后更新数据库在写请求和读请求并发时,也会出现类似的问题。面对这种场景,我们也可以加锁解决。
另在,在 Write-Through 模式下,不管是先更新缓存还是先更新数据库,都存在更新缓存或者更新数据库失败的情况,上面提到的重试机制和补偿机制在这里也是奏效的。
2.5 Write-Behind
Write behind 意为异步回写模式,它也具有类似 Read-Through/Write-Through 的访问控制层,不同的是,Write behind 在处理写请求时,只更新缓存而不更新数据库,对于数据库的更新,则是通过批量异步更新的方式进行的,批量写入的时间点可以选在数据库负载较低的时间进行。
在 Write-Behind 模式下,写请求延迟较低,减轻了数据库的压力,具有较好的吞吐性。但数据库和缓存的一致性较弱,比如当更新的数据还未被写入数据库时,直接从数据库中查询数据是落后于缓存的。同时,缓存的负载较大,如果缓存宕机会导致数据丢失,所以需要做好缓存的高可用。显然,Write behind 模式下适合大量写操作的场景,常用于电商秒杀场景中库存的扣减。
2.6 Write-Around
如果一些非核心业务,对一致性的要求较弱,可以选择在 cache aside 读模式下增加一个缓存过期时间,在写请求中仅仅更新数据库,不做任何删除或更新缓存的操作,这样,缓存仅能通过过期时间失效。这种方案实现简单,但缓存中的数据和数据库数据一致性较差,往往会造成用户的体验较差,应慎重选择。
总结在解决缓存一致性的过程中,有多种途径可以保证缓存的最终一致性,应该根据场景来设计合适的方案,读多写少的场景下,可以选择采用“ Cache-Aside 结合消费数据库日志做补偿”的方案,写多的场景下,可以选择采用“ Write-Through 结合分布式锁”的方案 ,写多的极端场景下,可以选择采用“ Write-Behind ” 的方案。
最近其他好文:
最近大火的 NFT 数字藏品是什么?
2021 腾讯技术十大热门文章
服务器开发设计之算法宝典
在下面这种并发执行场景下,来自线程 1 的写请求更新了数据库,接着来自线程 2 的读请求命中缓存,接着线程 1 才更新缓存,这样便会导致线程 2 读取到的缓存落后于数据库。同理,先更新缓存后更新数据库在写请求和读请求并发时,也会出现类似的问题。面对这种场景,我们也可以加锁解决。
另在,在 Write-Through 模式下,不管是先更新缓存还是先更新数据库,都存在更新缓存或者更新数据库失败的情况,上面提到的重试机制和补偿机制在这里也是奏效的。
2.5 Write-Behind
Write behind 意为异步回写模式,它也具有类似 Read-Through/Write-Through 的访问控制层,不同的是,Write behind 在处理写请求时,只更新缓存而不更新数据库,对于数据库的更新,则是通过批量异步更新的方式进行的,批量写入的时间点可以选在数据库负载较低的时间进行。
在 Write-Behind 模式下,写请求延迟较低,减轻了数据库的压力,具有较好的吞吐性。但数据库和缓存的一致性较弱,比如当更新的数据还未被写入数据库时,直接从数据库中查询数据是落后于缓存的。同时,缓存的负载较大,如果缓存宕机会导致数据丢失,所以需要做好缓存的高可用。显然,Write behind 模式下适合大量写操作的场景,常用于电商秒杀场景中库存的扣减。
2.6 Write-Around
如果一些非核心业务,对一致性的要求较弱,可以选择在 cache aside 读模式下增加一个缓存过期时间,在写请求中仅仅更新数据库,不做任何删除或更新缓存的操作,这样,缓存仅能通过过期时间失效。这种方案实现简单,但缓存中的数据和数据库数据一致性较差,往往会造成用户的体验较差,应慎重选择。
总结在解决缓存一致性的过程中,有多种途径可以保证缓存的最终一致性,应该根据场景来设计合适的方案,读多写少的场景下,可以选择采用“ Cache-Aside 结合消费数据库日志做补偿”的方案,写多的场景下,可以选择采用“ Write-Through 结合分布式锁”的方案 ,写多的极端场景下,可以选择采用“ Write-Behind ” 的方案。
最近其他好文:
最近大火的 NFT 数字藏品是什么?
2021 腾讯技术十大热门文章
服务器开发设计之算法宝典
浅谈分布式缓存解决方案
点击关注公众号,实用技术文章及时了解
接口高并发的解决思路:
加缓存
数据静态化
集群
分布式
同步转异步
限流、降级
适合加缓存的场景:
读多写少的数据,不经常需要修改的数据、一致性要求不高(数据只能保持最终一致性,不能保证数据同步一致性)
缓存的概念
1)外存
外存储器是指除计算机内存及CPU缓存以外的存储器,断电后仍然能保存数据。常用的有硬盘、u盘等。
2)内存
内存是计算机组成部分。被称为内存存储器,其作用是暂时存放CPU运算数据,以及与外存之间交互的存储器。只要计算机在运行中,CPU就会把需要运算的数据读到内存中,当运算完成后CPU在将运算结果写到外存中。断电后数据就会清空,常用的有内存条。
3)缓存
缓存就是介于内存和外存之间的存储,加快内存和外存之间的数据交互。软件中间件一般用外部计算机内存当缓存。
4)缓存的指标
1)缓存命中率
从缓存中读取数据的次数与总读取次数的比率,命中率越高越好。
2)缓存更新策略
如果缓存满了,从缓存中清除数据的策略。常见的有:LFU、LRU、FIFO
LRU(Least Recently Used):最久未使用算法,使用时间距离现在最久的那个被移除。
LFU(Least Frequencyly Used):最少使用算法,一定时间使用次数最少的那个被移除。
FIFO(First In First Out):先进先出算法,先放入缓存的先被移除。
Redis缓存在Java中的实现
在java代码中,我们一般对调用的方法进行缓存控制,比如我要查询具体的省市区县数据,findProvidenceAndCityAndArea(String id)
,那么我们应该在调用这个方法之前先从缓存中查找有没有这个数据,如果没有在调用该方法从数据库中查询,然后添加到缓存中去;下次接口调用时将会从缓存直接获取数据。Java中使用分布式缓存Redis。
1)缓存逻辑流程图
查询策略:
修改策略:
2)逻辑流程代码
查询代码:
/**
* 使用缓存,根据省份ID获取省市数据
* @param provinceid
* @return
*/
@Override
public Provinces detail(String provinceid)
Provinces provinces = null;
//1、在redis查询
provinces = (Provinces)redisTemplate.opsForValue().get(provinceid);
//2.如果命中,则返回缓存数据
if (null != provinces)
//redisTemplate.expire(provinceid,20000, TimeUnit.MILLISECONDS);
System.out.println("缓存中得到数据");
return provinces;
//3.如果没有命中,则去数据库中读取数据
provinces = super.detail(provinceid);
if (null != provinces)
//4.将查询出来的数据写入缓存
redisTemplate.opsForValue().set(provinceid,provinces);//set缓存
//redisTemplate.expire(provinceid,20000, TimeUnit.MILLISECONDS);//设置过期
return provinces;
修改策略采用双删策略:
@Override
public Provinces update(Provinces entity) //双删,防止多线程操作之间的时间间隔导致数据不一致
redisTemplate.delete(entity.getProvinceid());//直接删除缓存,预防数据库成功,缓存失败
super.update(entity);
redisTemplate.delete(entity.getProvinceid());//双删
return entity;
@Override
public Provinces add(Provinces entity)
redisTemplate.delete(entity.getProvinceid());
super.add(entity);
redisTemplate.delete(entity.getProvinceid());//双删
return entity;
@Override
public void delete(String provinceid)
redisTemplate.delete(provinceid);
super.delete(provinceid);
redisTemplate.delete(provinceid);//双删
SpringCache的用法
因为缓存代码逻辑有一定的固定性,所以可以利用模板设计模式对其进行封装。
SpringCache并非具体的缓存技术,而是对各种缓存中间件的封装,具体的底层缓存是EhCache还是Redis,只需要简单的配置就可以了。
设计理念
正如Spring框架的其他服务一样,Spring Cache首先是提供了一层抽象,和兴抽象主要体现在这俩个接口上:
org.springframework.cache.Cache
: 代表缓存本身org.springframework.cache.CacheManager
:代表对缓存的处理和管理
抽象的意义在于屏蔽实现细节的差异和提供扩展性,Cache的抽象解耦了缓存的使用和缓存的后端存储,方便后续更换存储。
使用SpringCache
使用示例:
1)声明缓存:在方法上添加@cacheable等注解,表示缓存该方法的结果。
@Cacheable// value指定当前接口,要使用哪一个缓存器 --- 如果该缓存器不存在,则创建一个
public Provinces detail(String provinceid) //一个接口方法,对应一个缓存器
return super.detail(provinceid);
2)开启Spring的cache功能
<cache:annotation-driven />
或者使用注解@EnableCaching
的方式
@Configuration
@EnableCaching
public class CacheConfig
3)配置后端的存储
JDK内存作为缓存
@Bean
public CacheManager cacheManager()
//jdk里,内存管理器
SimpleCacheManager cacheManager = new SimpleCacheManager();
cacheManager.setCaches(Collections.singletonList(new ConcurrentMapCache("province")));
return cacheManager;
RedisCacheManager
@Bean
public CacheManager cacheManager(RedisConnectionFactory connectionFactory)
return RedisCacheManager
.builder(connectionFactory)
.cacheDefaults(
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(20))) //缓存时间绝对过期时间20s
.transactionAware()
.build();
注解风格说明
1)@Cacheable:查询方法
@Cacheable// value指定当前接口,要使用哪一个缓存器 --- 如果该缓存器不存在,则创建一个
public Provinces detail(String provinceid) //一个接口方法,对应一个缓存器
return super.detail(provinceid);
2)@CachePut:新增或者修改方法
//这个AOP,先修改数据库,在删除缓存
@CachePut(key = "#entity.provinceid")
public Provinces update(Provinces entity)
return super.update(entity);
3)@CacheEvict:删除缓存
@CacheEvict
public void delete(String provinceid)
super.delete(provinceid);
缓存穿透解决方案
缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起Id为“-1”的数据或者id为不存在的数据。这是的用户很可能是恶意攻击,攻击会导致数据库压力过大。
解决方案:使用布隆过滤器
缓存击穿解决方案
缓存击穿是指缓存中没有,但是数据库中有的数据(一般是缓存过期),这是由于并发用户特别多,同时读取缓存没读取到数据,又同时去数据库读取数据,一期数据库压力瞬间增大,造成过大压力。
private BloomFilter<String> bf =null; //等效成一个set集合
/**
* 在bean初始化完成后,实例化bloomFilter,并加载数据
*/
@PostConstruct //对象创建后,自动调用本方法
public void init()
List<Provinces> provinces = this.list();
//当成一个SET----- 占内存,比hashset占得小很多
bf = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), provinces.size());// 32个
for (Provinces p : provinces)
bf.put(p.getProvinceid());
@Cacheable(value = "province")
public Provinces detail(String provinceid)
//先判断布隆过滤器中是否存在该值,值存在才允许访问缓存和数据库
if(!bf.mightContain(provinceid))
System.out.println("非法访问--------"+System.currentTimeMillis());
return null;
System.out.println("数据库中得到数据--------"+System.currentTimeMillis());
Provinces provinces = super.detail(provinceid);
return provinces;
缓存雪崩解决方案
缓存血本是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至宕机。和缓存击穿不同的是,缓存击穿指并发
查询同一条数据,缓存雪崩是不同数据同时过期,很多数据都要从数据库查询。
解决方案:加显示锁(可以解决缓存击穿问题)
public Provinces detail(String provinceid)
// 1.从缓存中取数据
Cache.ValueWrapper valueWrapper = cm.getCache(CACHE_NAME).get(provinceid);
if (valueWrapper != null)
logger.info("缓存中得到数据");
return (Provinces) (valueWrapper.get());
//2.加锁排队,阻塞式锁---100个线程走到这里---同一个sql的取同一把锁
doLock(provinceid);//32个省,最多只有32把锁,1000个线程
try//第二个线程进来了
// 一次只有一个线程
//双重校验,不加也没关系,无非是多刷几次库
valueWrapper = cm.getCache(CACHE_NAME).get(provinceid);//第二个线程,能从缓存里拿到值
if (valueWrapper != null)
logger.info("缓存中得到数据");
return (Provinces) (valueWrapper.get());//第二个线程,这里返回
Provinces provinces = super.detail(provinceid);
// 3.从数据库查询的结果不为空,则把数据放入缓存中,方便下次查询
if (null != provinces)
cm.getCache(CACHE_NAME).put(provinceid, provinces);
return provinces;
catch(Exception e)
return null;
finally
//4.解锁
releaseLock(provinceid);
缓存的数据一致性解决策略
数据更新就会引起数据库的更新和缓存更新,就容易出现缓存(Redis)和数据库(Mysql)间的数据一致性问题。无论哪种解决方案都有缺点,没有完美的解决方案。
1)数据实时同步更新:代码同步调用Cache增删改
优点:数据实时同步更新,保持强一致性
缺点:代码耦合,对业务代码有侵入性
2)数据准实时更新:mq异步更新数据
优点:数据同步有较短延迟 ,与业务解耦
缺点:实现复杂,架构较重,中间件的稳定性
3)缓存失效机制:缓存失效
优点:实现简单,无须引入额外逻辑
缺点:有一定延迟,存在缓存击穿/雪崩问题
4)定时任务更新:定时任务,最终数据一致性
优点:不影响正常业务
缺点:不保证一致性,依赖定时任务
感谢阅读,希望对你有所帮助 :)
来源:blog.csdn.net/baidu_38339840/article/
details/119978013
推荐
Java面试题宝典
技术内卷群,一起来学习!!
PS:因为公众号平台更改了推送规则,如果不想错过内容,记得读完点一下“在看”,加个“星标”,这样每次新文章推送才会第一时间出现在你的订阅列表里。点“在看”支持我们吧!
以上是关于浅谈缓存最终一致性的解决方案的主要内容,如果未能解决你的问题,请参考以下文章