为什么要用消息队列更新缓存

为什么要用消息队列更新缓存,第1张

用消息队列更新缓存原因:

1、通过消息队列将更新缓存 *** 作串行处理,可以解决并发更新的问题,比如线程A、线程B在并发更新数据库时,利用数据库事务隔离机制避免脏读。

2、然后把数据标识写入消息队列,接下来消费消息队列,再通过数据标识去读取数据库相应数据并刷新缓存。

无论大型或小型应用,灵活的缓存可以说不仅大大减轻了服务器的压力,而且因为更快速的用户体验而方便了用户。

Android的apk可以说是作为小型应用,其中99%的应用并不是需要实时更新的,而且诟病于蜗牛般的移动网速,与服务器的数据交互是能少则少,这样用户体验才更好,这也是我们有时舍弃webview而采用json传输数据的原因之一。

采用缓存,可以进一步大大缓解数据交互的压力,特此,我们简略列举一下缓存管理的适用环境:

1 提供网络服务的应用

2 数据更新不需要实时更新,但是哪怕是3-5分钟的延迟也是可以采用缓存机制。

3 缓存的过期时间是可以接受的(不会因为缓存带来的好处,导致某些数据因为更新不及时而影响产品的形象等)

带来的好处:

1 服务器的压力大大减小

2 客户端的响应速度大大变快(用户体验)

3 客户端的数据加载出错情况大大较少,大大提高了应有的稳定性(用户体验)

4 一定程度上可以支持离线浏览(或者说为离线浏览提供了技术支持)

一、缓存管理的方法

这里的缓存管理的原理很简:通过时间的设置来判断是否读取缓存还是重新下载。

里面会有一些细节的处理,后面会详细阐述。

基于这个原理,目前鄙人见过的两种比较常见的缓存管理方法是:数据库法和文件法。

二、数据库法缓存管理

这种方法是在下载完数据文件后,把文件的相关信息如url,路经,下载时间,过期时间等存放到数据库,下次下载的时候根据url先从数据库中查询,如果查询到当前时间并未过期,就根据路径读取本地文件,从而实现缓存的效果。

从实现上我们可以看到这种方法可以灵活存放文件的属性,进而提供了很大的扩展性,可以为其它的功能提供一定的支持;

从 *** 作上需要创建数据库,每次查询数据库,如果过期还需要更新数据库,清理缓存的时候还需要删除数据库数据,稍显麻烦,而数据库 *** 作不当又容易出现一系列的性能,ANR问题,实现的时候要谨慎,具体作的话,但也只是增加一个工具类或方法的事情。

还有一个问题,缓存的数据库是存放在/data/data/<package>/databases/目录下,是占用内存空间的,如果缓存累计,容易浪费内存,需要及时清理缓存。

当然这种方法从目前一些应用的实用上看,我没有发现什么问题。

本文我侧重强调第二种方法,第一种方法的实现,就此掠过。

三、文件法缓存管理

这种方法,使用FilelastModified()方法得到文件的最后修改时间,与当前时间判断是否过期,从而实现缓存效果。

实现上只能使用这一个属性,没有为其它的功能提供技术支持的可能。

*** 作上倒是简单,比较时间即可。本身处理也不容易带来其它问题,代价低廉。

四、文件法缓存管理的两点说明

1 不同类型的文件的缓存时间不一样。

笼统的说,不变文件的缓存时间是永久,变化文件的缓存时间是最大忍受不变时间。

说白点,文件内容是不变的,直到清理,我们是可以永远读取缓存的。

配置文件内容是可能更新的,需要设置一个可接受的缓存时间。

2 不同环境下的缓存时间标准不一样。

无网络环境下,我们只能读取缓存文件,哪怕缓存早就过期。

WiFi网络环境下,缓存时间可以设置短一点,一是网速较快,而是流量不要钱。

移动数据流量环境下,缓存时间可以设置长一点,节省流量,就是节省金钱,而且用户体验也更好。

举个例子吧,最近本人在做的一个应用在wifi环境下的缓存时间设置为5分钟,移动数据流量下的缓存时间设置为1小时。

这个时间根据自己的实际情况来设置:数据的更新频率,数据的重要性等。

五、何时刷新

开发者一方面希望尽量读取缓存,用户一方面希望实时刷新,但是成都网站制作响应速度越快越好,流量消耗越少越好,是一个矛盾。

其实何时刷新我也不知道,这里我提供两点建议:

