谷粒商城十三缓存与分布式锁

为了系统性能的提升,我们一般都会将部分数据放入缓存中,加速访问。而 db 承担数据持久化工作。
哪些数据适合放入缓存?

  • 即时性、数据一致性要求不高的
    即时性:物流状态信息,可能五分钟或者五十分钟看一次,频率看的高,但更新的速度是很慢的,我们对即时性的要求也不高。

    一致性:数据库与缓存不一致,例如分类,修改之后我们可能并不需要立马保持一致,不会产生太大的影响。

  • 访问量大且更新频率不高的数据(读多,写少)
    商品一旦录入之后,我们很少去做一些修改,但是我们经常会查询商品的信息,我们就可以把它放到缓存中。

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

使用redis缓存

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

spring:
  redis:
    host: 192.168.56.10
package com.atlinxi.gulimall.product.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.atlinxi.gulimall.product.service.CategoryBrandRelationService;
import com.atlinxi.gulimall.product.vo.Catelog2Vo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.stream.Collectors;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.atlinxi.common.utils.PageUtils;
import com.atlinxi.common.utils.Query;

import com.atlinxi.gulimall.product.dao.CategoryDao;
import com.atlinxi.gulimall.product.entity.CategoryEntity;
import com.atlinxi.gulimall.product.service.CategoryService;
import org.springframework.util.StringUtils;


