项目场景

当我们项目并发量特别高的时候为了系统性能的提升,我们一般都会将部分数据放入缓存中,加速访问。数据库作为持久化存储。

在数据库中加入缓存中间件Redis

逻辑流程如下:

当请求过来时,先去缓存里边查看有无数据,如果没有,在查询数据库,查出来的数据再放到缓存里边。以后请求要取得数据先去缓存里边找,这样可以减少数据库的压力。

redission 默认启用看门狗机制吗 redis看门狗实现_数据


以下是设置缓存的基本逻辑以及redis三大问题的解决**,缓存击穿**最麻烦需要加锁在后边说。

以下是给缓存放数据的简单演示使用RedisTemplate来操作的,还可以使用jedis

public Map<String, List<Catelog2Vo>> getCatalogJson(){
    /**
     * 1、空结果缓存:防止缓存穿透
     * 2、设置过期时间(加上随机值):解决缓存雪崩
     * 3、加锁:解决缓存击穿
     */

    //加入redis缓存,存的是json序列化后的字符串,如果拿出json字符串,我们还要逆转为所需要的类型;【序列化与反序列化】
    String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
    if(StringUtils.isEmpty(catalogJSON)){
        System.out.println("缓存未命中.............查询数据库........");
        //缓存中没有,查询数据库,并将查到的数据放入缓存中
        Map<String, List<Catelog2Vo>> catalogJsonFromDB =  getDataFromDb(); 
       //由于将查询的数据放入缓存需要时间 为了防止锁不住,因此在刚查询完数据库就开始将数据放入缓存在释放锁。
        String s = JSON.toJSONString(getDataFromDb);
         redisTemplate.opsForValue().set("catalogJSON",s,1, TimeUnit.DAYS);
       return catalogJsonFromDB;
    }
    System.out.println("缓存命中.........直接返回.....");
   //转为指定的对象
    Map<String, List<Catelog2Vo>> res=JSON.parseObject(catalogJSON,new TypeReference<Map<String, List<Catelog2Vo>>>(){});
    return  res;

};

高并发下缓存的三大问题

1、缓存穿透
缓存穿透 是指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数 据库也无此记录,我们没有将这次查询的 null 写入缓存,这将导致这个不存在的数据每次 请求都要到存储层去查询,失去了缓存的意义。
在流量大时,可能 DB 就挂掉了,要是有人利用不存在的 key 频繁攻击我们的应用,这就是 漏洞。
解决: 缓存空结果、并且设置短的过期时间。

2、缓存雪崩
缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失
效,请求全部转发到 DB,DB 瞬时压力过重雪崩。
解决:原有的失效时间基础上增加一个随机值,比如 1-5 分钟随机,这样每一个缓存的过期时间的 重复率就会降低,就很难引发集体失效的事件。

3、缓存击穿
对于一些设置了过期时间的 key,如果这些 key 可能会在某些时间点被超高并发地访问, 是一种非常“热点”的数据。
这个时候,需要考虑一个问题:如果这个 key 在大量请求同时进来前正好失效,那么所 有对这个 key 的数据查询都落到 db,我们称为缓存击穿。
解决: 加锁

最困难的大概就是缓存击穿问题的解决了,需要上锁分为本地锁和分布式锁

本地锁:synchronized 、以及JUC包下的一些锁等。当10w个并发来的时候比如查询某个热点字段,这个热点字段刚好在redis里边到期,为了避免这10w并发同时查库,因此会加一个锁,这些请求会竞争这个锁,当其中一个拿到锁后开始查库执行业务逻辑,并将结果放到redis中去。剩下的请求就会直接通过redis拿取数据。这些本地锁只锁当前进程的,在分布式情况下想要锁住必须使用分布式锁。

public Map<String, List<Catelog2Vo>> getCatalogJsonFromDBWithLocalLock() {
        /**
         * 业务优化,杜绝循环查库
         * 将多次查询数据库变为一次
         */

        /**
         * synchronized (this)同步代码块(this表示当前项目):SpringBoot所有的组件在容器中都是单例的
         * 或者在方法中加synchronized:public synchronized  Map<String, List<Catelog2Vo>> getCatalogJsonFromDB()
         *
         */
        //本地锁:synchronized,JUC(lock)这些本地锁只锁当前进程的,在分部式情况下想要锁住必须使用分布式锁
        //synchronized也是相当于加上锁,一般会将当前实例作为锁,就是这个this。这也是本地锁
        synchronized (this) {
            //在查库时,把数据查出来直接放到redis里边
            //查库逻辑比较繁琐,代码就不放出来了
            return getDataFromDb(); //查库的方法
           
        }

    }

