分布式缓存

本文探讨了如何利用缓存提高系统性能,包括数据选择、缓存失效问题及解决方案,如缓存穿透、雪崩和击穿,以及Redis和Redisson分布式锁的实现。还重点讲解了缓存一致性问题及其应对策略,最后介绍了SpringCache的使用和配置技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一. 缓存

1. 缓存使用

  • 为了系统性能的提升,我们一般会将部分数据放入缓存中,而db承担数据落盘工作
  • 哪些数据适合放入缓存?
    • 即时性和数据一致性要求不高的
    • 访问量大且更新频率低的数据(读多,写少)

在这里插入图片描述

data = cache.load(id);//从缓存中加载数据
if(data==null){
    data = db.load(id);//从数据库中加载
    cache.put(id,data);//保存到缓存中
}
return data;
  • 注意: 在开发中,凡是放入缓存中的数据,我们应该要指定过期时间,使其可以在 系统即使没有主动更新数据时,也能自动触发数据加载进缓存的流程,避免业务崩溃导致的数据永远不一致的问题

2.高并发下缓存失效问题

(1).缓存穿透
  • 缓存穿透: 指查询一个一定不存在的数据,由于缓存是不命中,将会去查询数据库,但是数据库也无此记录,我们没有将这一次查询的null写入缓存,这将导致整个不存在的数据每次请求都要到存储层查询,失去了缓存的意义,
  • 风险: 利用不存在的数据可绕过缓存进行攻击,数据库瞬时压力增大,最终导致崩溃
  • 解决: null结果缓存,并接入短暂过期时间,也就是说,就说数据库第一次查到null,也要将这个key写入缓存,value可以用一个占位符或者0
(2). 缓存雪崩
  • 缓存雪崩: 指在我们设置缓存时,key采用了相同的过期时间,导致缓存某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩
  • 解决: 原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件,其实就是不是过期时间错开,只要不在某一时刻同时失效即可
(3). 缓存击穿
  • 对于一些设置了过期时间的key,如果这些可以可能会在某些时间点被超高并发地访问,是一种非常"热点"的数据
  • 如果这个可以在大量请求同时进来前刚好失效,那么所有对这个可以的数据查询都落到DB,称之为缓存击穿
  • 解决: 加锁,大量并发只让一个人去查,让其他人等待,查到后释放锁,其他人获得锁,先查缓存,就会有数据,这样就不会到DB

二. 分布式锁

redis实现简单 分布式锁

  • 在这里插入图片描述

  • 加锁时必须加上过期时间,不设置的话,如果在占锁成功后,系统宕机,或者运行异常导致运中断,那么锁永远都不会被释放,并且占锁和设置过期时间也要是一个原子操作 setIfAbsent是原子操作

  • 释放锁的时候要进行判断,是不是自己的锁,有可能自己的锁已经过期了,如果不判断的话,可能删别人的锁

  • 判断锁 和 删除锁 也要是一个原子操作,所以if判断是不可行的

  • 使用lua脚本解锁 原子操作

  • public Map<String, List<Catalog2Vo>> getCatalogJsonFromDbWithRedisLock() {
            //1.占分布式锁,去redis抢锁
            //必须加上过期时间,不设置的话,如果在占锁成功后,系统宕机,或者运行异常导致运中断,那么锁永远都不会被释放,并且占锁和设置过期时间也要是一个原子操作
            String token = UUID.randomUUID().toString();
            Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", token, 300, TimeUnit.SECONDS);//setIfAbsent key不存在才set,此方法是原子操作
            if (lock) {
                //加锁成功
                Map<String, List<Catalog2Vo>> dataFromDb;
                try {
                    dataFromDb = getDataFromDb();
                } finally {
                    //操作完后,要释放锁
                    //释放锁的时候要进行判断,是不是自己的锁,有可能自己的锁已经过期了,如果不判断的话,可能删别人的锁
                    //判断锁 和 删除锁 也要是一个原子操作,所以if判断是不可行的
    //            if (token.equals(stringRedisTemplate.opsForValue().get("lock"))) {
    //                stringRedisTemplate.delete("lock");
    //            }
                    //使用lua脚本解锁 原子操作
                    String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end"; //lua脚本
                    stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Collections.singletonList("lock"), token);
                }
                return dataFromDb;
            } else {
                //加锁失败...重试
                //休眠200ms
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
    
                }
                return getCatalogJsonFromDbWithRedisLock();//自旋锁
            }
        }
    

redisson分布式锁

参照官网

三. 缓存数据一致性