@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {

    @Autowired
    private CategoryBrandRelationService categoryBrandRelationService;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;


    //    之前是使用@Autowired注入
//    现在继承了 mybatisplus的ServiceImpl,泛型是CategoryDao,
//    我们可以直接使用baseMapper,它代表的也就是CategoryDao
//    @Autowired
//    private CategoryDao categoryDao;


    /**
     * 级联更新所有相关数据
     * @param category
     */
    @Override
    public void updateCascade(CategoryEntity category) {
        this.updateById(category);

        if (!StringUtils.isEmpty(category.getName())){
            // 同步更新其他关联表中的数据
            categoryBrandRelationService.updateCategory(category.getCatId(),category.getName());

            // todo 更新其他关联表
        }
    }

    @Override
    public List<CategoryEntity> getLevel1Categorys() {
        List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
        return categoryEntities;
    }

    @Override
    public Map<String, List<Catelog2Vo>> getCatalogJson() {

        // 给缓存中放json字符串,拿出的json字符串,还要逆转为java对象 【序列化】

        // 1. 加入缓存逻辑,缓存中传的所有对象都是json字符串
        // json跨语言,跨平台兼容,例如php也可以从redis中获取使用
        // 如果是java序列化的话,那就不行了
        String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");

        if (StringUtils.isEmpty(catalogJSON)){
            Map<String, List<Catelog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb();

            String s = JSON.toJSONString(catalogJsonFromDb);
            stringRedisTemplate.opsForValue().set("catalogJSON",s);
            return catalogJsonFromDb;
        }

        // 转为我们指定的类型
        Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON,new TypeReference<Map<String, List<Catelog2Vo>>>(){});

        return result;
    }



    // 从数据库查询并封装分类数据
    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDb() {

        // 性能优化,将数据库的多次查询变为一次
        List<CategoryEntity> selectList = this.baseMapper.selectList(null);

        // 1. 查出所有一级分类数据
        List<CategoryEntity> level1Categorys = getParentCid(selectList,0L);

        // 2. 封装数据
        // 这儿的key和value都是level1Categorys
        Map<String, List<Catelog2Vo>> parentCid = level1Categorys.stream().collect(Collectors.toMap(key -> key.getCatId().toString(), value -> {
            // 1. 每一个的一级分类,查询这个一级分类的二级分类
            List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>()
                    .eq("parent_cid", value.getCatId()));

            // 2. 封装上面的结果
            List<Catelog2Vo> catelog2Vos = null;
            if (categoryEntities != null) {
                catelog2Vos = categoryEntities.stream().map(level2 -> {
                    Catelog2Vo catelog2Vo = new Catelog2Vo(value.getCatId().toString(), null, level2.getCatId().toString(), level2.getName());

                    // 1. 找当前二级分类的三级分类封装成vo
                    List<CategoryEntity> level3Catelog = baseMapper.selectList(new QueryWrapper<CategoryEntity>()
                            .eq("parent_cid", level2.getCatId()));

                    if (level3Catelog != null){
                        // 2. 封装成指定格式
                        List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(level3 -> {
                            Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(level2.getCatId().toString(),level3.getCatId().toString(),level3.getName());
                            return catelog3Vo;
                        }).collect(Collectors.toList());
                        catelog2Vo.setCatalog3List(collect);

                    }

                    return catelog2Vo;
                }).collect(Collectors.toList());
            }

            return catelog2Vos;

        }));

        return parentCid;
    }

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<CategoryEntity> page = this.page(
                new Query<CategoryEntity>().getPage(params),
                new QueryWrapper<CategoryEntity>()
        );

        return new PageUtils(page);
    }


    @Override
    public List<CategoryEntity> listWithTree() {

        // 1. 查出所有分类
        List<CategoryEntity> entities = baseMapper.selectList(null);

        List<CategoryEntity> entities2 = entities.stream()
                .sorted((categoryEntity1,categoryEntity2)->(categoryEntity1.getCatLevel() - categoryEntity2.getCatLevel()))
                .collect(Collectors.toList());

        // 2. 组装成父子的树形结构

        // 2.1 找到所有的一级分类
        List<CategoryEntity> level1Menu = entities.stream()
                // 过滤
                // 这里用的是 == ,0应该是int,categoryEntity.getParentCid() 是Long,==就是true,equals就是false,因为equals底层会先比较类型
                // getChildrens 中 categoryEntity.getParentCid().equals(root.getCatId()) 这俩都是Long类型,==就是false,equals就是true
                // equals是true很好理解,因为类型一样,值一样,==是false是因为,不是在常量池中创建的对象就是在堆中创建的对象,所以地址值肯定是不一样的
                .filter(categoryEntity -> categoryEntity.getParentCid() == 0
                // 上面的过滤会得到一个list,
                // menu只是一个变量名,指list返回的每一个元素
                ).map(menu -> {
                    menu.setChildren(getChildrens(menu, entities2));
                    return menu;
                }).sorted((menu1, menu2) -> (menu1.getSort()==null?0:menu1.getSort()) - (menu2.getSort()==null?0:menu2.getSort()))
                // 把结果收集成一个list
                .collect(Collectors.toList());

        return level1Menu;
    }



    /**
     * 递归查找所有菜单的子菜单
     *
     * @param root 当前分类
     * @param all  所有分类
     * @return
     */
    private List<CategoryEntity> getChildrens(CategoryEntity root, List<CategoryEntity> all) {

        List<CategoryEntity> children = all.stream()
                .filter(categoryEntity -> categoryEntity.getParentCid().equals(root.getCatId()))
                .map(categoryEntity -> {categoryEntity.setChildren(getChildrens(categoryEntity,all)); return categoryEntity;})
                .sorted((menu1,menu2) -> (menu1.getSort()==null?0:menu1.getSort()) - (menu2.getSort()==null?0:menu2.getSort()))
                .collect(Collectors.toList());
        return children;
    }


    @Override
    public void removeMenuByIds(List<Long> asList) {
        //todo 1.检查当前删除的菜单,是否被别的地方引用

        // 逻辑删除
        baseMapper.deleteBatchIds(asList);
    }


    /**
     * 根据三级分类查找其路径
     * @return
     */
    @Override
    public Long[] findCatelogPath(Long catelogId) {

        List<Long> paths = new ArrayList<>();

        List<Long> parentPath = findParentPath(catelogId, paths);

        Collections.reverse(parentPath);


        return paths.toArray(new Long[parentPath.size()]);
    }


    private List<Long> findParentPath(Long catelogId,List<Long> paths){

        paths.add(catelogId);
        // mabatis-plus return this.getBaseMapper().selectById(id);
        CategoryEntity byId = this.getById(catelogId);

        if (byId.getParentCid()!=0){
            findParentPath(byId.getParentCid(),paths);
        }

        return paths;
    }


    private List<CategoryEntity> getParentCid(List<CategoryEntity> selectList,Long parentCid){

        return selectList.stream().filter(item-> item.getParentCid().equals(parentCid)).collect(Collectors.toList());
    }
}

本地锁

为了解决缓存穿透的问题,我们需要加锁

synchronized (this)本地锁使用这个,this代表实例,springboot中的对象是单例的,所以在单节点的情况下是没问题的,但是在分布式下,spring的单例针对的是一个服务,分布式是多服务,那么就是有几个服务就会去查几次数据库,这就失去了锁的意义。

#

锁的时序问题

我们需要把需要锁的一整套流程都加入锁中,否则就会出现时序问题。

在这里插入图片描述

分布式锁

分布式锁演进-基本原理

