【分布式实战-高级】分布式缓存与分布式锁实战

一、缓存简介

1、缓存的作用

为了系统性能的提升,我们一般都会将部分数据放入缓存中,加速访问。而 db 承担数据落盘工作。

2、哪些数据适合放入缓存
  • 即时性、数据一致性要求不高的
  • 访问量大且更新频率不高的数据(读多写少)

举例:电商类应用,商品分类,商品列表等适合缓存并加一个失效时间(根据数据更新频率未定),后台如果发布一个商品,买家需要5分钟才能看到新的商品还是可以接受的。
在这里插入图片描述

3、缓存的方式
1)map实现本地缓存

本地缓存:运行在同一个项目里面,同一个副本里面。

本地缓存适用于单体应用,什么问题都没有,还很快。但是如果是分布式系统下,每一个系统都可能会放在多个不同的服务器,那么他们都需要有一个自己的本地缓存。

分布式系统下不应该使用本地缓存!
在这里插入图片描述

2)redis

分布式缓存,只需要一个缓存中间件。常用分布式缓存:redis。
在这里插入图片描述
直接使用redis对数据进行存取,就可以实现缓存操作。

第一次搜索某条数据的线程,去数据库中查找,找到后放入redis中。之后的线程在该数据过期之前,都直接从redis中取出该数据。

需要阿里巴巴的fast-Json工具,存入缓存和取出缓存的数据都为json数据。

在这里插入图片描述
在这里插入图片描述

二、高并发下缓存失效的三大问题

1、缓存穿透

产生条件: 查询一个一定不存在的数据。(利用不存在的数据进行攻击)

状态: 用户发送的请求永远无法命中该数据,且每次请求都去查询该数据。

结果: 数据库被疯狂请求,导致数据库压力增大,最终导致崩溃。

解决方法: null结果/空结果缓存,并加入短暂的过期时间。
在这里插入图片描述

2、缓存雪崩

产生条件: 设置缓存时,过多的key采用了相同的过期时间。

状态: 缓存在某一时刻同时失效。

结果: 请求全部转发到DB,DB瞬时压力过重雪崩。

解决方法: 原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

3、缓存击穿

产生条件: 设置了过期时间的热点key(比如淘宝京东上的新手机秒杀)热点数据。

状态: 无时无刻都有大量数据访问,但是在某一刻有大量的请求发送过来,正好撞上缓存失效。

结果: 所有的请求都落到DB上。

解决方法: 加锁,大量并发只让一个去查,其他人等待,查到以后释放锁,其他人获取到锁,先查缓存,就会有数据,不用去DB。

由于大多数的项目是分布式项目,所以就需要分布式锁才能保证安全,
在这里插入图片描述

三、Redis的Set操作实现分布式锁

分布式锁的缺点就是性能比较慢一点,本地锁/进程锁快一点,但是本地锁没办法锁住分布式的情况,本地锁只能锁定当前服务,每个服务都会获取同时一次数据。

在分布式情况下,想要锁住所有,必须使用分布式锁。

1、分布式锁原理:

在这里插入图片描述

2、redis分布式锁基本实现

redis中文网站

占坑操作

SET key value [EX seconds] [PX milliseconds] [NX|XX]

  • EX:过期时间秒;
  • PX:过期时间毫秒;
  • NX:not exist 仅在不存在的情况下设置密钥。(不存在密钥就放入数据并返回OK,存在就不放入数据并返回nil)。

原理: 大家都来用这个key来保存东西,保存成功的线程就是唯一一个占坑成功的线程,返回OK。其他异步线程保存不成功并返回nil。

代码实现:

//1、占分布式锁,去redis占坑
//setIfAbsent就是set nx
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock","111");
if(lock){
    //加锁成功,设置过期时间!!!
    redisTemplate.expire("lock",30,TimeUnit.MINUTES);
    //查询数据库并赋返回值
    //删除锁
    redisTemplate.delete("lock");
    //返回数据
}else{
    //加锁失败...
    //方案1、等待100ms之后重试
    //方案2、休眠100ms + 自旋,从头到尾再次调用当前方法(推荐)
}

在这里插入图片描述

以上代码的问题:

1、业务代码异常报错或者突然断电,没有执行删锁操作,这就造成了死锁问题;解决方案:设置锁的自动过期时间比如30s。

加锁和设置过期时间必须是原子性的操作!!!预防断电!!!

加锁的同时设置过期时间

在这里插入图片描述