1. 双写模式

  • 更新数据库时,同时更新缓存
  • 问题:并发情况下,更新数据库和更新缓存不是原子操作,可能出现脏数据
  • 解决:1.加锁,2.业务出现脏数据是否能容忍,能容忍的话,就等到缓存自动过期,数据趋向一致,所以说这个问题是暂时性的脏数据问题
  • 读到的最新数据有延迟:最终一致性

2. 失效模式

  • 更新数据库时,同时删除缓存
  • 问题:和双写模式的问题一样,两个操作不熟原子操作

3. 解决方案

  • 无论是双写还是失效模式,都会导致缓存不一致的问题,即多个实例同时更新会出问题,
    1. 如果是用户维度数据(订单数据,用户数据),这种并发几率小,不用考虑这个问题,缓存数据加上过期时间,每隔一段时间触发读的主动更新即可
    2. 如果是菜单,商品介绍等基础数据,也可以使用canal[^canal]订阅binlog的方式
    3. 缓存数据+过期时间也足够解决大部分业务对应缓存的要求
    4. 通过加锁保证并发读写,写写的时候按顺序排好队,读读无所谓,所以适合使用读写锁(业务不关心脏数据,运行临时脏数据可忽略)
  • 总结:
    • 我们能放入缓存的数据本就不应该是实时性,一致性要求超高的,所以缓存数据的时候加上过期时间,保证每天拿到最新的数据即可
    • 我们不应该过度设计,增加系统的复杂性
    • 遇到实时性,一致性要求高的数据,就应该查数据库,即使慢点

四. Spring Cache

1. 简介

  • spring从3.1开始定义了org.springframework.cache.Cacheorg..springframework.cache.CacheManager接口来统一不同的缓存技术,并支持使用JCache(JSR-107)注解来简化开发
  • Cache接口为缓存的组件规范定义,包含缓存的各种操作集合,Cache接口下spring提供了各种xxxCache的实现,如 RedisCache,EhCacheCache,ConcurrentMapCache等.
  • 每次调用需要缓存功能的方法时,spring会检查指定参数的指定目标方法是否已经被调用过,如果有,就直接从缓存中获取方法调用后的结果,如果没有,就调用方法并缓存结果后返回给用户,下次调用直接从缓存中获取

2. 使用

  • 配置

    • spring.cache=redis 使用redis做缓存
    • 主启动了开启缓存功能**@EnableCachinng**
  • 注解

    • @Cacheable : 触发将数据保存到缓存的操作
    • @CacheEvict : 触发将数据从缓存删除的操作
    • @CachePut : 不影响方法执行更新缓存
    • @Caching : 组合以上多个操作
    • @CacheConfig : 在类级别共享缓存的相同配置
  • 使用

    • 每一个需要缓存的数据我们都要来指定要放到哪个名字的缓存(缓存分区)
  • 默认行为:

    • 如果缓存中有需要的数据,将不会调用方法
    • 缓存中key默认生成, 缓存的名字::SimpleKey[] (不指定前缀时,使用缓存的名字作为前缀)
    • 缓存中value的值,默认使用jdk序列化机制
    • 默认TTL:-1
  • 自定义:

    • 指定缓存使用的key,使用注解的key属性,值是一个SpEL,例如使用方法名作为key: key = "#root.method.name"所以如果要指定固定的值,就需要使用key=" ‘key’ "格式

    • 指定缓存TTL,配置文件中spring.cache.redis.time-to-live=3600000,一个小时,单位毫秒

    • 指定数据保存格式

      • @Configuration
        @EnableCaching
        @EnableConfigurationProperties(CacheProperties.class)//加载配置
        public class MyCacheConfig {
        	@Bean
            public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
                RedisCacheConfiguration config =  RedisCacheConfiguration.defaultCacheConfig()
                        .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                        .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));//使用json格式保存数据
                //将配置文件中的值全部拿过来,不然如果使用了自己的RedisCacheConfiguration,那就读不到配置文件中的配置了
                CacheProperties.Redis redisProperties = cacheProperties.getRedis();
                if(redisProperties.getTimeToLive()!=null)
                    config = config.entryTtl(redisProperties.getTimeToLive());
                if (redisProperties.getKeyPrefix()!=null)
                    config = config.prefixKeysWith(redisProperties.getKeyPrefix());
                if (!redisProperties.isCacheNullValues())
                    config = config.disableCachingNullValues();
                if(!redisProperties.isUseKeyPrefix())
                    config = config.disableKeyPrefix();
                return config;
            }
        }
        
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

climb.xu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值