1 数据的最长多长时间不变,对应用无大的影响。

比如,你的数据更新时间为1天,则缓存时间设置为4~8小时比较合适,一天他总会看到更新,如果你觉得你是资讯类应用,再减少,2~4小时,如果你觉得数据比较重要或者比较受欢迎,用户会经常把玩,再减少,1~2小时,依次类推。

为了保险起见,你可能需要毫无理由的再次缩减一下。

2 提供刷新按钮。

上面说的保险起见不一定保险,最保险的方法使在相关界面提供一个刷新按钮,为缓存,为加载失败提供一次重新来过的机会,有了这个刷新按钮,我们的心也才真的放下来。

java灰度现网缓存兼容性问题 ,java缓存一致性问题及解决方案:使用缓存,肯定会存在一致性问题;

读取缓存步骤一般没有什么问题,但是一旦涉及到数据更新:数据库和缓存更新,就容 易出现缓存(Redis)和数据库(MySQL)间的数据一致性问题。

一、讨论一致性问题之前,先来看一个更新的 *** 作顺序问题:

先删除缓存,再更新数据库

问题:同时有一个请求 A 进行更新 *** 作,一个请求 B 进行查询 *** 作。可能出现:

(1)请求 A 进行写 *** 作(key = 1 value = 2),先删除缓存 key = 1 value = 1

(2)请求 B 查询发现缓存不存在

(3)请求 B 去数据库查询得到旧值 key = 1 value = 1

(4)请求 B 将旧值写入缓存 key = 1 value = 1

(5)请求 A 将新值写入数据库 key = 1 value = 2

缓存中数据永远都是脏数据

我们比较推荐 *** 作顺序:

先删除缓存,再更新数据库,再删缓存(双删,第二次删可异步延时)

public void write(String key,Object data){

redisdelKey(key);

dbupdateData(data);

Threadsleep(500);

redisdelKey(key);

}

接下来,看一看缓存同步的一些方案,见下图:

1、 数据实时同步更新

更新数据库同时更新缓存,使用缓存工具类和或编码实现。

优点:数据实时同步更新,保持强一致性

缺点:代码耦合,对业务代码有侵入性

2、 数据准实时更新

准一致性,更新数据库后,异步更新缓存,使用观察者模式/发布订阅/MQ 实现;

优点:数据同步有较短延迟 ,与业务解耦

缺点:实现复杂,架构较重

3 、缓存失效机制

弱一致性,基于缓存本身的失效机制

优点:实现简单,无须引入额外逻辑

缺点:有一定延迟,存在缓存击穿/雪崩问题

4、 定时任务更新

最终一致性,采用任务调度框架,按照一定频率更新

优点:不影响正常业务

优点:不保证一致性,依赖定时任务

二、 缓存击穿、缓存雪崩及解决方案

1 、缓存击穿

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于 并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力

瞬间增大,造成过大压力

2 、缓存雪崩

缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压 力过大甚至 down 机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩

是不同数据都过期了,很多数据都查不到从而查数据库。

解决方案:

1)单体服务:此时需要对数据库的查询 *** 作,加锁 ---- lock (因考虑到是对同一个参数数值上 一把锁,此处 synchronized 机制无法使用) 加锁的标准流程代码如下:

/

解决缓存雪崩和击穿方案

/

@Service("provincesService")

