Login
网站首页 > 文章中心 > 其它

【Redis场景2】缓存更新策略_双写一致)

作者:小编 更新时间:2023-08-08 12:41:59 浏览量:241人看过

在业务初始阶段,流量很少的情况下,通过直接操作数据是可行的操作,但是随着业务量的增长,用户的访问量也随之增加,在该阶段自然需要使用一些手段(缓存)来减轻数据库的压力;所谓遇事不决,那就加一层.

在当前技术栈中,redis当属缓存的第一梯队了,但是随着缓存的引入,业务架构和问题也随之而来.

缓存好处:

降低后端负载

提高读写效率,降低响应时间

缓存成本:

数据一致性成本

【Redis场景2】缓存更新策略_双写一致)-图1

代码维护成本

运维成本

场景选择

缓存更新策略

内存淘汰:

redis自动进行,当redis内存达到咱们设定的max-memery的时候,会自动触发淘汰机制,淘汰掉一些不重要的数据(可以自己设置策略方式)

宝塔redis配置图:

【Redis场景2】缓存更新策略_双写一致)

超时剔除:当我们给redis设置了过期时间ttl之后,redis会将超时的数据进行删除,方便咱们继续使用缓存

主动更新:我们可以手动调用方法把缓存删掉,通常用于解决缓存和数据库不一致问题

业务场景:

低一致性需求:使用内存淘汰机制.

高一致性需求:主动更新,并以超时剔除作为兜底方案

数据缓存不一致的解决方案

删除缓存还是更新缓存?

更新缓存:每次更新数据库都更新缓存,无效写操作较多

删除缓存(V):更新数据库时让缓存失效,查询时再更新缓存

如何保证缓存与数据库的操作的同时成功或失败?

单体系统,将缓存与数据库操作放在一个事务

分布式系统,利用TCC等分布式事务方案

先操作缓存还是先操作数据库?

先删除缓存,再操作数据库

先操作数据库,再删除缓存(V)

结论:先操作数据库,在操作缓存

【Redis场景2】缓存更新策略_双写一致)

第一种(淘汰):

第二种:也会出现一个时差的问题,但是需要满足以下条件

两个读写线程同时访问

缓存刚好失效(查询未命中)

在线程一写入缓存的时间内,线程二要完成数据库的更新和删除缓存

缓存写入速度很快

写数据库一般会先「加锁」,所以写数据库,通常是要比读数据库的时间更长的

以上择优原则先操作数据后删除缓存的

场景实现

该场景实现流程:以下分析结合部分代码(聚焦于redis的实现);

完整后端代码可在Github中获取:https://github.com/xbhog/hm-dianping

【Redis场景2】缓存更新策略_双写一致)

开发流程:

【查询店铺缓存流程】

从redis中查询店铺信息

命中缓存:返回店铺信息

查询数据库

结果为空:店铺信息不存在

设置店铺缓存

public Result queryById(Long id) {
return Result.ok(shop);
}

然后在后台修改店铺信息的时候,先修改数据库,然后删除缓存;

@Override
@Transactional
public Result updateShopById(Shop shop) {
return Result.ok();
}

这里有一个点,在方法上设置事务,当数据库更新成功,删除缓存(相当于更新缓存);因为这里删除缓存后,下次访问店铺信息的时候,查询数据库会重新建立缓存.

场景问题

虽然上述删除缓存的不管在前还是后面流程异常,都不会影响缓存的使用.但是不是双方一致,而是有所取舍(舍的缓存);

保证数据库和缓存都一致的方式:

重试:****无论是先操作缓存,还是先操作数据库,但凡后者执行失败了,我们就可以发起重试,尽可能地去做「补偿」.

同步重试(不可取)

立即重试很大概率还会失败

重试次数取值

重试会占用当前这个线程资源,阻塞操作.

异步重试(MQ)

canal

异步重试:RocketMQ

在上面店铺信息修改的时候,我们更新了数据库后删除redis缓存,为了避免第二步的执行失败,我们将redis的操作放到消息队列中,由消费者来操作缓存.

引用:

缓存和数据库一致性问题,看这篇就够了

消息队列保证可靠性:写到队列中的消息,成功消费之前不会丢失(重启项目也不担心)

消息队列保证消息成功投递:下游从队列拉取消息,成功消费后才会删除消息,否则还会继续投递消息给消费者(符合我们重试的场景)

至于写队列失败和消息队列的维护成本问题:

写队列失败:操作缓存和写消息队列,「同时失败」的概率其实是很小的

维护成本:我们项目中一般都会用到消息队列,维护成本并没有新增很多

代码实现:

配置pom.xml和application.yaml