在这里插入图片描述

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

其实就是redis加锁和解锁都需要原子性,然后value不能是固定的(随机的大字符串)。

public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {

        // 1. 分布式占锁,去redis占坑
        String uuid = UUID.randomUUID().toString();
        // redis nx 命令
        // 业务执行完了,删锁的时候机器断电了,其他服务器想占锁就占不到了,就造成死锁
        // 设置过期时间来解决这个问题,而且必须是同步的,原子的

        // 删锁的时候,如果v是固定的,则有可能误删
        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid,300, TimeUnit.SECONDS );

        if (lock){


            Map<String, List<Catelog2Vo>> dataFromDb;
            try{
                // 加锁成功,执行业务
                dataFromDb = getDataFromDb();
            }finally {
                // 判断v是否是自己的与对比成功删除 也应该是原子性的
                // 使用redis+lua脚本来实现

                // 如果redis调用了get命令 等于 我们传递的值,则删除,否则返回0
                String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
                //删除锁
                Long lock1 = stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
            }



//            stringRedisTemplate.delete("lock"); // 删除锁
            return dataFromDb;

        }else {
            // 加锁失败,重试
            // 休眠100ms,重试

            return getCatalogJsonFromDbWithRedisLock(); // 自旋的方式
        }

redisson

‌Redisson是一个基于Java的分布式Java对象和服务的框架,专门设计用于在分布式环境中操作各种数据结构。

上面的代码已经基本上可以实现分布式锁的功能,但是我们每段业务代码都需要写一套,虽然我们可以写个工具类,但redisson更专业。

使用redisson完成以后所有分布式锁功能

<!--        以后使用redisson作为所有分布式锁,分布式对象等功能框架-->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.12.0</version>
        </dependency>
package com.atlinxi.gulimall.product.config;

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;

@Configuration
public class MyRedissonConfig {


    /**
     * 所有对redisson的使用都是通过RedissonClient对象
     * @return
     * @throws IOException
     */
    @Bean(destroyMethod = "shutdown")
    public RedissonClient redisson() throws IOException{

        // 创建配置
        Config config = new Config();
        config.useSingleServer()
                .setAddress("redis://192.168.56.10:6379");

        return Redisson.create(config);
    }


}

可重入锁(Reentrant Lock)和看门狗机制

函数a调用函数b,两个函数都需要加锁,而且两者加的是同一把锁,

a函数被调用,a函数调用b函数,因为a函数此时已经加锁了,b函数知晓a函数加锁了,b函数直接拿来用,给a返回结果,然后a释放锁。

相反的,不可重入锁就是a拿到锁之后调用b,a的锁不给b用,就造成死锁了。

所以我们所有的锁都应该设计为可重入锁,避免涉及到死锁的问题。

