谷粒商城高级篇——商品分类使用redis本地缓存设计

业务分析

对于访问商品首页的时候需要去后台去访问商品的分类数据,是一个树形结构的json,这种数据我们把它加入缓存中提升页面的加载速度

导入依赖

gulimall-redis模块

这里我建议抽离出一个redis的模块,或者将redis的依赖导出common模块中,后期可以为其他服务调用复用,并且可以使用工具类封装redis的一些常用操作

pom.xml

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

gulimall-product

application.yml

spring:
  redis:
    host: 192.168.163.131
    port: 6379

核心代码部分

springboot对redis的依赖整合了starter,可以通过封装了的
RedisTemplate 去操作redis

这里我们使用StringRedisTemplate操作redis

@Autowired
StringRedisTemplate redisTemplate;
@Override
public Map<String, List<Catalogs2Vo>> getCatalogJson() {
    // 1.从缓存中读取分类信息
    String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
    if (StringUtils.isEmpty(catalogJSON)) {
        // 2. 缓存中没有,查询数据库
        Map<String, List<Catalogs2Vo>> catalogJsonFromDB = getCatalogJsonFromDB();
        // 3. 查询到的数据存放到缓存中,将对象转成 JSON 存储
        redisTemplate.opsForValue().set("catalogJSON", JSON.toJSONString(catalogJsonFromDB));
        return catalogJsonFromDB;
    }
    return JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalogs2Vo>>>(){});
}

/**
 * 加缓存前,只读取数据库的操作
 *
 * @return
 */
public Map<String, List<Catalogs2Vo>> getCatalogJsonFromDB() {
    System.out.println("查询了数据库");

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

    //1、查出所有分类
    //1、1)查出所有一级分类
    List<CategoryEntity> level1Categories = getParentCid(selectList, 0L);

    //封装数据
    Map<String, List<Catalogs2Vo>> parentCid = level1Categories.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
        //1、每一个的一级分类,查到这个一级分类的二级分类
        List<CategoryEntity> categoryEntities = getParentCid(selectList, v.getCatId());

        //2、封装上面的结果
        List<Catalogs2Vo> catalogs2Vos = null;
        if (categoryEntities != null) {
            catalogs2Vos = categoryEntities.stream().map(l2 -> {
                Catalogs2Vo catalogs2Vo = new Catalogs2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName().toString());

                //1、找当前二级分类的三级分类封装成vo
                List<CategoryEntity> level3Catelog = getParentCid(selectList, l2.getCatId());

                if (level3Catelog != null) {
                    List<Catalogs2Vo.Category3Vo> category3Vos = level3Catelog.stream().map(l3 -> {
                        //2、封装成指定格式
                        Catalogs2Vo.Category3Vo category3Vo = new Catalogs2Vo.Category3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());

                        return category3Vo;
                    }).collect(Collectors.toList());
                    catalogs2Vo.setCatalog3List(category3Vos);
                }

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

        return catalogs2Vos;
    }));

    return parentCid;
}

错误分析

这里可能会产生堆外内存溢出异常:OutOfDirectMemoryError。
下面进行分析:
SpringBoot 2.0 以后默认使用 lettuce 作为操作 redis 的客户端,它使用 netty 进行网络通信;
lettuce 的 bug 导致 netty 堆外内存溢出;
netty 如果没有指定堆外内存,默认使用 -Xmx 参数指定的内存;
可以通过 -Dio.netty.maxDirectMemory 进行设置;

这里是lettuce 的bug,连接池问题,资源没有得到很好的释放,达到正向资源循环

解决方案:不能只使用 -Dio.netty.maxDirectMemory 去调大堆外内存,这样只会延缓异常出现的时间。
升级 lettuce 客户端,或使用 jedis 客户端

通过修改

gulimall-redis

pom.xml

<!-- redis -->
<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>

使用jedis代替lettuce 客户端

并发下的问题

在大并发下,首次读取缓存不存在的情况,可能出现大量访问被定向到数据库,且存在缓存有效期失效时,缓存失效带来的一系列问题

分布式锁的redis的实现

上面的情况我们可以通过redis实现的分布式锁实现

redis分布式锁的原理:setnx,同一时刻只能设置成功一个

前提,锁的key是一定的,value可以变

没获取到锁阻塞或者sleep一会

设置好了锁,玩意服务出现宕机,没有执行删除锁逻辑,这就造成了死锁

解决:设置过期时间
业务还没执行完锁就过期了,别人拿到锁,自己执行完去删了别人的锁

解决:锁续期(redisson有看门狗),。删锁的时候明确是自己的锁。如uuid
判断uuid对了,但是将要删除的时候锁过期了,别人设置了新值,那删除了别人的锁

解决:删除锁必须保证原子性(保证判断和删锁是原子的)。使用redis+Lua脚本完成,脚本是原子的

代码变动

/**
* 从数据库查询并封装数据::分布式锁
*
* @return
*/
public Map<String, List<Catalogs2Vo>> getCatalogJsonFromDbWithRedisLock() {

    //1、占分布式锁。去redis占坑 设置过期时间必须和加锁是同步的,保证原子性(避免死锁)
    String uuid = UUID.randomUUID().toString();
    Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
    if (lock) {
        System.out.println("获取分布式锁成功...");
        Map<String, List<Catalogs2Vo>> dataFromDb = null;
        try {
            //加锁成功...执行业务
            dataFromDb = getCatalogJsonFromDB();
        } finally {
            // lua 脚本解锁
            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<>(script, Long.class), Collections.singletonList("lock"), uuid);
        }
        //先去redis查询下保证当前的锁是自己的
        //获取值对比,对比成功删除=原子性 lua脚本解锁
        // String lockValue = stringRedisTemplate.opsForValue().get("lock");
        // if (uuid.equals(lockValue)) {
        //     //删除我自己的锁
        //     stringRedisTemplate.delete("lock");
        // }
        return dataFromDb;
    } else {
        System.out.println("获取分布式锁失败...等待重试...");
        //加锁失败...重试机制
        //休眠一百毫秒
        try {
            TimeUnit.MILLISECONDS.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return getCatalogJsonFromDbWithRedisLock();     //自旋的方式
    }
}

其中加锁和删除锁的操作一定保证原子性,我们是通过
setIfAbsent
封装的是setnx的lua脚本
lua脚本具有原子性的操作

实现了分布式锁的作用,解决了缓存失效的问题

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值