这里的synchronized就是本地锁,this就是当前实例对象。拿到锁后,直接执行里边的查库方法,getDataFromDb()。查库方法逻辑有些复杂,因此就不放出来了,里边就是将数据查出来之后,再放到redis里。

分布式锁

redission 默认启用看门狗机制吗 redis看门狗实现_数据_02


分布式锁:这里就直接看redisson

Redisson解决了锁的自动续期
Redisson有个看门狗机制如果业务超长,每间隔10秒会自动续期,续期后时间30秒
如果设置了过期时间,则过期后不会自动续期
如果手动设置了过期时间,则过期后,不管业务是否跑完,就会释放锁。

redisson使用步骤:
1、导入依赖

<dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.14.1</version>
        </dependency>

2、配置redisson

@Configuration
public class MyRedissonConfig {
    /**
     * config.useSingleServer() 单节点模式
     * @Description 所有对Redisson操作都是通过RedissonClient对象
     * @Param destroyMethod = "shutdown" 销毁方法 服务停止销毁
     * @return org.redisson.api.RedissonClient
     */
    @Bean(destroyMethod = "shutdown")
    RedissonClient redisson() throws IOException {
        //创建配置
        Config config = new Config();
        config.useSingleServer().setAddress("redis://地址:6379");
        //创建实例
        return Redisson.create(config);
    }
}

3、直接使用

redisson默认就是可重入锁

@Autowired
    RedissonClient redisson;
 @ResponseBody
    @GetMapping("/hello")
    public String hello() {
    
    //获取一把锁 只要名字一样就是一把锁
        RLock lock = redisson.getLock("MY-LOCK");
     //lock.lock();
    //加锁 阻塞式等待 默认过期时间30s
    //锁自动续期 如果业务超长 运行期间自动续上新的30s 不用担心业务时间长锁自动过期被删除
    //加锁的业务只要运行完成就不会给当前锁续期 即使不手动解锁 默认30s删除
    
    lock.lock(10, TimeUnit.SECONDS);
         //10s自动解锁 自动解锁时间一定要大于业务执行时间
        //问题 lock.lock(10, TimeUnit.SECONDS); 锁时间到了以后不会自动续期
        //1.如果传了锁的超时时间 就发给reids执行脚本 进行占锁 默认超时时间就是我们指定的时间
        //2.如果没传锁的超时时间 就使用30*1000    LockWatchdogTimeout看门狗的默认时间
             //只要占锁成功 就会启动一个定时任务 (重新给锁设定过期时间 新的过期时间就是看门狗的默认时间)
         //定时任务时间 = internalLockLeaseTime(看门狗时间 )/ 3 10s
        //最佳实战  lock.lock(30, TimeUnit.SECONDS); 省掉了整个续期操作 自动解锁给长一点 手动解锁
        try {
            System.out.println("加锁成功 执行业务" + Thread.currentThread().getId());
            Thread.sleep(30000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();//解锁
            System.out.println("释放锁" + Thread.currentThread().getId());
        }
        return "hello";
    }

缓存一致性的解决问题:

(1) 双写模式:当修改请求发过来后,对数据库修改完毕,然后在查库将新的数据放入缓存覆盖之前的缓存

(2) 失效模式:当修改请求发过来后,改完数据,再将缓存清掉,下一次再请求得时候会看到缓存里没有数据,然后再去数据库重新查一份,再放到缓存中

对于业务的实时性要求不高的:缓存一致性的问题解决:加个缓存过期时间、加上读写锁
  对于业务的实时性高的:那就只能再去查询数据库,在存入缓存。(缺点:就是慢点)
 缓存一致性的完美解决方案:[Canal]()
 现在这个系统的缓存一致性解决:
 1、缓存所有的数据都加上过期时间,数据过期下一次查询就会触发主动更新
 *2、读写数据的时候,加上分布式读写锁。

PS:自己记录一下,锁的详细信息在:

CategoryServiceImpl.java