@ResponseBody
    @GetMapping("/hello")
    public String hello(){

        // 1. 获取一把锁,只要锁的名字一样,就是同一把锁
        RLock lock = redissonClient.getLock("mylock");
        try {
            // 2. 加锁
//            lock.lock(); // 阻塞式等待,默认加的锁都是30s时间
            // 1)锁的自动续期,如果业务超长,运行期间自动给锁续上新的30s不用担心业务时间长,锁自动过期会删掉的问题
            // 2)加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30s以后自动删除

            lock.lock(10, TimeUnit.SECONDS); // 10s自动解锁,自动解锁时间一定要大于业务的执行时间
            // 问题:lock.lock(10, TimeUnit.SECONDS); 在锁时间到了之后,不会自动续期。
            // 1. 如果我们传递了锁的超时时间,就发送给redis执行脚本,进行占锁,默认超时就是我们指定的时间
            // 2. 如果我们未指定超时时间,就使用 30 * 1000【LockWatchdogTimeout看门狗的默认时间】
            //      只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】
            //      1/3的看门狗时间,10s,会自动续期,续成30s

            // 最佳实战,
            // lock.lock(10, TimeUnit.SECONDS); 省掉了整个续期操作。
            // 如果设置了一个很大的时间还不能解锁,那其实说明业务逻辑可能有问题了
            System.out.println("加锁成功,执行业务。。。。。。" + Thread.currentThread().getId());
            Thread.sleep(30000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 3. 解锁    假设解锁代码没有运行,redisson会不会出现死锁
            System.out.println("释放锁:" + Thread.currentThread().getId());
            lock.unlock();
        }

        return "hello";
    }

公平锁

多个线程抢占锁,先到先得,反之,非公平锁就是释放锁之后,线程再竞争锁。

读写锁

读的时候需要等待写的锁释放,

@ResponseBody
    @GetMapping("/write")
    public String writeValue(){

        RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("rw-lock");
        String s = null;
        // 1. 改数据加写锁,读数据加读锁
        RLock rLock = readWriteLock.writeLock();
        rLock.lock();
        try {

            s = UUID.randomUUID().toString();
            Thread.sleep(30000);
            stringRedisTemplate.opsForValue().set("write",s);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rLock.unlock();
        }
        return s;
    }


    // 保证一定能读到最新数据,修改期间,写锁是一个排他锁(互斥锁,独享所),读锁是一个共享锁
    // 写锁没释放读就必须等待
    // 读 + 读:相当于无锁,并发读,只会在redis中记录好,所有当前的读锁。他们都会同时加锁成功
    // 写 + 读:等待写锁释放
    // 写 + 写:阻塞方式
    // 读 + 写:有读锁,写也需要等待

    // 只要有写的存在,都必须等待
    @ResponseBody
    @GetMapping("/read")
    public String readValue(){

        RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("rw-lock");
        String s = null;
        RLock rLock = readWriteLock.readLock();
        // 加读锁
        rLock.lock();
        try {

            s = stringRedisTemplate.opsForValue().get("write");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rLock.unlock();
        }
        return s;
    }

信号量

/**
     * 信号量
     *
     * 3车位,
     * park 例如 value 是 3 ,每获取一个减一个,直到0,然后阻塞
     *
     * 等释放后,park才执行
     *
     * 可以应用在限流,假设一个信号量value为 10 * 1000,如果可以获取到才允许访问,否则得等释放后再访问
     */
    @ResponseBody
    @GetMapping("/park")
    public String park() throws InterruptedException {

        RSemaphore park = redissonClient.getSemaphore("park");
//        park.acquire();//获取一个信号,获取一个值,占一个车位
        boolean b = park.tryAcquire();// 上面那个如果没有车位会一直等待,而这个会返回false
        return "ok";

    }


    @ResponseBody
    @GetMapping("/go")
    public String go() throws InterruptedException {

        RSemaphore park = redissonClient.getSemaphore("park");
        park.release();//释放一个车位
        return "ok";

    }

闭锁

/**
     * 放假,锁门
     * 1班没人了,2班没人了。。。
     * 5个班全部走完,我们可以锁大门
     */
    @ResponseBody
    @GetMapping("/lockDoor")
    public String lockDoor() throws InterruptedException {

        RCountDownLatch door = redissonClient.getCountDownLatch("door");

        door.trySetCount(5L);
        door.await(); // 等待闭锁都完成

        return "放假了。。。";

    }

    @ResponseBody
    @GetMapping("/gogogo")
    public String gogogo(@PathVariable("id") Long id) throws InterruptedException {

        RCountDownLatch door = redissonClient.getCountDownLatch("door");
        door.countDown();  // 计数减1

        return id + "班的人走了";

    }

缓存数据一致性

双写模式

脏数据解决方案

  1. 为写数据库、写缓存加锁
  2. 这种延迟可以通过设置过期时间,在得到脏数据后,重新读取的时候又会获得新的数据;就看业务对这种延迟的容忍性有多大
    在这里插入图片描述

失效模式 最终解决方案(加读写锁)

  1. 第一个请求,改为1,删除缓存
  2. 第二个请求,改为2,删除缓存,假设这台服务器因为一些原因响应时间较长
  • 当第1个请求缓存删完了以后,第3个请求进来读,缓存里面没有数据,然后读数据库,但是此时db-2还没改完,就读到了db-1的数据,
    • 此时要更新缓存,如果更新比db2快的话,它更新的就是db-1的数据,db-2在执行完就会再次把缓存删除,相当于没更新
    • 此时要更新缓存,如果更新比db2慢的话,那db中的数据就是db2,缓存中的数据就是db1了

解决问题

  1. 加锁,加锁的问题是会影响效率
  2. 如果这个数据经常修改,而且实时性要求高的,就要考虑要不要加缓存了

我们系统的一致性解决方案就用失效模式

  • 缓存的所有数据都有过期时间,数据过期下一次查询触发主动更新
  • 读写数据的时候,加上分布式的读写锁。
    • 经常写,经常读,会有很大的影响
    • 经常读,偶尔写,一点儿影响都没有
      在这里插入图片描述

解决方案

在这里插入图片描述

  1. 用户大概率不会同时提交或者删除订单,也不会同时修改用户的个性签名这些
  2. 菜单,商品介绍等基础数据,我们可以容忍大程度的缓存不一致
    例如iphone的介绍,如果修改之后,过一两天更新也可能是没关系的

Canal是一个阿里的中间件,它伪装成mysql的一个从服务器,从服务器的特点就是mysql数据库里面有什么变化,它都会同步过来

好处:我们不需要再关心缓存的操作,只需要关心数据库的
缺点:又加了一个中间件,还得开发一些我们自定义的功能,如果一次开发成型,我们就不需要管这些事儿了

binlog:二进制日志,日志里有每一次mysql什么东西更新了

数据异构大概的意思就是,例如京东,给每个人首页推荐的东西不一样,就是Canal通过浏览记录,购物车等等,经过计算,为你推荐。

我们暂时使用缓存的数据,都定义在读多写少的场景,暂时不适用Canal
在这里插入图片描述

改造数据一致性代码

我们使用的是

  1. 失效模式,设置过期时间
  2. 数据一致性的解决方案是读写锁

我们发现,后来的每一个业务代码都要用缓存,用的都是这种编码模式

Spring Cache

Spring 从 3.1 开始定义了 org.springframework.cache.Cache
和 org.springframework.cache.CacheManager 接口来统一不同的缓存技术;
并支持使用 JCache(JSR-107)注解简化我们开发;

基础概念

我们应用中需要配一个或多个CacheManager(缓存管理器),可以管理多种缓存,EmpCache,Salary Cache
在这里插入图片描述
CacheManager(缓存管理器)> Cache(缓存组件)

CacheManager帮我们造出很多缓存组件,Cache负责缓存的读写

我们这儿使用的缓存管理器是RedisCacheManager,缓存组件是RedisCache

整合spring cache

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

application.properties

#自动配置了哪些
#CacheAutoConfiguration会导入RedisCacheConfiguration
#自动配好了缓存管理器 RedisCacheManager
spring.cache.type=redis
# redis key ttl ms为单位
spring.cache.redis.time-to-live=3600000
# 缓存的前缀,如果指定了前缀就用我们指定的前缀,如果没有,就默认使用缓存的名字作为前缀
# @Cacheable(value = {"category"},key = "#root.method.name")
#   category::getLevel1Categorys
# 我们这里指定了,就是
# CACHE_getLevel1Categorys
spring.cache.redis.key-prefix=CACHE_
# 如果不使用前缀,那么注解中和配置文件中的都不会生效
# getLevel1Categorys
spring.cache.redis.use-key-prefix=true
# 是否缓存空值,防止缓存穿透
spring.cache.redis.cache-null-values=true

缓存注解

@Cacheable: Triggers cache population.

触发将数据保存到缓存的操作,代表当前方法的结果需要缓存,
缓存中有,读缓存,缓存中没有,存入缓存

@CacheEvict: Triggers cache eviction.触发将数据从缓存删除,失效模式

@CachePut: Updates the cache without interfering with the method execution.不影响方法执行更新缓存,双写模式

@Caching: Regroups multiple cache operations to be applied on a method.组合以上多个操作

@CacheConfig: Shares some common cache-related settings at class-level.在类级别共享缓存的相同配置

	// 读模式下使用缓存
	// key是指存到redis的key
	// @Cacheable(value = {"category"},key = "'getLevel1Categorys'")
    // 获取到函数名,当作key
    @Cacheable(value = {"category"},key = "#root.method.name")
    @Override
    public List<CategoryEntity> getLevel1Categorys() {
        List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
        return categoryEntities;
    }




@Cacheable(value = "category",key = "#root.methodName")
    @Override
    public Map<String, List<Catelog2Vo>> getCatalogJson() {
        // 性能优化,将数据库的多次查询变为一次
        List<CategoryEntity> selectList = this.baseMapper.selectList(null);

        // 1. 查出所有一级分类数据
        List<CategoryEntity> level1Categorys = getParentCid(selectList,0L);

        // 2. 封装数据
        // 这儿的key和value都是level1Categorys
        Map<String, List<Catelog2Vo>> parentCid = level1Categorys.stream().collect(Collectors.toMap(key -> key.getCatId().toString(), value -> {
            // 1. 每一个的一级分类,查询这个一级分类的二级分类
            List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>()
                    .eq("parent_cid", value.getCatId()));

            // 2. 封装上面的结果
            List<Catelog2Vo> catelog2Vos = null;
            if (categoryEntities != null) {
                catelog2Vos = categoryEntities.stream().map(level2 -> {
                    Catelog2Vo catelog2Vo = new Catelog2Vo(value.getCatId().toString(), null, level2.getCatId().toString(), level2.getName());

                    // 1. 找当前二级分类的三级分类封装成vo
                    List<CategoryEntity> level3Catelog = baseMapper.selectList(new QueryWrapper<CategoryEntity>()
                            .eq("parent_cid", level2.getCatId()));

                    if (level3Catelog != null){
                        // 2. 封装成指定格式
                        List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(level3 -> {
                            Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(level2.getCatId().toString(),level3.getCatId().toString(),level3.getName());
                            return catelog3Vo;
                        }).collect(Collectors.toList());
                        catelog2Vo.setCatalog3List(collect);

                    }

                    return catelog2Vo;
                }).collect(Collectors.toList());
            }

            return catelog2Vos;

        }));

        return parentCid;
    }




 /**
     * 级联更新所有相关数据
     * @param category
     *
     * @CacheEvict 失效模式
     *
     * key 是spel表达式,如果是字符串需要加单引号,否则会默认为表达式
     *
     * 1. @Caching 同时进行多种缓存操作
     * 2. @CacheEvict(value = "category",allEntries = true) 删除category分区下所有的数据
     * 3. 存储同一类型的数据,可以指定成同一个分区。分区名默认就是缓存的前缀
     */