public class ProvincesServiceImpl3 extends ProvincesServiceImpl implements ProvincesService{

private static final Logger logger = LoggerFactorygetLogger(ProvincesServiceImpl3class);

@Resource

private CacheManager cm;//使用注解缓存

private ConcurrentHashMap<String, Lock> locks = new ConcurrentHashMap<>();//线程安全的

private static final String CACHE_NAME = "province";

public Provinces detail(String provinceid) {

// 1从缓存中取数据

CacheValueWrapper valueWrapper = cmgetCache(CACHE_NAME)get(provinceid);

if (valueWrapper != null) {

loggerinfo("缓存中得到数据");

return (Provinces) (valueWrapperget());

}

//2加锁排队,阻塞式锁---100个线程走到这里---同一个sql的取同一把锁

doLock(provinceid);//32个省,最多只有32把锁,1000个线程

try{//第二个线程进来了

// 一次只有一个线程

//双重校验,不加也没关系,无非是多刷几次库

valueWrapper = cmgetCache(CACHE_NAME)get(provinceid);//第二个线程,能从缓存里拿到值?

if (valueWrapper != null) {

loggerinfo("缓存中得到数据");

return (Provinces) (valueWrapperget());//第二个线程,这里返回

}

Provinces provinces = superdetail(provinceid);

// 3从数据库查询的结果不为空,则把数据放入缓存中,方便下次查询

if (null != provinces){

cmgetCache(CACHE_NAME)put(provinceid, provinces);

}

return provinces;

}catch(Exception e){

return null;

}finally{

//4解锁

releaseLock(provinceid);

}

}

private void releaseLock(String userCode) {

ReentrantLock oldLock = (ReentrantLock) locksget(userCode);

//查询锁是否存在和查询当前线程是否保持此锁

if(oldLock !=null && oldLockisHeldByCurrentThread()){

oldLockunlock();

}

}

private void doLock(String lockcode) {//给一个搜索条件,对应一个锁

//provinceid有不同的值,参数多样化

//provinceid相同的,加一个锁,---- 不是同一个key,不能用同一个锁

ReentrantLock newLock = new ReentrantLock();//创建一个锁

Lock oldLock = locksputIfAbsent(lockcode, newLock);//若已存在,则newLock直接丢弃

if(oldLock == null){

newLocklock();//首次加锁,成功取锁,执行

}else{

oldLocklock();//阻塞式等待取锁

}

}

}

2} 集群或微服务场景下:

此场景下的锁换成分布式锁(redis或zk等);同时设置多次取锁功能;

/

解决缓存雪崩和击穿方案

/

@Service("provincesService")

public class ProvincesServiceImpl5 extends ProvincesServiceImpl implements ProvincesService{

private static final Logger logger = LoggerFactorygetLogger(ProvincesServiceImpl3class);

@Resource

private CacheManager cm;//使用注解缓存

@Autowired

private RedisTemplate<String, Object> redisTemplate;

private ConcurrentHashMap<String, Lock> locks = new ConcurrentHashMap<>();//线程安全的

private static final String CACHE_NAME = "province";

public Provinces detail(String provinceid) throws Exception{

// 1从缓存中取数据

CacheValueWrapper valueWrapper = cmgetCache(CACHE_NAME)get(provinceid);

if (valueWrapper != null) {

loggerinfo("缓存中得到数据");

return (Provinces) (valueWrapperget());

}

//2加锁排队,阻塞式锁---100个线程走到这里---同一个sql的取同一把锁

//32个省,最多只有32把锁,1000个线程

boolean flag=false;

flag = RedisUtilsetNX(provinceid, 3000);

//如果首次没有取到锁,可以取10次

if(!flag){

for(int i=0;i<10;i++){

Threadsleep(200);

flag = RedisUtilsetNX(provinceid, 3000);//分布式锁

if(flag){

break;

}

}

}

//如果首次没有取到锁,一直取直到取到为止

/ if(!flag){

for (;;){

Threadsleep(200);

flag = RedisUtilsetNX(provinceid, 3000);//分布式锁

if(flag){

break;

}

}

}/

try{//第二个线程进来了

// 一次只有一个线程

//双重校验,不加也没关系,无非是多刷几次库

valueWrapper = cmgetCache(CACHE_NAME)get(provinceid);//第二个线程,能从缓存里拿到值?

if (valueWrapper != null) {

loggerinfo("缓存中得到数据");

return (Provinces) (valueWrapperget());//第二个线程,这里返回

}

Provinces provinces = superdetail(provinceid);

// 3从数据库查询的结果不为空,则把数据放入缓存中,方便下次查询

if (null != provinces){

cmgetCache(CACHE_NAME)put(provinceid, provinces);

}

return provinces;

}catch(Exception e){

return null;

}finally{

//4解锁

RedisUtilreleaseLock(provinceid);

}

}

}

这里加分布式锁解决缓存一致性问题,也解决缓存击穿的问题;分布式锁参考:分布式锁使用及原理。

今天缓存一致性问题到此结束,下篇我们使用布隆过滤器解决缓存穿透问题,敬请期待。

以上就是关于为什么要用消息队列更新缓存全部的内容,包括:为什么要用消息队列更新缓存、如何Android数据库缓存进行管理、java灰度现网缓存兼容性问题等相关内容解答,如果想了解更多相关内容,可以关注我们,你们的支持是我们更新的动力!

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/sjk/9703183.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-05-01
下一篇 2023-05-01

发表评论

登录后才能评论

评论列表(0条)

保存