改良版加锁代码:

Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock","1111",300,TimeUnit.SECONDS)

在这里插入图片描述
如果业务代码时间超长30s,分布式锁过期时间为10s,那么,10s后会有2个线程在执行业务代码,30s时,第一个线程会删除第三个线程的分布式锁。

两个问题:

  1. 业务超时了怎么办;
  2. 删锁的时候,导致删除别人的锁。

改良代码:

String uuid = UUID.randomUUID().toString();
//1、占分布式锁,去redis占坑
//setIfAbsent就是set nx
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock",uuid,300,TimeUnit.SECONDS)
if(lock){
    //加锁成功
    //查询数据库并赋返回值
    //查看是否是自己加的锁,如果是自己加的锁,就删除,只能删除自己的锁
    String lockValue = redisTemplate.opsForValue().get("lock");
    if(lockValue.equals(uuid)){
    	redisTemplate.delete("lock");
    }
    //返回数据
}else{
    //加锁失败...
    //方案1、等待100ms之后重试
    //方案2、休眠100ms + 自旋,从头到尾再次调用当前方法(推荐)
}

在这里插入图片描述
在这里插入图片描述

真正的删锁,需要使用脚本

在这里插入图片描述

终极形态:

String uuid = UUID.randomUUID().toString();
// 一、加锁保证原子性
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock",uuid,300,TimeUnit.SECONDS)
if(lock){
    //加锁成功
    //二、查询数据库并赋返回值
    /**
    * 三、解锁保证原子性	
    * 脚本解析
    * 通过get得到KEYS[1](下面的Arrays.asList("lock")参数)对应的值
    * 如果上面得到的值等于ARGV[1](下面的uuid),就执行删除操作,删除KEYS[1],否则返回0;
    */
    String script = "if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end";
    redisTemplate.excute(new DefaultRedisScript<Integer>(script,Integer.class),Arrays.asList("lock"),uuid);
    String uuid = UUID.randomUUID().toString();
    //四、返回数据
}else{
    //加锁失败...
    //方案1、等待100ms之后重试
    //方案2、休眠100ms + 自旋,从头到尾再次调用当前方法(推荐)
}

加锁保证原子性,解锁保证原子性!

业务还没执行完成,锁就过期了,所以此时需要对锁进行自动续期。最简单的方法就是把锁的过期时间设置为较长,比如3000s。

如下:

String uuid = UUID.randomUUID().toString();
// 加锁保证原子性
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock",uuid,3000,TimeUnit.SECONDS)
if(lock){
    //创建一个返回值的引用
    try{
    	//查询数据库并赋返回值
    }finally{
        //解锁
        String script = "if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end";
    	redisTemplate.excute(new DefaultRedisScript<Integer>(script,Integer.class),Arrays.asList("lock"),uuid);
    String uuid = UUID.randomUUID().toString();
    }
    //返回数据
}else{
    //加锁失败...
    //方案1、等待100ms之后重试
    //方案2、休眠100ms + 自旋,从头到尾再次调用当前方法(推荐)
}

四、官方推荐分布式锁!Redisson

1、Redisson简介

Redis官方说明,Redis的Set操作设计模式不推荐用来实现redis分布式锁。

官方原文:

注意: 下面这种设计模式并不推荐用来实现redis分布式锁。应该参考the Redlock algorithm的实现,因为这个方法只是复杂一点,但是却能保证更好的使用效果。

官方推荐实现分布式锁,Redisson:

分布式锁更专业的框架-Redisson
在这里插入图片描述

Redisson官方开源地址

Redisson文档(右侧有中文文档)
在这里插入图片描述

用Redisson完成所有的分布式锁功能

2、整合Redisson作为分布式锁等功能框架
1)导入依赖

在这里插入图片描述

Redisson和之前的jedis与lettuce一样,都是操作redis的客户端。只是Redisson能提供更强大更深入的功能特性。

<!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.13.4</version>
</dependency>
2)配置redisson

官方配置方法

第三方框架整合

使用程序化配置方法:

创建一个新配置类MyRedissonConfig.java

package pers.tangxz.learn.configs;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.io.IOException;

/**
 * @Info:
 * @Author: 唐小尊
 * @Date: 2020/09/25 23:32
 */
@Configuration
public class MyRedissonConfig {
    /**
     * 所有对Redisson的使用都是通过RedissonClient对象
     * @return
     * @throws IOException
     */
    @Bean(destroyMethod="shutdown")
    public RedissonClient redisson() throws IOException {
        //1、创建配置
        Config config = new Config();
        //2、使用单节点模式redis://或者rediss://(安全连接)
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        //3、根据config创建出Redisson实例并返回
        return Redisson.create(config);
    }
}