//    @CacheEvict(value = "category",key = "'getLevel1Categorys'")
//    @Caching(evict = {
//            @CacheEvict(value = "category",key = "'getLevel1Categorys'"),
//            @CacheEvict(value = "category",key = "'getCatalogJson'")
//    })
    @CacheEvict(value = "category",allEntries = true)
    @Transactional
    @Override
    public void updateCascade(CategoryEntity category) {
        this.updateById(category);

        if (!StringUtils.isEmpty(category.getName())){
            // 同步更新其他关联表中的数据
            categoryBrandRelationService.updateCategory(category.getCatId(),category.getName());

            // todo 更新其他关联表
        }
    }



默认配置

  1. 如果缓存中有,方法不用调用;如果缓存中没有,会调用方法,最后将方法的结果放入缓存

  2. 每一个需要缓存的数据我们都来指定要放到哪个名字的缓存。【缓存的分区(按照业务类型分)】

  3. key默认自动生成,缓存的名字::SimpleKey []

  4. value的值,默认使用jdk序列化机制,将序列化后的数据存到redis

  5. ttl默认-1

默认配置原理

缓存自动配置 CacheAutoConfiguration

// 将所有属性绑定在CacheProperties
@EnableConfigurationProperties({CacheProperties.class})
@Import({CacheAutoConfiguration.CacheConfigurationImportSelector.class, CacheAutoConfiguration.CacheManagerEntityManagerFactoryDependsOnPostProcessor.class})
public class CacheAutoConfiguration {


// 缓存自动配置会导入一些缓存的配置类
static class CacheConfigurationImportSelector implements ImportSelector {
        CacheConfigurationImportSelector() {
        }

