20.缓存-缓存使用

本文详细介绍了如何利用Redis优化系统性能,包括选择适合缓存的数据、本地缓存与分布式缓存的区别、集成Redis的操作步骤、解决内存溢出问题以及应对缓存击穿的策略。通过实例展示了缓存数据的存取、更新以及使用分布式锁保证数据一致性。

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

1 缓存使用

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

哪些数据适合放入锾存?

  • 即时性、数据—致性要求不高的
  • 访问量大且更新频率不高的数据(读多,写少)

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

image-20220511144955873

2 本地缓存与分布式缓存

2.1 本地缓存

image-20220511145243767

2.2 分布式缓存-本地模式在分布式下的问题

其中一个服务的缓存修改了,但是其他服务里的缓存没有办法更新,无法保证数据的一致性

image-20220511145344581

2.3分布式缓存

image-20220511145803127

3 整合Redis

3.1 引入依赖

product模块

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

3.2 配置redis

redis:
  host: 192.168.56.10
  port: 6379

3.3 单元测试

@Test
void test03() {
    ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();

    ops.set("hello", "world_" + UUID.randomUUID());

    String hello = ops.get("hello");
    System.out.println("保存的数据是:" + hello);
}

4 改造三级分类业务

将原来的getCatalogJson方法改名为getCatalogJsonFromDB,并创建一个新的getCatalogJson方法

@Autowired
StringRedisTemplate stringRedisTemplate;

@Override
public Map<String, List<Catelog2Vo>> getCatalogJson() {
    // 给缓存中放JSON字符串,拿出的JSON字符串,还需要逆转为能用的对象类型

    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;
    }

    // 将缓存中查询出的json转换为指定的对象
    Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
    });
    return result;
}


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

    // 查询出表pms_category所有的记录实体
    List<CategoryEntity> categoryEntityList = baseMapper.selectList(null);

    // 查出所有的一次分类
    List<CategoryEntity> level_1_categorys = getParent_cid(categoryEntityList, 0L);

    // 封装数据,构造一个以1级id为键,2级分类列表为值的map
    Map<String, List<Catelog2Vo>> collect = level_1_categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), l1 -> {
        // 根据一级分类id查找二级分类
        List<CategoryEntity> level_2_categorys = getParent_cid(categoryEntityList, l1.getCatId());

        // 封装结果为Catelog2Vo的集合
        List<Catelog2Vo> catelog2Vos = null;

        if (level_2_categorys != null) {

            // 把 level_2_categorys 封装为 catelog2Vos
            catelog2Vos = level_2_categorys.stream().map(l2 -> {
                Catelog2Vo catelog2Vo = new Catelog2Vo(l1.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());

                // 根据二级分类id查找三级分类
                List<CategoryEntity> level_3_categorys = getParent_cid(categoryEntityList, l2.getCatId());

                // 将 level_3_categorys 封装为 catelog3Vos
                if (level_3_categorys != null) {
                    List<Catelog2Vo.Catelog3Vo> catelog3Vos = level_3_categorys.stream().map(l3 -> {
                        Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
                        return catelog3Vo;
                    }).collect(Collectors.toList());

                    catelog2Vo.setCatalog3List(catelog3Vos);
                }

                return catelog2Vo;

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

        }

        return catelog2Vos;
    }));

    return collect;
}

重启服务,访问http://localhost:10000/index/catalog.json测试

5 压力测试内存溢出

云服务器的吞吐量低是网络带宽原因。

问题原因:

  1. SpringBoot2.0以后默认使用Lettuce作为操作redis的客户端。它使用netty进行通信
  2. Lettuce的bug导致netty堆外内存溢出
  3. netty如果没有指定堆外内存,会默认使用服务配置里的 -Xmx 作为堆外内存的大小
  4. 可以通过-Dio.netty.maxDirectMemory进行设置

解决方法:

  1. 升级 Lettuce客户端
  2. 切换使用 jedis客户端

5.1 切换为jedis

1、排除Lettuce依赖包

2、导入jedis依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <exclusions>
        <exclusion>
            <groupId>io.lettuce</groupId>
            <artifactId>lettuce-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>

3、重启服务测试

坑:切换为jedis客户端后压力测试时报如下异常nested exception is redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketTimeoutException: Read timed out] with root cause

问题原因:

我的redis在阿里云服务器上,压力测试时可能由于网速太慢的问题,redis连接可能超时了

解决方法:

配置redis的超时时间

redis:
  timeout: 100000

5.2 redisTemplate和Lettuce的关系

redisTemplate:spring对Lettuce、jedis的再次封装

Lettuce、jedis:操作redis的底层客户端

6 缓存失效问题

6.1 缓存穿透

image-20220511164308543

6.2 缓存雪崩

image-20220511164548747

6.3 缓存击穿

image-20220511164733266

7 解决缓存击穿

7.1 分布式锁

image-20220512180731679

7.2 锁时序图

image-20220512181249782

7.3 本地锁查询数据库

getCatalogJsonFromDB方法加锁,

getCatalogJson方法中添加缓存的代码转移到getCatalogJsonFromDB

改进后的getCatalogJson方法

@Override
public Map<String, List<Catelog2Vo>> getCatalogJson() {
    // 给缓存中放JSON字符串,拿出的JSON字符串,还需要逆转为能用的对象类型

    String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");

    if (!StringUtils.hasText(catalogJSON)) {
        // 缓存中没有,查询数据库
        return getCatalogJsonFromDB();
    }

    // 将缓存中查询出的json转换为指定的对象
    Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
    });
    return result;
}

改进后的getCatalogJsonFromDB方法

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

    synchronized (this) {
        // 得到锁以后,在去缓存中确定一次
        String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");
        if(!StringUtils.isEmpty(catalogJSON)) {
            Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
            });
            return result;
        }

        // 查询出表pms_category所有的记录实体
        List<CategoryEntity> categoryEntityList = baseMapper.selectList(null);

        // 查出所有的一次分类
        List<CategoryEntity> level_1_categorys = getParent_cid(categoryEntityList, 0L);

        // 封装数据,构造一个以1级id为键,2级分类列表为值的map
        Map<String, List<Catelog2Vo>> collect = level_1_categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), l1 -> {
            // 根据一级分类id查找二级分类
            List<CategoryEntity> level_2_categorys = getParent_cid(categoryEntityList, l1.getCatId());

            // 封装结果为Catelog2Vo的集合
            List<Catelog2Vo> catelog2Vos = null;

            if (level_2_categorys != null) {

                // 把 level_2_categorys 封装为 catelog2Vos
                catelog2Vos = level_2_categorys.stream().map(l2 -> {
                    Catelog2Vo catelog2Vo = new Catelog2Vo(l1.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());

                    // 根据二级分类id查找三级分类
                    List<CategoryEntity> level_3_categorys = getParent_cid(categoryEntityList, l2.getCatId());

                    // 将 level_3_categorys 封装为 catelog3Vos
                    if (level_3_categorys != null) {
                        List<Catelog2Vo.Catelog3Vo> catelog3Vos = level_3_categorys.stream().map(l3 -> {
                            Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
                            return catelog3Vo;
                        }).collect(Collectors.toList());

                        catelog2Vo.setCatalog3List(catelog3Vos);
                    }

                    return catelog2Vo;

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

            }

            return catelog2Vos;
        }));

        // 将查到的数据放入缓存
        String s = JSON.toJSONString(collect);
        stringRedisTemplate.opsForValue().set("catalogJSON", s);
        return collect;
    }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值