3、使用Redisson分布式锁

官方中文文档

1)基本使用

1、可重入锁

锁A里面调用了锁B,A和B是同一把锁,那么B将直接使用A的锁。

2、不可重入锁

锁A里面调用了锁B,A和B是同一把锁,那么B就不会执行,直接死锁。

redisson直接获取的锁,默认就是可重入锁。

基于Redis的Redisson分布式可重入锁RLock Java对象实现了java.util.concurrent.locks.Lock接口。同时还提供了异步(Async)反射式(Reactive)RxJava2标准的接口。

@GetMapping(value = "/hello")
public String hello(){
    //1、获取一把锁,只要锁名字相同,就是同一把锁
    RLock myLock = redisson.getLock("my-lock");
    //2、加锁
    myLock.lock();
    try{
    //3、执行业务
    System.out.println("加锁成功,执行业务..."+Thread.currentThread().getId());
    }catch (Exception e){

    }finally {
    //4、解锁,假设解锁代码没有运行,redisson会不会出现死锁。
    //在程序运行过程中,终止程序。
    //即使没有解锁成功,三十秒后也会自动过期
    myLock.unlock();
    System.out.println("释放锁");
    }
    return "123";
}

Redisson中有一个看门狗机制,可以对锁进行续期。默认情况下,看门狗检查所的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。

另外Redisson还通过加锁的方法提供了leaseTime的参数来指定加锁的时间。超过这个时间后锁便自动解开了。

2)看门狗原理,redisson解决死锁
//1、获取一把锁,只要锁名字相同,就是同一把锁
RLock myLock = redisson.getLock("my-lock");
//2、加锁,十秒钟自动解锁。业务超时时,无法自动续期。
//会出现删错锁的情况。
//自动解锁时间一定要大于业务的执行时间。
myLock.lock(10,TimeUnit.SECONDS);

  • 如果我们传递了锁的超时时间,就会发送给redis执行脚本,进行占锁,默认超时时间就是我们指定的时间。
  • 如果我们未指定锁的超时时间,就使用30 * 1000【LockWatchdogTimeout看门狗的默认时间】每隔十秒就会再次自动续期,就会续成30s。
  • internalLockLeaseTime:看门狗时间,30/3=10,10s后续期一次。

看门狗续期源码

private void renewExpiration() {
    RedissonLock.ExpirationEntry ee = (RedissonLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.get(this.getEntryName());
    if (ee != null) {
        Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
            public void run(Timeout timeout) throws Exception {
                RedissonLock.ExpirationEntry ent = (RedissonLock.ExpirationEntry)RedissonLock.EXPIRATION_RENEWAL_MAP.get(RedissonLock.this.getEntryName());
                if (ent != null) {
                    Long threadId = ent.getFirstThreadId();
                    if (threadId != null) {
                        RFuture<Boolean> future = RedissonLock.this.renewExpirationAsync(threadId);
                        future.onComplete((res, e) -> {
                            if (e != null) {
                                RedissonLock.log.error("Can't update lock " + RedissonLock.this.getName() + " expiration", e);
                            } else {
                                if (res) {
                                    //重新调用方法,自旋
                                    RedissonLock.this.renewExpiration();
                                }

                            }
                        });
                    }
                }
            }//定时任务延时为 默认时间/3
        }, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);
        ee.setTimeout(task);
    }
}
3)最佳实战

直接给定三十秒,如果一个业务 超过三十秒,就说明这个业务完蛋了。

所以我们只需要手动加锁,手动解锁就行。

lock.lock(30,TimeUnit.SECONDS);
4)读写锁

读锁不阻塞。

写锁会阻塞读操作。写锁释放之后,才可以读。写锁没释放,读就一直等待。

写锁:排他锁(互斥锁)

读锁:共享锁

RReadWriteLock rwlock = redisson.getReadWriteLock("anyRWLock");
// 最常见的使用方法
rwlock.readLock().lock();
// 或
rwlock.writeLock().lock();

读写锁细节:

在这里插入图片描述

5)分布式闭锁(CountDownLatch)

模拟学校,全部同学来完之后,才允许关门。

全部任务执行完成之后,才允许放行。

RCountDownLatch latch = redisson.getCountDownLatch("door");
//设置闭锁所需要down的次数。
latch.trySetCount(5);
//闭锁在这里等待,直到闭锁值为0
latch.await();