        public String[] selectImports(AnnotationMetadata importingClassMetadata) {
            CacheType[] types = CacheType.values();
            String[] imports = new String[types.length];

            for(int i = 0; i < types.length; ++i) {
                imports[i] = CacheConfigurations.getConfigurationClass(types[i]);
            }

            return imports;
        }
    }

缓存配置类 CacheConfigurations

final class CacheConfigurations {
	static {
        Map<CacheType, String> mappings = new EnumMap(CacheType.class);
        mappings.put(CacheType.GENERIC, GenericCacheConfiguration.class.getName());
        mappings.put(CacheType.EHCACHE, EhCacheCacheConfiguration.class.getName());
        mappings.put(CacheType.HAZELCAST, HazelcastCacheConfiguration.class.getName());
        mappings.put(CacheType.INFINISPAN, InfinispanCacheConfiguration.class.getName());
        mappings.put(CacheType.JCACHE, JCacheCacheConfiguration.class.getName());
        mappings.put(CacheType.COUCHBASE, CouchbaseCacheConfiguration.class.getName());
        mappings.put(CacheType.REDIS, RedisCacheConfiguration.class.getName());
        mappings.put(CacheType.CAFFEINE, CaffeineCacheConfiguration.class.getName());
        mappings.put(CacheType.SIMPLE, SimpleCacheConfiguration.class.getName());
        mappings.put(CacheType.NONE, NoOpCacheConfiguration.class.getName());
        MAPPINGS = Collections.unmodifiableMap(mappings);
    }

CacheAutoConfiguration帮我们导入了 RedisCacheConfiguration

package org.springframework.boot.autoconfigure.cache;

class RedisCacheConfiguration {

