谷粒商城:缓存

目录

缓存

本地缓存

分布式缓存

引入缓存

redis-quick-start

三级分类,引入cache

缓存穿透、击穿、雪崩

缓存穿透

缓存雪崩

缓存击穿

本地锁

[分布式锁 Redis SETNX](#分布式锁 Redis SETNX)


缓存

缓存中的数据应满足:

  1. 高频访问,低频更新
  2. 即时性,一致性 要求不高

本地缓存

java 复制代码
private Map<String,Object> cache = new HashMap<>();

在service层中引入 hashMap 作为 本地缓存。获取数据时,先访问map,map中没有对应数据,再去查询 数据库。

优点:

  • 访问速度快,延迟极低。

  • 实现简单,无需额外依赖。

缺点:

  • 数据一致性难以保证,多实例环境下,每个实例都有自己 独立的本地缓存。

  • 缓存容量有限。

  • 实例重启或崩溃会导致缓存丢失。

分布式缓存

将数据存储在独立的缓存服务(如Redis)中,多个应用实例共享同一缓存数据。

优点:

  • 数据一致性较好,多个实例共享同一份数据。

  • 缓存容量可扩展,支持大规模数据。

  • 缓存服务独立,实例重启不会丢失数据。

缺点:

  • 访问速度受网络延迟影响。

  • 需要额外的缓存服务,增加了系统复杂性。

  • 可能存在单点故障问题(需通过集群解决)。

引入缓存

redis-quick-start

1.引入spring-boot-starter-data-redis,自动配置Redis

XML 复制代码
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

2.配置连接信息

XML 复制代码
  redis:
    host: 192.168.40.128
    port: 6379

3.使用 RedisTemplate 或 StringRedisTemplate

java 复制代码
public class GulimallProductApplicationTests {

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @Test
    public void redisTest(){
        ValueOperations<String,String> valueOperations =  stringRedisTemplate.opsForValue();
        valueOperations.set("hello","world_" + UUID.randomUUID().toString());
        System.out.println(valueOperations.get("hello"));
    }
}

三级分类,引入cache

java 复制代码
    @Autowired
    private StringRedisTemplate redisTemplate;

    @Override
    public Map<String, List<Level2CategoryVo>> getLevel2AndLevel3Category() {
        //1.先从缓存中获取 catalogJson
        ValueOperations<String,String> valueOperations = redisTemplate.opsForValue();
        String catalogJson = valueOperations.get("catalogJson");
        Map<String, List<Level2CategoryVo>> res = null;
        if(StringUtils.isEmpty(catalogJson)){
            //缓存中无对应数据,查询数据库
            res = getCatalogJsonFromDB();
            //查询后 加入缓存
            valueOperations.set("catalogJson", JSON.toJSONString(res));
        }else{
            res = JSON.parseObject(catalogJson,new TypeReference<Map<String, List<Level2CategoryVo>> >(){});
        }
        return res;
    }

缓存穿透、击穿、雪崩

缓存系统中常见的三种问题,它们都会导致缓存失效,进而对数据库造成压力,甚至引发系统崩溃。

缓存穿透

缓存穿透是指查询一个不存在的数据,由于缓存和数据库中都没有,导致每次请求都直接访问数据库。

解决方案:缓存空值

对于查询结果为空的 Key,缓存一个空值(如 null),并设置较短的过期时间。

缓存雪崩

缓存雪崩是指大量缓存数据在同一时间过期,导致大量请求直接访问数据库,造成数据库压力过大甚至崩溃。

解决方案:分散过期时间

为缓存 Key 设置随机的过期时间,避免同时过期。

缓存击穿

缓存击穿是指某个热点数据在缓存中过期时,大量请求同时涌入数据库,导致数据库压力骤增。

解决方案:互斥锁

当缓存失效时,使用分布式锁(如 Redis 的 SETNX)确保只有一个线程去加载数据,其他线程等待。


加锁,解决缓存击穿,只有获得锁的线程可以访问数据。

本地锁

service在容器中总是单例的,可以使用当前 Service 对象作为锁来实现本地锁。

synchronized 是 Java 中最简单的锁机制,可以直接锁定当前对象(this)。

java 复制代码
    @Override
    public Map<String, List<Level2CategoryVo>> getLevel2AndLevel3Category() {
        //1.先从缓存中获取 catalogJson
        ValueOperations<String,String> valueOperations = redisTemplate.opsForValue();
        String catalogJson = valueOperations.get("catalogJson");
        Map<String, List<Level2CategoryVo>> res = null;
        if(StringUtils.isEmpty(catalogJson)){
            //缓存中无对应数据,查询数据库
            res = getCatalogJsonFromDB();
            //查询后 加入缓存
            valueOperations.set("catalogJson", JSON.toJSONString(res));
        }else{
            res = JSON.parseObject(catalogJson,new TypeReference<Map<String, List<Level2CategoryVo>> >(){});
        }
        return res;
    }

    /**
     * 从数据库中 获取 2 3 级 分类
     * @return
     */
    private Map<String, List<Level2CategoryVo>> getCatalogJsonFromDB(){
        synchronized (this){
            //再次确认,有无缓存
            ValueOperations<String,String> valueOperations = redisTemplate.opsForValue();
            String catalogJson = valueOperations.get("catalogJson");
            Map<String, List<Level2CategoryVo>> res = null;
            if(StringUtils.isEmpty(catalogJson)) {
                //缓存中无对应数据,查询数据库
                res = getCatalogJsonFromDB();
                //查询后 加入缓存
                valueOperations.set("catalogJson", JSON.toJSONString(res));
                return res;
            }

            List<CategoryEntity> selectList = this.baseMapper.selectList(null);

            //一级分类
            List<CategoryEntity> level1 = this.getByParentCid(selectList,0L);

            Map<String, List<Level2CategoryVo>> categoryMap = level1.stream().collect(Collectors.toMap(k -> k.getCatId().toString(),v ->{
                //查询该一级分类下的二级分类
                List<CategoryEntity> level2List = getByParentCid(selectList,v.getCatId());

                List<Level2CategoryVo> level2CategoryVos = null;
                if (level2List != null){
                    level2CategoryVos = level2List.stream().map(level2 -> {
                        //查询 二级分类 下的 三级分类
                        List<CategoryEntity> level3List = getByParentCid(selectList,level2.getCatId());

                        List<Level2CategoryVo.Level3Category> collect = null;
                        if (level3List != null) {
                            collect = level3List.stream().map(level3 -> {
                                Level2CategoryVo.Level3Category level3Category = new Level2CategoryVo.Level3Category(level2.getCatId(), level3.getCatId(), level3.getName());
                                return level3Category;
                            }).collect(Collectors.toList());
                        }
                        Level2CategoryVo level2CategoryVo = new Level2CategoryVo(v.getCatId(), collect, level2.getCatId(), level2.getName());
                        return level2CategoryVo;
                    }).collect(Collectors.toList());
                }
                return level2CategoryVos;
            }));
            return categoryMap;
        }

    }

由于网络io延迟,在redis保存缓存的过程中,会少量查询DB。

**缺点:**本地锁仅适用于单机环境。分布式中各主机获取到锁后,会各自查询数据库。

分布式系统需要使用分布式锁

分布式锁 Redis SETNX

全称为 SET if Not eXists ,用于在键不存在时设置键的值。它是一个原子操作,可用于实现分布式锁。成功set,即为成功获取到 锁

  1. 先从缓存中获取数据
  2. 缓存无对应数据,申请锁
  3. 成功获取锁后,查询数据库
  4. 释放锁

1.申请锁时,同时指定uuid与过期时间

java 复制代码
        String uuid = UUID.randomUUID().toString();
        //必须设置 过期时间,避免死锁,set和过期 必须是原子的
        Boolean lock = valueOperations.setIfAbsent("lock",uuid,300,TimeUnit.SECONDS);

设置过期时间,避免发生异常(如断电,程序异常等),导致锁未释放,死锁。

插入键值对和设置过期时间,应为原子操作,不能拆开。

2.释放锁时,匹配uuid

java 复制代码
        if (lock){
            System.out.println("成功获取lock...");
            //成功获取到锁
            Map<String, List<Level2CategoryVo>> res = null;

            try {
                res = getCatalogJsonFromDB();
            }finally {
                String script = "if redis.call('get',KEYS[1]) == ARGV[1] " +
                        "then return redis.call('del',KEYS[1]) " +
                        "else return 0 " +
                        "end";
                redisTemplate.execute(new DefaultRedisScript<Long>(script,Long.class),Arrays.asList("lock"),uuid);
                System.out.println("释放lock");
            }

            return res;

匹配uuid,只能释放自己的锁,防止在执行业务时间过长,锁过期,误删别人的锁。

使用script,才能同时匹配并删除键值对,原子操作。

3.自旋等待锁

java 复制代码
            System.out.println("自旋等待lock...");
            //没有获取到锁,自旋等待
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            return getCatalogJsonFromDBWithLock();

完整核心代码

java 复制代码
    @Override
    public Map<String, List<Level2CategoryVo>> getLevel2AndLevel3Category() {
        //1.先从缓存中获取 catalogJson
        ValueOperations<String,String> valueOperations = redisTemplate.opsForValue();
        String catalogJson = valueOperations.get("catalogJson");
        Map<String, List<Level2CategoryVo>> res = null;
        if(StringUtils.isEmpty(catalogJson)){
            //缓存中无对应数据,查询数据库
            res = getCatalogJsonFromDBWithLock();
        }else{
            res = JSON.parseObject(catalogJson,new TypeReference<Map<String, List<Level2CategoryVo>> >(){});
        }
        return res;
    }

    /**
     * 获取分布式锁,查询数据库
     * @return
     */
    private Map<String, List<Level2CategoryVo>> getCatalogJsonFromDBWithLock(){
        ValueOperations<String,String> valueOperations = redisTemplate.opsForValue();
        String uuid = UUID.randomUUID().toString();
        //必须设置 过期时间,避免死锁,set和过期 必须是原子的
        Boolean lock = valueOperations.setIfAbsent("lock",uuid,300,TimeUnit.SECONDS);
        if (lock){
            System.out.println("成功获取lock...");
            //成功获取到锁
            Map<String, List<Level2CategoryVo>> res = null;

            try {
                res = getCatalogJsonFromDB();
            }finally {
                String script = "if redis.call('get',KEYS[1]) == ARGV[1] " +
                        "then return redis.call('del',KEYS[1]) " +
                        "else return 0 " +
                        "end";
                redisTemplate.execute(new DefaultRedisScript<Long>(script,Long.class),Arrays.asList("lock"),uuid);
                System.out.println("释放lock");
            }

            return res;
        }else{
            System.out.println("自旋等待lock...");
            //没有获取到锁,自旋等待
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            return getCatalogJsonFromDBWithLock();
        }
    }

    /**
     * 从数据库中 获取 2 3 级 分类
     * @return
     */
    private Map<String, List<Level2CategoryVo>> getCatalogJsonFromDB(){

        //获得锁后,再次确认,有无缓存
        ValueOperations<String,String> valueOperations = redisTemplate.opsForValue();
        String catalogJson = valueOperations.get("catalogJson");
        Map<String, List<Level2CategoryVo>> res = null;
        if(!StringUtils.isEmpty(catalogJson)) {
            //缓存有数据
            res = JSON.parseObject(catalogJson,new TypeReference<Map<String, List<Level2CategoryVo>> >(){});
            return res;
        }

        System.out.println("查询数据库...");
        //缓存中无数据,再查询数据库
        List<CategoryEntity> selectList = this.baseMapper.selectList(null);

        //一级分类
        List<CategoryEntity> level1 = this.getByParentCid(selectList,0L);

        Map<String, List<Level2CategoryVo>> categoryMap = level1.stream().collect(Collectors.toMap(k -> k.getCatId().toString(),v ->{
            //查询该一级分类下的二级分类
            List<CategoryEntity> level2List = getByParentCid(selectList,v.getCatId());

            List<Level2CategoryVo> level2CategoryVos = null;
            if (level2List != null){
                level2CategoryVos = level2List.stream().map(level2 -> {
                    //查询 二级分类 下的 三级分类
                    List<CategoryEntity> level3List = getByParentCid(selectList,level2.getCatId());

                    List<Level2CategoryVo.Level3Category> collect = null;
                    if (level3List != null) {
                        collect = level3List.stream().map(level3 -> {
                            Level2CategoryVo.Level3Category level3Category = new Level2CategoryVo.Level3Category(level2.getCatId(), level3.getCatId(), level3.getName());
                            return level3Category;
                        }).collect(Collectors.toList());
                    }
                    Level2CategoryVo level2CategoryVo = new Level2CategoryVo(v.getCatId(), collect, level2.getCatId(), level2.getName());
                    return level2CategoryVo;
                }).collect(Collectors.toList());
            }
            return level2CategoryVos;
        }));

        //查询后 加入缓存
        valueOperations.set("catalogJson", JSON.toJSONString(categoryMap),1, TimeUnit.DAYS);
        return categoryMap;
    }
相关推荐
澜堇1 小时前
企业级部署zabbix分布式监控系统
分布式·zabbix
山河已无恙1 小时前
SpringBoot + SSE + rabbitMQ 实现服务端分布式广播推送
spring boot·分布式·java-rabbitmq
kunkun1012 小时前
RabbitMQ的高级特性介绍(二)
分布式·rabbitmq
敲上瘾2 小时前
定长内存池原理及实现
c++·缓存·aigc·池化技术
aloha_7892 小时前
redis解决缓存穿透/击穿/雪崩
java·数据库·redis·mysql·缓存·java-ee·springboot
时雨h4 小时前
芋道 Spring Cloud Alibaba 消息队列 RocketMQ 入门
微服务·面试·架构
在努力的韩小豪5 小时前
【微服务架构】本地负载均衡的实现(基于随机算法)
后端·spring cloud·微服务·架构·负载均衡
Pandaconda10 小时前
【后端开发面试题】每日 3 题(二十)
开发语言·分布式·后端·面试·消息队列·熔断·服务限流
Hello.Reader13 小时前
初探 Dubbo Rust SDK打造现代微服务的新可能
微服务·rust·dubbo
打死不学Java代码13 小时前
Redis分布式锁如何实现——简单理解版
java·开发语言·redis·分布式·缓存·面试