// 在其他线程或其他JVM里
RCountDownLatch door = redisson.getCountDownLatch("anyCountDownLatch");
latch.countDown();//计数减一
6)分布式信号量(可用于分布式限流)

假如当前服务最多承受每秒一万。只要所有服务一上来就先去获取一个信号量,这些信号量一上来就是一万。

模拟车库停车,只有三个车位,来一辆车占用一个车位

普通信号量

//1、注入
@Autowired
RedissonClient redisson;
@Autowired
StringRedisTemplate redisTemplate;

//2、设置初始值代码:
redisTemplate.opsForValue().set("locka", String.valueOf(10));

//3、占车位代码:
RSemaphore park = redisson.getSemaphore("park");
//获取一个信号,无返回值。如果没有信号量,则一直等待。
park.acquire();
//尝试获取一个信号量,如果没有信号量,则返回false,如果获取到了信号量,则返回true。
park.tryAcquire();
//尝试获取一个信号量,最多等待23s
park.tryAcquire(23, TimeUnit.SECONDS);
//尝试23s异步获取一个信号量
park.tryAcquireAsync(23, TimeUnit.SECONDS);

//4、释放车位代码
RSemaphore park = redisson.getSemaphore("park");
Boolean b = park.release();//释放一个信号,释放一个车位。
park.release(10);//释放10个信号量

可过期性信号量

RPermitExpirableSemaphore semaphore = redisson.getPermitExpirableSemaphore("mySemaphore");
String permitId = semaphore.acquire();
// 获取一个信号,有效期只有2秒钟。
String permitId = semaphore.acquire(2, TimeUnit.SECONDS);
// ...
semaphore.release(permitId);

五、分布式下的缓存一致性问题

1、设置锁的细节

锁的粒度,越细越快。

比如product-11-lock、product-12-lock:每一个商品单独加锁

2、缓存里面的数据如何和数据库保持一致

缓存数据一致性

  • 双写模式
  • 失效模式

两种模式在大并发中都容易出现错误。

1)双写模式

改数据库的同时写缓存。很容易出bug。

在一个数据没写完之前,又有一个新数据比之前的线程更快,那么就会产生脏数据。

解决方案:

  • 在一个线程写数据前,直接加锁,直到写入缓存完成。
  • 如果允许暂时的数据不一致,则允许这种情况发生,一般都是一天或者几个小时后自动失效的缓存,且对数据一致性要求不高的数据,才允许这种情况。
2)失效模式(简单实用)
  • 把数据库改完之后直接删掉缓存中的数据。
  • 等待下一次主动查询进行更新。
  • 经常修改的数据,实时性比较高,是否需要加缓存是需要考量的。
  • 如果一个数据,经常修改,经常加锁,导致更慢,这种情况就不加锁
3)最好的设计

对每一个缓存添加一个过期时间,哪怕暂时的脏数据,只要过期时间一到,数据一清空,用户再次请求就可以得到正确的最新的数据。

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

阿里开源的一个中间件,可以模拟,它是数据库的一个从服务器,比如mysql有一个库,里面装了canal,canal就把自己伪装成一个从服务器,从服务器的特点就是mysql里面有什么变化,它就会同步过来,正好利用canal的这个特性,只要业务代码更新了数据库,mysql的这个数据库肯定要开启binlog这个二进制日志,日志里面有每一次的mysql更新的记录,canal就假装成mysql的从库,把mysql的每一个更新都拿过来,相当于只要有更新,canal就知道了。

好处就是编码期间,只需要该数据库就行了,不用管缓存的任何操作,canal在后台,只要相应的数据库改了,相应的缓存也会都被改掉。屏蔽掉了缓存的操作。

缺点就是又加了一个中间件,还要额外开发一些自定义的功能。

好处就是一次开发成型,以后就不管这些事情了。

  • canal在大数据情况下通常用来解决数据异构问题。
    • 比如每一个人去京东,每一个人的首页都不一样,这都是基于爱好的。
    • 可以实时订阅访问记录表的更新和商品信息表的更新。通过这些东西的分析计算,生成另外一张用户商品推荐表。
    • 上大数据相关的系统的时候,可以使用这些方案。

在这里插入图片描述

五、springCache

官方地址