	// 注入了缓存管理器
	@Bean
    RedisCacheManager cacheManager(CacheProperties cacheProperties, CacheManagerCustomizers cacheManagerCustomizers, ObjectProvider<org.springframework.data.redis.cache.RedisCacheConfiguration> redisCacheConfiguration, ObjectProvider<RedisCacheManagerBuilderCustomizer> redisCacheManagerBuilderCustomizers, RedisConnectionFactory redisConnectionFactory, ResourceLoader resourceLoader) {
        // 决定缓存用哪个配置
        RedisCacheManagerBuilder builder = RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(this.determineConfiguration(cacheProperties, redisCacheConfiguration, resourceLoader.getClassLoader()));
        // 按照我们配置文件中配置的缓存名字
        List<String> cacheNames = cacheProperties.getCacheNames();
        if (!cacheNames.isEmpty()) {
            builder.initialCacheNames(new LinkedHashSet(cacheNames));
        }

        if (cacheProperties.getRedis().isEnableStatistics()) {
            builder.enableStatistics();
        }

        redisCacheManagerBuilderCustomizers.orderedStream().forEach((customizer) -> {
            customizer.customize(builder);
        });
        // 帮我们初始化所有的缓存
        return (RedisCacheManager)cacheManagerCustomizers.customize(builder.build());
    }




	private org.springframework.data.redis.cache.RedisCacheConfiguration createConfiguration(CacheProperties cacheProperties, ClassLoader classLoader) {
        Redis redisProperties = cacheProperties.getRedis();
        org.springframework.data.redis.cache.RedisCacheConfiguration config = org.springframework.data.redis.cache.RedisCacheConfiguration.defaultCacheConfig();
        config = config.serializeValuesWith(SerializationPair.fromSerializer(new JdkSerializationRedisSerializer(classLoader)));
        if (redisProperties.getTimeToLive() != null) {
            config = config.entryTtl(redisProperties.getTimeToLive());
        }

        if (redisProperties.getKeyPrefix() != null) {
            config = config.prefixCacheNameWith(redisProperties.getKeyPrefix());
        }

        if (!redisProperties.isCacheNullValues()) {
            config = config.disableCachingNullValues();
        }

        if (!redisProperties.isUseKeyPrefix()) {
            config = config.disableKeyPrefix();
        }

        return config;
    }

RedisCacheConfiguration

我们自定义配置的注入的容器是这个,名字和上面一样,但是包不一样

package org.springframework.data.redis.cache;

import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Optional;
import java.util.function.Consumer;
import org.springframework.cache.interceptor.SimpleKey;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterRegistry;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;

public class RedisCacheConfiguration {
    private final Duration ttl;
    private final boolean cacheNullValues;
    private final CacheKeyPrefix keyPrefix;
    private final boolean usePrefix;
    private final SerializationPair<String> keySerializationPair;
    private final SerializationPair<Object> valueSerializationPair;
    private final ConversionService conversionService;

    private RedisCacheConfiguration(Duration ttl, Boolean cacheNullValues, Boolean usePrefix, CacheKeyPrefix keyPrefix, SerializationPair<String> keySerializationPair, SerializationPair<?> valueSerializationPair, ConversionService conversionService) {
        this.ttl = ttl;
        this.cacheNullValues = cacheNullValues;
        this.usePrefix = usePrefix;
        this.keyPrefix = keyPrefix;
        this.keySerializationPair = keySerializationPair;
        this.valueSerializationPair = valueSerializationPair;
        this.conversionService = conversionService;
    }

    public static RedisCacheConfiguration defaultCacheConfig() {
        return defaultCacheConfig((ClassLoader)null);
    }

自定义配置

