缓存
快来分享你的内容吧~
- 多级缓存如何做到实时数据一致性,特别是本地缓存(多节点)问题描述在基于Caffeine(本地缓存)+Redis(分布式缓存)搭建的多级缓存架构中,当数据更新时如何及时的更新缓存,特别是本地缓存在多个服务节点上都存在背景信息我在简历中的聚合支付平台项目中提到了使用多级缓存来实现商户渠道配置数据的高效读取,商户渠道配置在聚合支付平台中是非常重要的数据同时读取很频繁具体疑问具体就是想知道这种情况下如何做到数据库更新时实时的删除缓存特别是本地缓存已有理解我了解...查看全文程序员鱼皮:我觉得你可以先思考 1 个问题,你的这个业务场景真的有必要用多级缓存架构么?具体来说:QPS 有多少?一个聚合支付平台能有多少商户?Redis 真的扛不住么?还是说就是为了再快一点用个本地缓存?只有你明确了【必须要用】本地缓存,才应该是考虑怎么实现一致性,为了一个没必要的目标去付出一致性的代价,这件事情本身就是立不住脚的。为了一袋醋包了一盘饺子,即使你能把题目答出来,可能也不够真实。再回到你的问题

- 2024-07-14·Java后端查看全文大家好,我是程序媛雪儿,今天给大家带来的知识碎片是缓存雪崩。 一、是什么 缓存雪崩:因为大量的key过期,或者redis直接宕机,导致大量的请求到达数据库,带来了巨大的压力。...程序员鱼皮:感谢分享620分享
- 2024-07-11·Java后端
- 2024-02-03·后端这个主要是最近准备八股文缓存专题时做的笔记,主要是对于缓存击穿的解决方案,目前总结起来大概三种 分别是缓存预热、分布式锁以及永不过期,这三种这里做了详细的讲述,看看到过年前能不能把缓存专题给整完,fighting! 什么是缓存击穿? 缓存击穿是指在高并发系统中,一个热点数据缓存的过期或者缓存中的热点数据不存在,导致大量并发请求直接访问数据库,从而给数据库造成巨大压力,甚至宕机的现象。 说具体点就是查看全文鱼友0412:本文非常优质!欢迎鱼友参与知识碎片的贡献,并获取奖励:https://docs.qq.com/form/page/DQkdHTGFLQmJjV3VU#/fill知识碎片汇总:https://yuyuanweb.feishu.cn/wiki/AqfawFUT0iD69kkiRKoci6Nqnqc2210分享
- 2022-12-26
多级缓存如何做到实时数据一致性,特别是本地缓存(多节点)
### 问题描述 在基于Caffeine(本地缓存)+Redis(分布式缓存)搭建的多级缓存架构中,当数据更新时如何及时的更新缓存,特别是本地缓存在**多个服务节点上都存在** ### 背景信息 我在简历中的聚合支付平台项目中提到了使用多级缓存来实现商户渠道配置数据的高效读取,商户渠道配置在聚合支付平台中是非常重要的数据同时读取很频繁 ### 具体疑问 具体就是想知道这种情况下如何做到数据库更新时**实时**的删除缓存特别是**本地缓存** ### 已有理解 我了解到一个比较合适的方案是使用**消息队列来实现**,通过**广播消息**通知所有节点删除本地消息,但是我了解到广播消息是**无法保证每个节点都能成功消费消息**(没有重试机制) 然后还有一个兜底的方案是给本地缓存加**过期时间**,但是这样也无法做到实时更新 我现在就在想这种场景是不是不适合使用多级缓存毕竟缓存的是比较重要的数据,但是这些数据的读取很频繁且修改很少 ### 预期目标 希望能给我提供一些参考方案,是不用多级缓存还是用其他手段保证缓存一致性 ### 相关资料 
优雅使用redis
redis原生工具类使用麻烦那下面一个工具类就能优雅的使用它啦 ### 准备工作 添加pom依赖 ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> ``` ### 添加全局配置 ```java /** * @version v1.0.0 * @belongsProject: qcdfz-gaiguitang-backend * @belongsPackage: com.qcdfz.qcdfzgaiguitangbackend.config * @author: fgh * @description: * @createTime: 2025-03-11 16:23 */ @Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); return template; } } ``` ### 封装工具类 ```java /** * @version v1.0.0 * @belongsProject: qcdfz-gaiguitang-backend * @belongsPackage: com.qcdfz.qcdfzgaiguitangbackend.util * @author: fgh * @description: * @createTime: 2025-03-11 16:44 */ @Configuration public class RedisUtil { @Resource private RedisTemplate<String, Object> redisTemplate; // =============================Common============================ /** * 指定缓存失效时间 * * @param key 键 * @param time 时间(秒) * @return */ public boolean expire(String key, long time) { try { if (time > 0) { redisTemplate.expire(key, time, TimeUnit.SECONDS); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 根据key 获取过期时间 * * @param key 键 不能为null * @return 时间(秒) 返回0代表为永久有效 */ public long getExpire(String key) { return redisTemplate.getExpire(key, TimeUnit.SECONDS); } /** * 判断key是否存在 * * @param key 键 * @return true 存在 false不存在 */ public boolean hasKey(String key) { try { return redisTemplate.hasKey(key); } catch (Exception e) { e.printStackTrace(); return false; } } /** * 删除缓存 * * @param key 可以传一个值 或多个 */ /** * 删除缓存 * * @param keys 可以传一个值 或多个 */ @SuppressWarnings("unchecked") public void del(String... keys) { if (keys != null && keys.length > 0) { if (keys.length == 1) { redisTemplate.delete(keys[0]); } else { redisTemplate.delete(new java.util.HashSet<>(java.util.Arrays.asList(keys))); } } } /** * 根据指定模式获取Redis中的键集合 * * @param pattern 模式字符串,用于匹配键如"*"表示通配符 * @return 匹配模式的键集合 */ public Set<String> getKeysByPattern(String pattern) { return redisTemplate.keys(pattern); } // ============================String============================= /** * 普通缓存获取 * * @param key 键 * @return 值 */ public Object get(String key) { return key == null ? null : redisTemplate.opsForValue().get(key); } /** * 普通缓存放入 * * @param key 键 * @param value 值 * @return true成功 false失败 */ public boolean set(String key, Object value) { try { redisTemplate.opsForValue().set(key, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 普通缓存放入并设置时间 * * @param key 键 * @param value 值 * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期 * @return true成功 false 失败 */ public boolean set(String key, Object value, long time) { try { if (time > 0) { redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS); } else { set(key, value); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 递增 * * @param key 键 * @param delta 要增加几(大于0) * @return */ public long incr(String key, long delta) { if (delta < 0) { throw new RuntimeException("递增因子必须大于0"); } return redisTemplate.opsForValue().increment(key, delta); } /** * 递减 * * @param key 键 * @param delta 要减少几(小于0) * @return */ public long decr(String key, long delta) { if (delta < 0) { throw new RuntimeException("递减因子必须大于0"); } return redisTemplate.opsForValue().decrement(key, delta); } // ================================Map================================= /** * HashGet * * @param key 键 不能为null * @param item 项 不能为null * @return 值 */ public Object hget(String key, String item) { return redisTemplate.opsForHash().get(key, item); } /** * 获取hashKey对应的所有键值 * * @param key 键 * @return 对应的多个键值 */ public Map<Object, Object> hmget(String key) { return redisTemplate.opsForHash().entries(key); } /** * HashSet * * @param key 键 * @param map 对应多个键值 * @return true 成功 false 失败 */ public boolean hmset(String key, Map<String, Object> map) { try { redisTemplate.opsForHash().putAll(key, map); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * HashSet 并设置时间 * * @param key 键 * @param map 对应多个键值 * @param time 时间(秒) * @return true成功 false失败 */ public boolean hmset(String key, Map<String, Object> map, long time) { try { redisTemplate.opsForHash().putAll(key, map); if (time > 0) { expire(key, time); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 向一张hash表中放入数据,如果不存在将创建 * * @param key 键 * @param item 项 * @param value 值 * @return true 成功 false失败 */ public boolean hset(String key, String item, Object value) { try { redisTemplate.opsForHash().put(key, item, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 向一张hash表中放入数据,如果不存在将创建 * * @param key 键 * @param item 项 * @param value 值 * @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间 * @return true 成功 false失败 */ public boolean hset(String key, String item, Object value, long time) { try { redisTemplate.opsForHash().put(key, item, value); if (time > 0) { expire(key, time); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 删除hash表中的值 * * @param key 键 不能为null * @param item 项 可以使多个 不能为null */ public void hdel(String key, Object... item) { redisTemplate.opsForHash().delete(key, item); } /** * 判断hash表中是否有该项的值 * * @param key 键 不能为null * @param item 项 不能为null * @return true 存在 false不存在 */ public boolean hHasKey(String key, String item) { return redisTemplate.opsForHash().hasKey(key, item); } /** * hash递增 如果不存在,就会创建一个 并把新增后的值返回 * * @param key 键 * @param item 项 * @param by 要增加几(大于0) * @return */ public double hincr(String key, String item, double by) { return redisTemplate.opsForHash().increment(key, item, by); } /** * hash递减 * * @param key 键 * @param item 项 * @param by 要减少记(小于0) * @return */ public double hdecr(String key, String item, double by) { return redisTemplate.opsForHash().increment(key, item, -by); } // ============================Set============================= /** * 根据key获取Set中的所有值 * * @param key 键 * @return */ public Set<Object> sGet(String key) { try { return redisTemplate.opsForSet().members(key); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 根据value从一个set中查询,是否存在 * * @param key 键 * @param value 值 * @return true 存在 false不存在 */ public boolean sHasKey(String key, Object value) { try { return redisTemplate.opsForSet().isMember(key, value); } catch (Exception e) { e.printStackTrace(); return false; } } /** * 将数据放入set缓存 * * @param key 键 * @param values 值 可以是多个 * @return 成功个数 */ public long sSet(String key, Object... values) { try { return redisTemplate.opsForSet().add(key, values); } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 将set数据放入缓存 * * @param key 键 * @param time 时间(秒) * @param values 值 可以是多个 * @return 成功个数 */ public long sSetAndTime(String key, long time, Object... values) { try { Long count = redisTemplate.opsForSet().add(key, values); if (time > 0) expire(key, time); return count; } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 获取set缓存的长度 * * @param key 键 * @return */ public long sGetSetSize(String key) { try { return redisTemplate.opsForSet().size(key); } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 移除值为value的 * * @param key 键 * @param values 值 可以是多个 * @return 移除的个数 */ public long setRemove(String key, Object... values) { try { Long count = redisTemplate.opsForSet().remove(key, values); return count; } catch (Exception e) { e.printStackTrace(); return 0; } } // ===============================list================================= /** * 获取list缓存的内容 * * @param key 键 * @param start 开始 * @param end 结束 0 到 -1代表所有值 * @return */ public List<Object> lGet(String key, long start, long end) { try { return redisTemplate.opsForList().range(key, start, end); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 获取list缓存的长度 * * @param key 键 * @return */ public long lGetListSize(String key) { try { return redisTemplate.opsForList().size(key); } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 通过索引 获取list中的值 * * @param key 键 * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推 * @return */ public Object lGetIndex(String key, long index) { try { return redisTemplate.opsForList().index(key, index); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 将list放入缓存 * * @param key 键 * @param value 值 * @return */ public boolean lSet(String key, Object value) { try { redisTemplate.opsForList().rightPush(key, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 将list放入缓存 * * @param key 键 * @param value 值 * @param time 时间(秒) * @return */ public boolean lSet(String key, Object value, long time) { try { redisTemplate.opsForList().rightPush(key, value); if (time > 0) expire(key, time); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 将list放入缓存 * * @param key 键 * @param value 值 * @return */ public boolean lSet(String key, List<Object> value) { try { redisTemplate.opsForList().rightPushAll(key, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 将list放入缓存 * * @param key 键 * @param value 值 * @param time 时间(秒) * @return */ public boolean lSet(String key, List<Object> value, long time) { try { redisTemplate.opsForList().rightPushAll(key, value); if (time > 0) expire(key, time); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 根据索引修改list中的某条数据 * * @param key 键 * @param index 索引 * @param value 值 * @return */ public boolean lUpdateIndex(String key, long index, Object value) { try { redisTemplate.opsForList().set(key, index, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 移除N个值为value * * @param key 键 * @param count 移除多少个 * @param value 值 * @return 移除的个数 */ public long lRemove(String key, long count, Object value) { try { Long remove = redisTemplate.opsForList().remove(key, count, value); return remove; } catch (Exception e) { e.printStackTrace(); return 0; } } } ``` ### 使用方法 ```java @Service public class RoomsServiceImpl extends ServiceImpl<RoomsMapper, Rooms> implements RoomsService { @Resource private RedisUtil redisUtil; /** * 获取聊天室列表 * @return */ @Override public List<ChatRoom> getChatRoomList() { // ..... redisUtil.lGet(key, 0, -1) //..... } } ```
如何优雅地进行缓存预热?
# 一、问题和解决方案 场景:**缓存在同一时间大面积的失效,导致大量的请求都直接落到了数据库上,对数据库造成了巨大的压力。** 缓存服务宕机也会导致缓存雪崩现象,导致所有的请求都落到了数据库上。 为了保证非数据不占用太多内存空间,我们设置了逻辑过期时间。 但是如果热点数据出现过期就会造成缓存穿透、雪崩这些问题。为了解决这些问题,我们需要对已经过期或者将要过期的数据进行缓存重建。 重新导入数据到Redis中,并且重新设置逻辑过期时间。缓存重建需要对一些热点数据进行预热。之前我是这么预热的 ``` @Test void testSaveShop() { //测试id=1,时间10s redisUtils.saveShop2Redis(1L, 120L); } ``` 这样一个一个的写入id号,效率着实有点太慢了,而且万一不记得了,程序就会出现报错了。 针对缓存重建问题,我这里介绍使用缓存预热的两种方法来实现 # 二、缓存预热两种方案 ## 1、定时任务 使用`@EnableScheduling`开启定时任务 ### 1)获取ID列表 我们在mapper上创建方法,获取数据id号 ``` @Select("SELECT id FROM tb_shop") List<Integer> selectAllIds(); ``` ### 2)缓存重建逻辑 这段缓存重建的逻辑: 先根据传入的id号从数据库中获取值, 封装逻辑过期时间和数据,最后将数据进行写入 ``` //缓存重建 public void saveShop2Redis(Long id, Long expireSecond) { String key = CACHE_SHOP_KEY + id; //1、查询店铺数据 Shop shop = shopMapper.selectById(id); //2、封装逻辑过期时间 RedisData redisData = new RedisData(); redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSecond)); redisData.setData(shop); //3、写入redis stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData)); } ``` ### 3)定时任务缓存类 因为不需要特别复杂的逻辑,所以我这里就使用最为简单的spring自带的定时任务。 创建缓存重建定时任务类 - 首先开启定时任务注解 - 调用shopMapper方法,查询所有id号,遍历 - 将遍历的id号传入saveShop2Redis方法中,并设置逻辑过期时间60秒 - 日志打印输出 ``` @Component @Slf4j public class CachePreheatTask { @Resource private RedisUtils redisUtils; @Resource private ShopMapper shopMapper; // 执行缓存预热任务的方法 @Scheduled(cron = "0 19 9 * * ?") public void preheatCache() { // 执行缓存预热逻辑 List<Integer> selectAllIds = shopMapper.selectAllIds(); for (Integer allId : selectAllIds) { //测试id=1,时间10s redisUtils.saveShop2Redis(Long.valueOf(allId), 60L); log.debug("缓存数据预热成功,id为:{}" ,allId); } } } ``` 这里的表达式:cron = "0 19 9 * * ?" 表示的是每天上午9点19执行定时任务 调用 `selectAllIds`方法,把数据库表中的id查询出来,然后进行遍历 再利用for循环,把每次查询出的id传给`saveShop2Redis`方法进行缓存重建 为了方便测试,我设置逻辑过期时间为60秒。 `@Scheduled`注解是Spring框架中用于创建定时任务的注解,它有三个不同类型的参数:`cron`、`fixedDelay`、`fixedRate`,分别用于不同的定时任务需求。 > 1. `cron`参数:用于指定一个cron表达式,可以精确控制任务的执行时间。cron表达式是一个字符串,包含六个或七个空格分隔的时间字段,用于指定秒、分、时、日、月、周几等时间点。例如,`"0 * * * * ?"`表示每分钟执行一次。 > > 2. `fixedDelay`参数:用于指定任务执行结束后到下一次任务开始的间隔时间,单位为毫秒。即任务的执行周期是任务结束后延迟指定的时间后再执行。 > > 例如,`@Scheduled(fixedDelay = 1000)`表示任务执行结束后延迟1秒后再执行。 > > 3. `fixedRate`参数:用于指定任务开始执行后到下一次任务开始的间隔时间,单位为毫秒。即任务的执行周期是任务开始后固定的时间间隔再执行。例如,`@Scheduled(fixedRate = 1000)`表示任务开始后每隔1秒执行一次。 这些参数可以根据实际需求来选择,`cron`表达式适用于需要精确控制执行时间的场景,`fixedDelay`适用于任务执行时间不固定的场景,`fixedRate`适用于固定频率执行任务的场景。 | 表达式 | 意义 | | | -------------------- | -------------: | ---- | | 每隔5秒钟执行一次 | */5 * * * * ? | | | 每隔1分钟执行一次 | 0 * /1 * * * ? | | | 每天1点执行一次 | 0 0 1 * * ? | | 每天23点55分执行一次 | 0 55 23 * * ? | 这样就能达到我想要的效果,可以随便设置定时任务的执行时间,这样就可以提前进行预热了。 ## 2、消息队列 下面再介绍一种可以进行数据预热的方式——消息队列。 思考一下:我们的诉求是什么? 我们需要将数据进行预热,那我们是不是要拿到数据的id号。 拿到了id号呢,我们怎么让程序自动地去执行这段重建逻辑呢? 对的,使用消息队列,把id号传给消息队列,然后在项目启动的时候,让生产者去发送这个消息。消费者拿到消息之后,就会去执行重建的逻辑了。 ```mermaid graph TD 消息shopID号 --> 消息队列 --> 发送消息 --> 消费消息 --> 缓存重建 ``` 这里一些配置什么的我就不写了,都是固定的, ### 1)生产者代码 ``` @Component public class MyMessageProducer { @Resource private RabbitTemplate rabbitTemplate; // 向指定交换机发送消息 public void sendMessage(String exchange, String routingKey, String message) { //将消息发送到指定的交换机和路由键 rabbitTemplate.convertAndSend(exchange,routingKey,message); } } ``` ### 2)消费者代码 ``` @Component @Slf4j public class MyMessageConsumer { @Resource private RedisUtils redisUtils; /** * 接收消息的方法 * * @param message * @param channel * @param deliveryTag */ //使用@SneakyThrows注解简化异常处理 @SneakyThrows //使用该注解指定程序要监听的队列,,并设置消息的确认机制为手动 @RabbitListener(queues = {"hmdp_queue"}, ackMode = "MANUAL") //@Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag 用于从消息头中获取投递标签deliveryTag //在mq中,每条消息都会被分配一个唯一投递标签,用于标识该消息在通道中的投递状态和顺序,使用该注解可以从消息头中获取该投递标签,并将其赋值给deliveryTag参数, public void receiveMessage(String message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) { long shopId = Long.parseLong(message); redisUtils.saveShop2Redis(shopId, 60L); log.info("收到消息,传入的shopId为:{}", message+",缓存数据预热成功!"); //手动确认消息,消息确认标志设置为false,消息才能被确认 channel.basicAck(deliveryTag, false); } } ``` ### 3)创建队列交换机 在程序执行前创建好交换机和对列 ``` public class biInitMain { public static void main(String[] args) { try { ConnectionFactory factory = new ConnectionFactory(); factory.setHost("192.168.88.130"); factory.setPort(5672); Connection connection = factory.newConnection(); Channel channel = connection.createChannel(); String EXCHANGE_NAME = "hmdp_exchange"; channel.exchangeDeclare(EXCHANGE_NAME, "direct"); // 声明一个队列,并且设置持久化消息 String queueName = "hmdp_queue"; String ROUTING_KEY="hmdp_routingKey"; channel.queueDeclare(queueName, true, false, false, null); //队列绑定交换机,routing_key用于指定消息应该发送到哪个队列。 channel.queueBind(queueName, EXCHANGE_NAME, ROUTING_KEY); } catch (Exception e) { e.printStackTrace(); } } } ``` ### 4)发送消息 用于获取热点id并将其发送到消息队列。这个程序应该只执行一次,以确保不会重复发送相同的id。 `@PostConstruct`注解会让项目启动时初始化这段代码,被执行一次。这样消息也就被发送给消费者了,缓存重建的逻辑也就执行成功了 ``` @PostConstruct public void init() { myMessageProducer.sendMessage("hmdp_exchange","hmdp_routingKey",String.valueOf(1L)); // 启动项目时初始化bloomFilter //bloomFilter = bloomFilterManager.create("bloomShopID", expectedInsertions, falseProbability); //List<Integer> list = shopMapper.selectAllIds(); //for (Integer shopId : list) { // bloomFilter.add(BLOOM_FILTER_SHOP + shopId); //} } ``` 看看控制台 <img src="https://pic.code-nav.cn/post_picture/1654343837738381313/5WjgmPfXgfenSjdd.webp" alt="image.png" width="100%" /> 其实代码到这里还是有点小问题的,细心的兄弟应该看出这里的问题了。 对的,之前使用定时任务,获取的是所有数据的id,获取的是所有的数据。 而这次消息队列改造,传入的是一个固定的id值。其实这里应该需要去获取一些热点数据id,再将这些id号传给方法。其中涉及到日志记录、监控数据判断是否是热点数据。 到这里我的缓存预热就结束了,其实就类似于项目的一个小优化的一样
什么都缓存只会害了你
<html> <head></head> <body> <div class="content ql-editor"> <p>现在对于缓存的误解:</p> <ol> <li data-list="bullet"><span class="ql-ui"></span>不管是什么性能问题,通用解决方案都是缓存 + 多线程</li> <li data-list="bullet"><span class="ql-ui"></span>redis并不是唯一的缓存方案</li> <li data-list="bullet"><span class="ql-ui"></span>缓存并不是提高性能的唯一方案,甚至不是最优方案</li> </ol> <p><br></p> <p>你没有尝试其他方案、或者对业务不是特别了解,不要动辄上缓存。</p> <p><br></p> <p><br></p> <p><br></p> <h2 class="ql-line-height-1_6"><strong style="color: rgb(51, 51, 51);">缓存的本质</strong></h2> <p><br></p> <p>在机械硬盘时代,系统一般装在C盘。</p> <blockquote> <div class="blockquote-item"> C盘的读写速度通常比其他盘快,这是因为硬盘分区的创建顺序和物理特性决定的。在早期的机械硬盘时代,硬盘分区的创建是从外圈向内圈进行的,由于角速度固定,外圈的线速度比内圈大,因此外圈的分区(如C盘)读写速度相对较快。此外,由于C盘作为系统盘,通常位于硬盘的最外圈,这使得C盘的读写速度在所有分区中是最快的。因此,将软件安装到C盘可以加快软件的运行速度,因为软件读取速度更快,响应也更迅速。 </div> </blockquote> <p><br></p> <p>总结:</p> <ol> <li data-list="bullet" class="ql-line-height-1_6"><span class="ql-ui"></span><br></li> <li data-list="bullet" class="ql-line-height-1_6"><span class="ql-ui"></span>内圈的读写速度可能比外圈慢2-3倍</li> <li data-list="bullet" class="ql-line-height-1_6"><span class="ql-ui"></span>我们分区的目的之一,就是将最快的外圈分配给系统使用。提高整个系统的表现</li> <li data-list="bullet" class="ql-line-height-1_6"><span class="ql-ui"></span>大量的软件会把配置文件放到C盘中</li> </ol> <p><br></p> <p>这个情况就描述了:</p> <ol> <li data-list="bullet"><span class="ql-ui"></span>系统有多种资源ABC,都能解决一个问题</li> <li data-list="bullet"><span class="ql-ui"></span>而我们应该尽量使用性能最高的资源A,当A无法解决的时候才会使用次一级的B</li> </ol> <p><br></p> <p><br></p> <p>了解了这个逻辑再聊第二个场景:</p> <p>电脑上有三级缓存。</p> <blockquote> <div class="blockquote-item"> 缓存是指可以进行高速数据交换的存储器,它先于内存与CPU交换数据,因此速率很快。L1 Cache(一级缓存)是CPU第一层高速缓存。内置的L1高速缓存的容量和结构对CPU的性能影响较大,不过高速缓冲存储器均由静态RAM组成,结构较复杂,在CPU管芯面积不能太大的情况下,L1级高速缓存的容量不可能做得太大。一般L1缓存的容量通常在32—256KB。L2 Cache(二级缓存)是CPU的第二层高速缓存,分内部和外部两种芯片。内部的芯片二级缓存运行速率与主频相同,而外部的二级缓存则只有主频的一半。L2高速缓存容量也会影响CPU的性能,原则是越大越好,普通台式机CPU的L2缓存一般为128KB到2MB或者更高,笔记本、服务器和工作站上用CPU的L2高速缓存最高可达1MB-3MB。由于高速缓存的速度越高价格也越贵,故有的计算机系统中设置了两级或多级高速缓存。紧靠CPU的一级高速缓存的速度最高,而容量最小,二级高速缓存的容量稍大,速度也稍低 [1]。 </div> </blockquote> <p><span style="color: unset; background-color: unset;"><br></span></p> <p>总结:</p> <ol> <li data-list="bullet"><span class="ql-ui"></span>一级缓存速度最快,但是由于架构的原因做不大</li> <li data-list="bullet"><span class="ql-ui"></span>二级缓存比一级缓存大,但是慢一半,但是仍然很快</li> <li data-list="bullet"><span class="ql-ui"></span>三级缓存比二级缓存大,但是更慢,但是仍然很快</li> <li data-list="bullet"><span class="ql-ui"></span>当查询的时候,先查询一级缓存,然后再查询二级缓存,再查询三级缓存</li> </ol> <p><br></p> <p>对于缓存,有一个基本的逻辑:</p> <ol> <li data-list="bullet"><span class="ql-ui"></span>为什么要用缓存,因为有性能的瓶颈,需要更快的响应速度</li> <li data-list="bullet"><span class="ql-ui"></span>有多个不同的介质:有高速但是容量有限的高速介质,有便宜并且大量的低速介质</li> <li data-list="bullet"><span class="ql-ui"></span>缓存就是在多种不同速度的介质存储多个副本。</li> <li data-list="bullet"><span class="ql-ui"></span>当查询的时候优先查询高速介质,查询不到再去查询低速介质,从而提高整个系统的表现</li> </ol> <p><br></p> <p>对于网络下载,硬盘可以做网络下载的缓存。</p> <p>对手硬盘来说,内存可以做硬盘的缓存。</p> <p><br></p> <p>redis使用的是内存,而传统数据库使用的介质是硬盘。</p> <p>内存的性能高于硬盘</p> <p>那么,可以用redis做传统数据库的缓存。</p> <p><br></p> <h2>缓存跳过计算过程</h2> <p>缓存可以直接跳过计算过程,直接查询到结果。</p> <p><br></p> <p>比如:</p> <ol> <li data-list="ordered"><span class="ql-ui"></span>我需要统计学生的数量,但是全表扫描太慢,我可以直接缓存一个结果</li> <li data-list="ordered"><span class="ql-ui"></span>我需要进行一个复杂计算,我可以直接查询之前是否进行过计算,然后从缓存里面拿到结果</li> </ol> <p><br></p> <p>这个就不需要解释了。</p> <h2><br></h2> <h2>缓存的问题</h2> <h3>妥协</h3> <p>缓存是一个妥协:</p> <ol> <li data-list="bullet"><span class="ql-ui"></span>数据库性能差</li> <li data-list="bullet"><span class="ql-ui"></span>用了缓存承担了80%的业务</li> </ol> <p><br></p> <p>一旦业务穿透了缓存(缓存雪崩 缓存穿透 缓存击穿),那么直接打挂数据库。</p> <p>这种方式很优雅嘛?</p> <p><br></p> <p>如果有其他的解决方案,我个人不是很建议直接缓存。</p> <ol> <li data-list="bullet"><span class="ql-ui"></span>大厂有运维人员,而小厂你不能24小时盯着系统</li> <li data-list="bullet"><span class="ql-ui"></span>缓存的复杂性会导致各种各样的问题,比如数据一致性</li> <li data-list="bullet"><span class="ql-ui"></span>隐患是存在的,“一旦穿透缓存,数据库就挂”</li> </ol> <p><br></p> <p><br></p> <h3>复杂度</h3> <p>和其他方案相比,缓存并不是一层,而是多层。</p> <ol> <li data-list="bullet"><span class="ql-ui"></span>布隆过滤器</li> <li data-list="bullet"><span class="ql-ui"></span>缓存</li> <li data-list="bullet"><span class="ql-ui"></span>数据库</li> </ol> <p><br></p> <p>缓存的解决方案,基本上都是三层。而且要写入多个数据源。</p> <p>作为一个解决方案,缓存是“很重”的一个解决方案。</p> <ol> <li data-list="bullet"><span class="ql-ui"></span>有几层,就需要写几次</li> <li data-list="bullet"><span class="ql-ui"></span>当数据变更的时候,缓存变更和替换难度相对来说比较大</li> <li data-list="bullet"><span class="ql-ui"></span>缓存一致和缓存失效是老大难问题</li> </ol> <p><br></p> <h2>为什么不可行</h2> <h3>直接高速介质?</h3> <p>了解了缓存的本质以后:</p> <ol> <li data-list="bullet"><span class="ql-ui"></span>我确认这个数据是高频的</li> <li data-list="bullet"><span class="ql-ui"></span>这个数据数据量还很小</li> <li data-list="bullet"><span class="ql-ui"></span>为什么不全量存到高速介质?而不是缓存?</li> </ol> <p><br></p> <h3>公司穷</h3> <p>举个例子来描述缓存的局限性:</p> <ol> <li data-list="bullet"><span class="ql-ui"></span>我有个老旧的系统,他内存使用率已经达到了85%,内存天天爆红</li> <li data-list="bullet"><span class="ql-ui"></span>然后我使用了redis对系统进行了优化</li> </ol> <p><br></p> <p>这怎么优化?优化不了一点。</p> <ol> <li data-list="bullet"><span class="ql-ui"></span>缓存的本质就是“热点数据存在高速介质,提高整个系统的表现”</li> <li data-list="bullet"><span class="ql-ui"></span>你公司没有提供有效的高速介质,高速介质已经满了</li> </ol> <p><br></p> <p>总结一句话:老板,加钱买内存。内存是高速介质,然后我可以将“热点数据”存到内存中,提高整个系统的表现。加ssd也可以啊——但是我不用改代码。</p> <p><br></p> <h3>无热点查询</h3> <p>缓存生效的是热点数据。</p> <ol> <li data-list="bullet"><span class="ql-ui"></span>有10%的数据属于高频数据</li> <li data-list="bullet"><span class="ql-ui"></span>有90%的数据低频</li> </ol> <p><br></p> <p>就系统而言,缓存的提升的几个核心要素就是:“缓存的命中率”和“缓存的大小、“高速介质和低速介质的速度差异”。比较核心的是“缓存的命中率”</p> <p><br></p> <p>而命中率不取决于“技术”,而取决于“业务”。</p> <ol> <li data-list="bullet"><span class="ql-ui"></span>哪些业务是用户的高频查询词汇</li> <li data-list="bullet"><span class="ql-ui"></span>哪些内容是高性能消耗的内容,值得去缓存</li> </ol> <p><br></p> <p>不确认业务场景,强行进行缓存,意义不大。</p> <h2><br></h2> <h2>缓存不等于内存 + redis</h2> <p>缓存可以直接跳过计算过程,直接查询到结果。</p> <p><br></p> <p>那么:比如数据库进行count(*)比较慢,所以我直接缓存结果。</p> <p>能不能直接存到数据库里呢?</p> <ol> <li data-list="bullet"><span class="ql-ui"></span>缓存并不是非要缓存在redis</li> <li data-list="bullet"><span class="ql-ui"></span>他存到数据库也是完全没问题的</li> </ol> <p><br></p> <p><br></p> <h2>是否有其他的方案?</h2> <p>数据库查询慢,是否可以买高速SSD?</p> <p>是否可以不用select *,减少内存的消耗</p> <p>是否可以变成分布式存储?</p> <p>是否可以替换为其他解决方案呢?</p> <p>是否有那么高的时效性?</p> <p>是否可以进行最终一致?</p> <p>用户量是否有那么高?</p> <p>缓存的命中率是否高?</p> <p><br></p> <p>作为解决方案而言:</p> <ol> <li data-list="bullet"><span class="ql-ui"></span>缓存的实现难度是比较大的</li> <li data-list="bullet"><span class="ql-ui"></span>缓存有缓存一致性、缓存雪崩、缓存穿透等</li> <li data-list="bullet"><span class="ql-ui"></span>会引入一些复杂的中间件</li> </ol> <p><br></p> <p>你没有尝试其他方案、或者对业务不是特别了解,不要动辄上缓存。</p> <p><br></p> <p><br></p> <p><br></p> <p><br></p> <p><br></p> <p><br></p> <p><br></p> <p><br></p> <p><br></p> <p><br></p> <p><br></p> <p><br></p> <h3><br></h3> <p><br></p> <p><br></p> <p><br></p> <p><br></p> <p><br></p> <p><br></p> <p><br></p> <p><br></p> </div> </body> </html>
大家好,我是程序媛雪儿,今天给大家带来的知识碎片是缓存雪崩。 一、是什么 缓存雪崩:因为大量的key过期,或者redis直接宕机,导致大量的请求到达数据库,带来了巨大的压力。 二、解决方案 1、如果是因为大量的key过期,那就在设置过期的时间TTL加个随机值,可以减少key同时过期的概率 2、如果是redis宕机了,那就在部署redis的时候,使用redis集群,比如哨兵模式、三主三从的集群模式,来提高服务的可用性 3、给缓存业务添加降级限流策略,比如nginx或spring cloud gateway去添加限流策略 4、给业务添加多级缓存 ,比如把Guava或Caffeine作为一级缓存,nginx作为二级缓存 今天雪儿的分享到这里就结束啦,如果大家想详细了解,欢迎关注同名微信公众号和b站视频,里面有带图解的笔记和视频资料,雪儿祝大家学的开心,学有所获,晚安~[月亮]#知识碎片#
Spring的三级缓存
斗胆写一篇技术博客,记录今天学习的三级缓存解决循环依赖问题,写的错误的和不好的地方请大家见谅(((φ(◎ロ◎;)φ))) ## 三级缓存的概念 **1. 第一级缓存(Singleton Objects):** - 存放已经完全初始化好的单例bean.当一个bean被完全创建并初始化后,它会被存储在这里. **2. 第二级缓存(Early Singleton Objects):** - 存放原始的bean对象,即已经实例化但还没有完全初始化的bean.当Spring容器创建一个bean并注入了所有依赖项后,但还未调用afterPropertiesSet()方法或init-method时,bean会被存放在这里. **3. 第三级缓存(Singleton Factories):** - 存放ObjectFactory,即bean创建工厂(用于创建早期对象).当Spring容器创建bean的实例时,但还未进行属性填充(依赖注入)之前,会先存放一个bean工厂在第三级缓存中. ## 循环依赖解决过程详解: 1. Bean A 的创建初始化: - Spring容器开始创建Bean A. - 实例化:容器首先实例化Bean A(new OperationA()),此时它是一个原始的Java对象,不包含任何Spring的代理或增强. - 第三级缓存:这个原始对象被包装为一个ObjectFactory,并存入第三级缓存(SingletonFactories). 2. Bean A 请求 Bean B: - 在Bean A的属性填充过程中,Spring容器发现Bean A需要依赖Bean B. - Bean B 创建:容器开始创建Bean B,同样先实例化Bean B. 3. Bean B 请求 Bean A: - 类似于Bean A,Bean B在属性填充时需要依赖Bean A. - 早期引用:由于Bean A尚未完全初始化,Spring容器从第三级缓存中取出Bean A的ObjectFactory. - 第二级缓存:调用ObjectFactory得到Bean A的早期引用(一个尚未填充属性的Bean A实例),并将其存入第二级缓存(EarlySingletonObjects). 4. Bean B 依赖注入 Bean A: - 将Bean A的早期引用注入到Bean B中. - Bean B 初始化:Bean B完成属性填充,执行afterPropertiesSet或自定义的初始化方法,然后从第二级缓存移动到第一级缓存(SingletonObjects). 5. Bean B 完成创建: - Bean B现在是一个完全初始化的单例对象,可以从第一级缓存中获取. 6. Bean A 依赖注入 Bean B: - 回到Bean A的创建过程,Spring容器从第一级缓存中获取完全初始化的Bean B. 7. Bean A 完成属性填充: - Bean A的所有属性(包括注入的Bean B)都已填充完毕. 8. Bean A 初始化: - 执行Bean A的afterPropertiesSet或自定义的初始化方法. 9. Bean A 完成创建: - Bean A从第二级缓存移动到第一级缓存,现在是一个完全初始化的单例对象. 10. Bean A 和 B 都可用: - 至此,Bean A和Bean B都完全创建并初始化,可以被应用程序使用. - 概念区分: - 实例化对象:指的是Java的new操作创建的对象,此时对象尚未经过Spring容器的任何处理. - 早期对象:指的是已经实例化但还未完全初始化的bean,它存在于第二级缓存中. - 完全初始化对象:指的是已经完成所有属性填充和初始化方法调用的bean,存在于第一级缓存中. - 对象工厂(ObjectFactory):是一个能够创建bean实例的工厂,存在于第三级缓存中,用于在循环依赖中提前暴露bean的早期引用. - 实例工厂(Instance Factory):通常指的是Spring容器内部用于创建bean实例的逻辑,这里可以认为是ObjectFactory的具体实现. 通过以上方式,就通过引入三级缓存,解决了循环依赖的问题,在上述流程执行完之后,ServiceA和ServiceB都被成功的完成了实例化和初始化. 欢迎大佬批评指正!
拜托!十个面试官有九个都要问缓存穿透、缓存击穿、缓存雪崩
<html> <head></head> <body> <div class="content ql-editor"> <p><br></p> <h3><strong style="color: rgb(51, 51, 51);">都是缓存惹的祸</strong></h3> <p><br></p> <p>在项目开发中,我们的数据都是要持久化到磁盘中去,比如使用 MySQL 进行持久化存储,但是呢由于流量越来越大,查询速度也逐渐变慢了起来,于是我们决定!使用缓存!然而使用缓存导致会经常面临三座大山!缓存穿透!!缓存击穿!!缓存雪崩!!,接下来我们将会逐一分析他们导致的原因以及解决方法。</p> <p><br></p> <h3><strong style="color: rgb(51, 51, 51);">缓存雪崩</strong></h3> <p><br></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1612254411675926529/puz8986s.jpeg"></p> <p><br></p> <h4><strong style="color: rgb(51, 51, 51);">介绍</strong></h4> <p><br></p> <p>缓存雪崩是指在某个时间点,大量缓存同时失效或被清空,导致大量请求直接打到数据库或后端系统,造成系统负载激增,甚至引发系统崩溃。这通常是由于缓存中的大量数据在同一时间失效引起的。想象一个在线电商系统,用户访问频繁,需要频繁查询商品信息。假设某一系列的商品突然全部同一时间失效,那就会造成我们的缓存雪崩。或者某一个时刻 Redis 缓存中间件故障了,导致服务全部打到了数据库,也会导致缓存雪崩的情况。</p> <p><br></p> <h4><strong style="color: rgb(51, 51, 51);">解决办法</strong></h4> <p><br></p> <p>缓存键同时失效:</p> <p><br></p> <p>1)过期时间随机化:设置缓存的过期时间,加上一个随机值,避免同一时间大量缓存失效。</p> <p><br></p> <p>2)使用多级缓存:引入多级缓存机制,如本地缓存和分布式缓存相结合,减少单点故障风险。</p> <p><br></p> <p>3)缓存预热:系统启动时提前加载缓存数据,避免大量请求落到冷启动状态下的数据库。</p> <p><br></p> <p>4)加互斥锁:保证同一时间只有一个请求来构建缓存,别的只能等它构建完成再从缓存中读取。</p> <p><br></p> <p>缓存中间件故障:</p> <p><br></p> <p>1)服务熔断:暂停业务的返回数据,直接返回错误。</p> <p><br></p> <p>2)构建集群:构建多个 Redis 集群保证其高可用。</p> <p><br></p> <h3><strong style="color: rgb(51, 51, 51);">缓存击穿</strong></h3> <p><br></p> <p class="ql-align-center"><img src="https://pic.code-nav.cn/planet_post_image/1612254411675926529/58usw3vy.jpeg"></p> <p><br></p> <h4><strong style="color: rgb(51, 51, 51);">介绍</strong></h4> <p><br></p> <p>缓存击穿是指针对某一热点数据的大量请求导致缓存失效,进而直接请求数据库,增加数据库负载。这种情况通常发生在某个特定的缓存 key 在失效时,恰好有大量请求到达。想象一下大家都在抢茅台,但在某一时刻茅台的缓存失效了,大家的请求打到了数据库中,这就是缓存击穿,那他跟缓存雪崩有什么区别呢?缓存雪崩是多个 key 同时,缓存击穿是某个热点 key 崩溃。也可以认为缓存击穿是缓存雪崩的子集。</p> <p><br></p> <h4><strong style="color: rgb(51, 51, 51);">解决办法</strong></h4> <p><br></p> <p>1)加互斥锁:保证同一时间只有一个请求来构建缓存,别的只能等它构建完成再从缓存中读取。跟缓存雪崩相同。</p> <p><br></p> <p>2)永久:不要给热点数据设置过期时间。</p> <p><br></p> <h3><strong style="color: rgb(51, 51, 51);">缓存穿透</strong></h3> <p><br></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1612254411675926529/97o93x70.jpeg"></p> <p><br></p> <h4><strong style="color: rgb(51, 51, 51);">介绍</strong></h4> <p><br></p> <p>缓存穿透是指查询一个不存在的数据,由于缓存和数据库中均不存在,导致每次请求都直接访问数据库,增加数据库负载。攻击者可以通过构造不存在的 key 发起大量请求,造成系统宕机。比如有些小黑子对于我们开发的网站进行恶意的请求,将一些数据库不存在的 ID 疯狂的打在我们的服务器上,如果没做好缓存穿透的预防,还真给你们小黑子得逞了。</p> <p><br></p> <h4><strong style="color: rgb(51, 51, 51);">解决办法</strong></h4> <p><br></p> <p>1)防止非法请求:检查非法请求,封禁其 IP 以及账号,防止它再次为非作歹。</p> <p><br></p> <p>2)缓存空值:允许缓存空值或者可以给他一个默认值。</p> <p><br></p> <p>3)使用布隆过滤器:通过布隆过滤器给数据做一个标记,当发生缓存穿透时也不会请求数据库造成压力,直接通过布隆过滤器和 Redis 判断返回。</p> <p><br></p> </div> </body> </html>
柒夭八股:缓存击穿的解决方案
<html> <head></head> <body> <div class="content ql-editor"> <p><br></p> <p><br></p> <p>这个主要是最近准备八股文缓存专题时做的笔记,主要是对于缓存击穿的解决方案,目前总结起来大概三种</p> <p>分别是缓存预热、分布式锁以及永不过期,这三种这里做了详细的讲述,看看到过年前能不能把缓存专题给整完,fighting!</p> <p><br></p> <p><br></p> <p><br></p> <h2>什么是缓存击穿?</h2> <p><br></p> <p><br></p> <p><br></p> <p>缓存击穿是指在高并发系统中,<strong>一个热点数据缓存的过期或者缓存中的热点数据不存在,导致大量并发请求直接访问数据库</strong>,从而给数据库造成巨大压力,甚至宕机的现象。</p> <p>说具体点就是当某个热点数据在缓存中过期了,如果此时有大量并发请求同时访问这个数据,由于缓存中这个数据不存在,其所有的请求都会直接访问数据库,从而导致数据库压力急剧增加。</p> <p><img src="https://pic.code-nav.cn/planet_post_image/1627889630378479618/fnn9c6dd.jpeg"></p> <p><br></p> <p><br></p> <p><br></p> <h2>如何解决缓存击穿?</h2> <p><br></p> <p><br></p> <p><br></p> <p>一般来说,解决缓存击穿的方法主要有三种:</p> <ol> <li data-list="ordered"><span class="ql-ui"></span>加分布式锁</li> <li data-list="ordered"><span class="ql-ui"></span>热点数据预加载(缓存预热)</li> <li data-list="ordered"><span class="ql-ui"></span>热点数据永不过期</li> </ol> <p><br></p> <p><br></p> <p><br></p> <h3>1. 分布式锁</h3> <p><br></p> <p><br></p> <p><br></p> <p>这里我们来逐步进行分析,分析如何通过互斥锁的方式来解决缓存击穿问题、</p> <p><br></p> <p><br></p> <p><br></p> <p><br></p> <h4><span style="color: rgb(96, 27, 222);">1、查询缓存不存在查询数据库</span></h4> <p><br></p> <p><br></p> <p><br></p> <p><br></p> <p>这个就是第一版的解决方案,也是我们对于普通数据的缓存方案,这个方案首先查询缓存中对应的数据是否存在,如果不存在就请求数据库,数据库中存在就将当前数据写入到缓存中,这个方案的问题也比较明显:</p> <p>如果<strong>缓存中热点数据的缓存过期或者被删除,大量的请求将会打到数据库,</strong>从而导致数据库压力较大。</p> <p>伪代码如下:</p> <div class="ql-code-block-container"> <div class="ql-code-block"> public String queryInfo(String id){ </div> <div class="ql-code-block"> // 1. 从缓存中获取对应的数据 </div> <div class="ql-code-block"> String cacheData = cache.get(id); </div> <div class="ql-code-block"> // 2.判断获取的数据是否为空 </div> <div class="ql-code-block"> if(cacheData == null){ </div> <div class="ql-code-block"> // 查询缓存数据为空,,查询数据库 </div> <div class="ql-code-block"> String dbData = db.queryById(id); </div> <div class="ql-code-block"> if(dbData != null){ </div> <div class="ql-code-block"> cache.set(id,data); </div> <div class="ql-code-block"> cacheData = dbData; </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"> // 返回数据 </div> <div class="ql-code-block"> return cacheData; </div> <div class="ql-code-block"> } </div> </div> <p><br></p> <p><br></p> <p><br></p> <p><br></p> <h4><span style="color: rgb(96, 27, 222);">2、通过分布式互斥锁的方案降低数据库压力</span></h4> <p><br></p> <p><br></p> <p><br></p> <p><br></p> <p>分布式锁的解决方案就是保证<strong>只有一个请求可以访问数据库,其他需要访问数据库的请求等待结果</strong>。这样的话可以避免大量的请求同时访问数据库。</p> <p><img src="https://pic.code-nav.cn/planet_post_image/1627889630378479618/6h3v1qk9.jpeg"></p> <p>这个主要是在原有的基础上进行一定的调整,即限制对于数据库的请求,每次只有一个请求可以访问数据库,这种方案可以有效地避免缓存击穿问题,因为<strong>只有一个线程可以在同一时间内查询数据库</strong>,其他线程则需要等待,这样的话就不会同时穿透到数据库。</p> <p>其主要在原有的基础上进行修改,伪代码如下:</p> <div class="ql-code-block-container"> <div class="ql-code-block"> public String queryInfo(String id){ </div> <div class="ql-code-block"> // 1. 从缓存中获取对应的数据 </div> <div class="ql-code-block"> String cacheData = cache.get(id); </div> <div class="ql-code-block"> // 2.判断获取的数据是否为空 </div> <div class="ql-code-block"> if(cacheData == null){ </div> <div class="ql-code-block"> // 查询缓存数据为空,,查询数据库 </div> <div class="ql-code-block"> Lock lock = getLock(id); </div> <div class="ql-code-block"> lock.lock(); </div> <div class="ql-code-block"> try{ </div> <div class="ql-code-block"> String dbData = db.queryById(id); </div> <div class="ql-code-block"> if(dbData != null){ </div> <div class="ql-code-block"> cache.set(id,data); </div> <div class="ql-code-block"> cacheData = dbData; </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"> }finally{ </div> <div class="ql-code-block"> lock.unlock(); </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"> // 返回数据 </div> <div class="ql-code-block"> return cacheData; </div> <div class="ql-code-block"> } </div> </div> <p>但是这样有一个弊端,那就是<strong>获取分布式锁的请求,都会执行一次查询数据库的请求,</strong>然后将查询结果更新到缓存中去。但是从理论上来说,只有<strong>第一次加载数据库记录的请求是有效</strong>的。</p> <p>这种情况会导致以下两个问题的发生:</p> <ol> <li data-list="ordered"><span class="ql-ui"></span>海量的用户获取锁之后都查询数据库,这样的话<strong>会造成数据库性能的浪费</strong>,因为如此多的请求当中,只有第一次的查询请求是有效的,其他的查询请求都是可以避免的。</li> <li data-list="ordered"><span class="ql-ui"></span>查询数据库过多的话可能会导致<strong>用户响应时间变长,接口吞吐量下降等情况</strong>。</li> </ol> <p>所以,针对以上情况,可以采用双重判定锁的方式来进行优化。</p> <p>双重判定锁的逻辑很简单,如下图所示,在查询数据库前,再次查询一下缓存,查看缓存中是否存在对应的数据,如果存在,则直接返回,不存在才去查询数据库。</p> <p><img src="https://pic.code-nav.cn/planet_post_image/1627889630378479618/n3nsnpv7.jpeg"></p> <p><br></p> <p>根据以上情况,其伪代码如下:</p> <div class="ql-code-block-container"> <div class="ql-code-block"> public String queryInfo(String id){ </div> <div class="ql-code-block"> // 1. 从缓存中获取对应的数据 </div> <div class="ql-code-block"> String cacheData = cache.get(id); </div> <div class="ql-code-block"> // 2.判断获取的数据是否为空 </div> <div class="ql-code-block"> if(cacheData == null){ </div> <div class="ql-code-block"> // 查询缓存数据为空,,查询数据库 </div> <div class="ql-code-block"> Lock lock = getLock(id); </div> <div class="ql-code-block"> lock.lock(); </div> <div class="ql-code-block"> try{ </div> <div class="ql-code-block"> // 进行缓存数据的二次判断 </div> <div class="ql-code-block"> cacheData = cache.get(id); </div> <div class="ql-code-block"> if(cacheData == null){ </div> <div class="ql-code-block"> // 缓存数据为空 </div> <div class="ql-code-block"> String dbData = db.queryById(id); </div> <div class="ql-code-block"> if(dbData != null){ </div> <div class="ql-code-block"> cache.set(id,data); </div> <div class="ql-code-block"> cacheData = dbData; </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"> }finally{ </div> <div class="ql-code-block"> lock.unlock(); </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"> // 返回数据 </div> <div class="ql-code-block"> return cacheData; </div> <div class="ql-code-block"> } </div> </div> <p>总结一下双重判断锁的逻辑</p> <ol> <li data-list="ordered"><span class="ql-ui"></span>获取锁:在查询数据库之前,先尝试获取一个分布式锁,只有一个线程可以成功获取锁,其他线程等待。</li> <li data-list="ordered"><span class="ql-ui"></span>查询数据库:如果双重判断锁确认缓存中不存在对应数据,那么执行查询数据库操作,获取数据。</li> <li data-list="ordered"><span class="ql-ui"></span>将数据写入缓存:获取到数据之后,将数据写入缓存,并且设置一个合适的过期时间,以防止缓存永远不会被更新</li> <li data-list="ordered"><span class="ql-ui"></span>释放锁:最后,释放获取的锁。使得其他线程可以继续使用这个锁。</li> </ol> <p><br></p> <p><br></p> <p><br></p> <h3>2. 热点数据预加载</h3> <p><br></p> <p><br></p> <p><br></p> <p>热点数据预加载,在活动开始之前,<strong>针对已知的热点数据从数据库加载到缓存中</strong>,这样的话可以避免海量的请求第一次访问热点数据的时候需要从数据库读取的流程,降低数据库的压力。</p> <p>可以极大地减少请求的响应时间,有效避免缓存击穿。</p> <p><br></p> <p><br></p> <h3>3. 热点数据永不过期</h3> <p><br></p> <p><br></p> <p>热点数据永不过期,指的是将可以预知的热点数据,在<strong>活动开始之前,将过期时间设置为 -1</strong>,这样的话就不会有缓存击穿的风险了。</p> <p>这个可以搭配热点数据预加载的方案一起实现,等到热点数据的时间结束之后,其数据的访问量降低了,通天阁后台任务的方案针对缓存设置过期时间,从而降低 Redis 存储的压力。</p> <p><br></p> <p><br></p> </div> </body> </html>
Redis在Windows的各版本安装包下载地址
如今的程序开发中,无论是.NET程序开发,.NET Core程序开发,.NET 5程序开发还是Java,Go或者其他开发语言,中间件技术已日趋成熟,在各种应用开发中还经常用到中间件,如:消息中间件,缓存中间件还有应用中间件等等。 鉴于很多开发者的还使用的Windows操作系统,且Redis官网并未提供Windows操作系统版本的Redis服务端下载。所以,本文为大家分享可运行在Windows操作系统中安装Redis的服务端/客户端安装包以压缩包。 地址:https://codedefault.com/p/redis-for-windows-installer-download-links
Redis 命令参考
本文档是 Redis Command Reference 和 Redis Documentation 的中文翻译版, 阅读这个文档可以帮助你了解 Redis 命令的具体使用方法, 并学会如何使用 Redis 的事务、持久化、复制、Sentinel、集群等功能。 地址:http://redisdoc.com/