1、简介
  • Spring从3.1开始定义了org.springframework.cache.Cache和org.springframework.cache.CacheManager接口来统一不同的缓存技术;并使用JCache(JSR-107)注解简化开发。
  • Cache接口为缓存的组件规范定义,包含缓存的各种操作集合;Cache接口下Spring提供了各种xxxCache的实现;如RedisCache,EhCacheCache,ConcurrentMapCache等;
  • 每次调用需要缓存功能的方法时,Spring会检查检查指定参数的指定的目标方法是否已经被调用过;如果有,就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户。下次调用直接从缓存中获取。
  • 使用Spring缓存抽象时,我们需要关注以下两点;
    1. 确定方法需要被缓存以及他们的缓存策略。
    2. 从缓存中读取之前缓存存储的数据。

在这里插入图片描述

缓存管理器可以存放无数个各种各样的缓存。

2、整合SpringCache简化缓存的开发
1)引入依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

用redis作为缓存

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2)写配置

  • 自动配置了哪些

    • CacheAutoConfiguration 会导入 RedisCacheConfiguration;
    • 自动配好了缓存管理器 RedisCacheManager
  • 写一个配置文件application.properties

    • spring.cache.type=redis
      
  • 测试使用缓存

  • @Cacheable:触发将数据保存到缓存的操作。
  • @CacheEvict:触发将数据从缓存中删除的操作
  • @CachePut:更新缓存,不影响方法执行。
  • @Caching:组合以上多个缓存操作。
  • @CacheConfig:在类级别共享缓存的相同配置。
  • 开启缓存功能
//在启动类添加注解
@EnableCaching
2)存缓存
  • 只需要注解就能完成缓存操作。

    • 在service的实现层,给某个方法添加 @Cacheable注解。表示当前方法的结果需要缓存,如果缓存中有,则方法不调用。如果缓存中没有,会调用方法,最后将方法的结果存入缓存中。
    • 每一个需要缓存的数据,我们都来指定要放到哪个名字的缓存。【缓存的分区(按照业务类型分)】
    • @Cacheable({“category”,“product”}),给两个分区存储该方法的返回数据。
  • 默认行为

    1. 如果缓存中有,方法不用调用;
    2. key默认自动生成,缓存的名字::SimpleKey{}(自主生成的key值);
    3. 缓存的value值,默认使用JDK序列化机制,将序列化后的数据存到redis。
    4. 默认ttl时间-1;
  • 自定义操作:

    1. 指定生成的缓存使用的key;

      1. key属性指定,接受一个SpEl表达式,可以得到方法名,root名,参数名等。
      2. 官网的SpEl表达式语法
      3. key的值为一个表达式,所以如果需要设置字符串,就需要用单引号。
      4. @Cacheable(value={“category”},key="‘level1Category’")
      5. @Cacheable(value={“category”},key="#root.method.name")
    2. 指定缓存的数据的存活时间;

      1. 在application.properties配置文件中设置存活一个小时

      2. 下面的配置需要在自定义缓存管理器中手动配置才能生效。

      3. spring.cache.redis.time-to-live=3600000 #毫秒为单位
        
    3. 将数据保存为json格式。

      1. CacheAutoConfiguration -> RedisCacheConfiguration -> 自动配置了RedisCacheManager -> 初始化所有的缓存 -> 每个缓存决定使用什么配置 -> 如果redisCacheConfiguration有就用已有的,没有就用默认配置 -> 想改缓存的配置,只需要给容器中放一个 RedisCacheConfiguration 即可 -> 就会应用到当前RedisCacheManager 管理的所有缓存分区中。

      2. 自定义缓存管理器

        1. 新建一个配置类,MyCacheConfig.java

        2. 把主类的@EnableCache剪切到MyCacheConfig.java中

        3. package pers.tangxz.learn.configs;
          
          import com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer;
          import org.springframework.cache.annotation.EnableCaching;
          import org.springframework.context.annotation.Bean;
          import org.springframework.context.annotation.Configuration;
          import org.springframework.data.redis.cache.RedisCacheConfiguration;
          import org.springframework.data.redis.serializer.RedisSerializationContext;
          import org.springframework.data.redis.serializer.StringRedisSerializer;
          
          /**
           * @Info:
           * @Author: 唐小尊
           * @Date: 2020/09/26 13:36
           */
          @Configuration
          @EnableCaching
          public class MyCacheConfig {
              @Bean
              RedisCacheConfiguration redisCacheConfiguration(){
          
                  RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
          
                  config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
                  //GenericFastJsonRedisSerializer兼容各种泛型的Json序列化
                  config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericFastJsonRedisSerializer()));
          
                  return config;
              }
          }
          
        4. 加上在配置文件中配置的ttl过期时间之后

          package pers.tangxz.learn.configs;
          
          import com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer;
          import org.springframework.boot.autoconfigure.cache.CacheProperties;
          import org.springframework.boot.context.properties.EnableConfigurationProperties;
          import org.springframework.cache.annotation.EnableCaching;
          import org.springframework.context.annotation.Bean;
          import org.springframework.context.annotation.Configuration;
          import org.springframework.data.redis.cache.RedisCacheConfiguration;
          import org.springframework.data.redis.serializer.RedisSerializationContext;
          import org.springframework.data.redis.serializer.StringRedisSerializer;
          
          /**
           * @Info:
           * @Author: 唐小尊
           * @Date: 2020/09/26 13:36
           */
          @Configuration
          @EnableCaching
          @EnableConfigurationProperties(CacheProperties.class)
          public class MyCacheConfig {
          
          //    @Autowired
          //    CacheProperties cacheProperties;
          
              @Bean
              RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties){
          
                  RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
          
                  config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
                  //GenericFastJsonRedisSerializer兼容各种泛型的Json序列化
                  config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericFastJsonRedisSerializer()));
          
                  //将配置文件中的所有配置都生效
                  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.isCacheNullValues()){
                      config = config.disableKeyPrefix();
                  }
                  return config;
              }
          }
          
        5. 完整配置文件

          #SpringCache设置为redis缓存
          spring.cache.type=redis
          #加一个过期时间一个小时
          spring.cache.redis.time-to-live=3600000
          #如果指定了前缀就用我们指定的前缀,如果没有就默认使用缓存的名字作为前缀,就让默认的分区名作为前缀。
          #spring.cache.redis.key-prefix=CACHE_
          #是否使用前缀
          spring.cache.redis.use-key-prefix=true
          #是否缓存空值,防止缓存穿透
          spring.cache.redis.cache-null-values=true
          