  1. 指定生成的缓存使用的key
    key属性指定,接收一个SpEL
  2. 指定缓存的数据的存活时间:配置文件修改
  3. 将数据保存为json格式

将数据保存为json格式

自定义RedisCacheConfiguration即可

package com.atlinxi.gulimall.product.config;

import org.springframework.beans.factory.annotation.Autowired;
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.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;


/**
 *
 * Spring Cache默认配置的原理
 *
 * CacheAutoConfiguration(缓存配置类)-> 帮我们导入了 RedisCacheConfiguration,-> 它又帮我们自动配置了RedisCacheManager(缓存管理器)
 * -> 初始化所有的缓存 -> 每个缓存决定使用什么配置 ->如果RedisCacheConfiguration有就用已有的,没有就用默认配置 -> 想改缓存的配置,只需要给容器中放一个RedisCacheConfiguration即可
 * -> 就会应用到RedisCacheManager管理的所有缓存分区中
 */

// 开启属性配置的绑定功能,相当于让这个类的绑定配置文件生效
@EnableConfigurationProperties(CacheProperties.class)
@Configuration
// 开启缓存
@EnableCaching
public class MyCacheConfig {


    // 因为已经注入容器了,所以可以这么用
//    @Autowired
//    CacheProperties cacheProperties;

    /**
     *
     *
     *1. 原来(源代码)中配置文件绑定的配置类是这样子的
     * 只是说spring.cache这个文件绑定
     * 并没有放到容器中,我们不能直接使用
     * @ConfigurationProperties(
     *     prefix = "spring.cache"
     * )
     * public class CacheProperties {
     *
     *
     * 解决用这个
     * EnableConfigurationProperties
     *
     *
     *
     *
     * @return
     */
    // @Bean 将结果放入容器
    // 方法中的所有参数都会从容器中确定,和@Autowired效果一样
    @Bean
    public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties){

        // 照着RedisCacheConfiguration写就行
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();


        // 因为源码中每次修改完之后,都会返回一个新的对象,所以只能这么写,不能链式调用
        // SerializationPair redis序列化器
        // 将redis的key序列化器保持原样,还是string,value序列化器使用spring提供的jackson,json数据
        config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
        config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));

        // 将配置文件中的所有配置都生效
        // 这相当于人家怎么配的,我们又重新写了一份
        CacheProperties.Redis redisProperties = cacheProperties.getRedis();
        if (redisProperties.getTimeToLive() != null) {
            config = config.entryTtl(redisProperties.getTimeToLive());
        }

        if (redisProperties.getKeyPrefix() != null) {
            config = config.prefixCacheNameWith(redisProperties.getKeyPrefix());
        }

        if (!redisProperties.isCacheNullValues()) {
            config = config.disableCachingNullValues();
        }

        if (!redisProperties.isUseKeyPrefix()) {
            config = config.disableKeyPrefix();
        }

        return config;
    }
}

springCache不足

  • 读模式

    1. 缓存穿透:查询一个null数据。解决:缓存空数据,spring.cache.redis.cache-null-values=true
    2. 缓存击穿:大量并发同时查询一个正好过期的数据,使大量并发同时落到数据库。解决:加锁,springCache默认没有为我们加锁
      @Cacheable(value = “category”,key = “#root.methodName”,sync=true)
      sync,springCache源码中是使用synchronized进行加锁,加的是本地锁,而非分布式锁,但关系不大,即使集群有100台机器,也才查一百次而已
    3. 缓存雪崩:大量的key同时过期。
      在超大型系统里面可能会存在,但在一般应用中,即使key同时过期,只要不是十几万个key同时过期,就不需要考虑这个问题。
      解决:加随机时间,很容易弄巧成拙,比如a数据3s过期,随机时间为1s,b数据2s过期,随机时间为2s,不加随机时间,失效时间没有冲撞在一起,加了之后反而冲撞了。
      真正解决:加过期时间即可,因为我们存储的时间是不一样的。spring.cache.redis.time-to-live=3600000
  • 写模式:(缓存与数据库一致)

    1. 读写加锁。适用于读多写少的场景,加锁是影响效率的
    2. 引入Canal,感知到Mysql的更新去更新缓存
    3. 读多写多,直接去数据库查询就行

总结:

  • 常规数据(读多写少,即时性,一致性要求不高的数据),完全可以使用springCache
    写模式(只要缓存的数据有过期时间就足够了)
  • 特殊数据:特殊设计

我觉得不管是太太,小孩也是,任何关系,尤其是夫妻关系,我觉得爱还没有尊重重要,当你不尊敬对方的时候,那关系就很难维持

那个尊敬,不管你是大牌小牌,卑微的人还是你做的不对的时候,在道德上面拿不到标准,你没有努力的时候,你懒散的时候,你敷衍的时候,讲谎话的时候,他不尊重就是不尊重,从眼神看的出来,不是态度,你也不能去逼人家,这都很自然的,包括你小孩要看你就似乎很自然的

李安

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值