2.0.3


org.apache.rocketmq
rocketmq-client
④9.3


org.apache.rocketmq
rocketmq-spring-boot-starter
${rocketmq-spring-boot-starter-version}

rocketmq:
  name-server: xxx.xxx.xxx.174:9876;xxx.xxx.xxx.246:9876
  producer:
group: shopDataGroup

在更新店铺的操作中引入MQ,异步发送信息:

@Override
@Transactional
public Result updateShopById(Shop shop) {
return Result.ok();
}

设置消费者监听器:

package com.hmdp.mq;
/**
 * @author xbhog
 * @describe:
 * @date 2022/12/21
 */
@Slf4j
@Component
@RocketMQMessageListener(topic = TOPIC_SHOP,consumerGroup = "shopRe",
    messageModel = MessageModel.CLUSTERING)
public class RocketMqNessageListener  implements RocketMQListener {
}

}

查看重试结果:

 ====>>开始更新数据库
36:29.174 DEBUG 69636 --- [nio-8081-exec-2] com.hmdp.mapper.ShopMapper.updateById    : ==>  Preparing: UPDATE tb_shop SET name=?, type_id=?, area=?, address=?, avg_price=?, sold=?, comments=?, score=?, open_hours=? WHERE id=?
36:29.192 DEBUG 69636 --- [nio-8081-exec-2] com.hmdp.mapper.ShopMapper.updateById    : ==> Parameters: 102茶餐厅(String), 1(Long), 大关(String), 金华路锦昌文华苑29号(String), 80(Long), 4215(Integer), 3035(Integer), 37(Integer), 10:00-22:00(String), 1(Long)
36:29.301 DEBUG 69636 --- [nio-8081-exec-2] com.hmdp.mapper.ShopMapper.updateById    : <==    Updates: 1
36:29.744  INFO 69636 --- [Thread_shopRe_1] com.hmdp.mq.RocketMqNessageListener      : ========>异步消费开始
36:30.011  INFO 69636 --- [Thread_shopRe_1] com.hmdp.mq.RocketMqNessageListener      : ======>重试次数0
36:30.014  WARN 69636 --- [Thread_shopRe_1] a.r.s.s.DefaultRocketMQListenerContainer : consume message failed. messageExt:.......

java.lang.RuntimeException: 模拟异常抛出
.......

36:42.636  INFO 69636 --- [Thread_shopRe_2] com.hmdp.mq.RocketMqNessageListener      : ========>异步消费开始
36:42.689  INFO 69636 --- [Thread_shopRe_2] com.hmdp.mq.RocketMqNessageListener      : ======>重试次数1
36:42.689  WARN 69636 --- [Thread_shopRe_2] a.r.s.s.DefaultRocketMQListenerContainer : consume message failed. messageExt:.......

java.lang.RuntimeException: 模拟异常抛出
.......

37:12.764  INFO 69636 --- [Thread_shopRe_3] com.hmdp.mq.RocketMqNessageListener      : ========>异步消费开始
37:12.820  INFO 69636 --- [Thread_shopRe_3] com.hmdp.mq.RocketMqNessageListener      : ======>重试次数2
37:12.821  WARN 69636 --- [Thread_shopRe_3] a.r.s.s.DefaultRocketMQListenerContainer : consume message failed. messageExt:MessageExt .......

java.lang.RuntimeException: 模拟异常抛出
.......

38:12.896  INFO 69636 --- [Thread_shopRe_4] com.hmdp.mq.RocketMqNessageListener      : ========>异步消费开始
38:12.960  INFO 69636 --- [Thread_shopRe_4] com.hmdp.mq.RocketMqNessageListener      : ======>重试次数3
38:12.960  WARN 69636 --- [Thread_shopRe_4] a.r.s.s.DefaultRocketMQListenerContainer : consume message failed. messageExt:MessageExt .......

java.lang.RuntimeException: 模拟异常抛出
.......
40:1③045  INFO 69636 --- [Thread_shopRe_5] com.hmdp.mq.RocketMqNessageListener      : ========>异步消费开始
40:1③110  INFO 69636 --- [Thread_shopRe_5] com.hmdp.mq.RocketMqNessageListener      : ======>重试次数4
40:1③110  INFO 69636 --- [Thread_shopRe_5] com.hmdp.mq.RocketMqNessageListener      : 消费失败:cache:shop:1

版权声明:倡导尊重与保护知识产权。未经许可,任何人不得复制、转载、或以其他方式使用本站《原创》内容,违者将追究其法律责任。本站文章内容,部分图片来源于网络,如有侵权,请联系我们修改或者删除处理。

编辑推荐

热门文章