3)缓存一致性的操作
  • 支持双写模式的注解:@CachePut,可以帮助我们更新缓存。
  • 支持失效模式的注解@CacheEvict从缓存中删除数据
  1. @CacheEvict 的使用(失效模式)
    1. 删除@Cacheable(value={“category”},key="#root.method.name")的缓存,其key为获取数据的方法的名字:getLevel1Categorys;
    2. 在修改的Service方法上面添加注解@CacheEvict(value="category",key="'getLevel1Categorys'")
    3. 删掉该缓存,然后下一次获取数据的时候,再把新数据放入缓存。
  2. @Caching 同时进行多种缓存操作
    1. 修改一个关联两个缓存的数据,就需要同时删除这两个数据,使用@Caching(组合多个操作)
    2. 在修改方法上添加注解
@Caching(evict{
@CacheEvict(value="category",key="'getLevel1Categorys'"),
@CacheEvict(value="category",key="'getCatalogJson'")
 })
  1. 直接清除某个分区的全部缓存

    1. @CacheEvict(value=“category”,allEntries=true)
    2. 存储同一类型的数据,都可以指定成同一个分区。分区名默认就是缓存的前缀。
  2. @CachePut(双写模式)

    1. 如果某个修改方法,返回的是修改后的最新的对象,那么改完之后,直接存入数据。
3、SpringCache的原理与不足
  • 读模式
    • 缓存穿透

      • 查询一个永不存在的数据

      • 解决:缓存空数据

      • #是否缓存空值,防止缓存穿透
        spring.cache.redis.cache-null-values=true
        
    • 缓存击穿

      • 大量并发进来同时查询一个正好过期的数据

      • 解决:加锁

      • @Cacheable(value={"category"},key="#root.method.name",sync=true)
        
    • 缓存雪崩

      • 大量的key同时过期(超大型系统里面可能会存在)

      • 解决:加随机过期时间

      • #加一个过期时间
        spring.cache.redis.time-to-live=1000
        
  • 写模式(缓存与数据库一致)
    • 读写加锁;
    • 引入Canal,感知到MySQL的更新去更新数据库;
    • 读多写多,直接去数据库查询就行。
  • 总结:
    • 常规数据(读多写少,即时性,一致性要求不高的数据),完全可以使用Spring-Cache。写模式(只要缓存的数据有过期时间就足够了)
    • 特殊数据:特殊设计
  • 原理:
    • CacheManager(RedisCacheManager) -> Cache(RedisCache) -> Cache 负责缓存的读写。
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值