✅P156-157_缓存-缓存使用-加锁解决缓存击穿问题

gong_yz大约 6 分钟谷粒商城

操作前注意删除缓存


本地锁实现

代码实现

给查数据库的方法加上锁,得到锁之后,再去缓存中查询一次,如果没有,再继续查询

代码如下:

cfmall-product/src/main/java/com/gyz/cfmall/product/service/impl/CategoryServiceImpl.java

@Override
public Map<String, List<Catelog2Vo>> getCatalogJson() {
    //加入缓存逻辑
    String catelogJSON = stringRedisTemplate.opsForValue().get("catelogJSON");
    if (StringUtils.isEmpty(catelogJSON)) {
        //缓存中没有,查询数据库
        System.out.println("缓存未命中.....查询数据库");
        Map<String, List<Catelog2Vo>> categoryJsonMap = getCatalogJsonFromDb();
        String newCategoryJson = JSON.toJSONString(categoryJsonMap);
        // 设置过期时间,解决缓存雪崩
        stringRedisTemplate.opsForValue().set("catelogJSON", newCategoryJson, 1, TimeUnit.DAYS);
        return categoryJsonMap;
    }
    System.out.println("缓存命中.....直接返回");
    Map<String, List<Catelog2Vo>> result = JSON.parseObject(catelogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
    });
    return result;
}
//从数据库查询并封装分类的数据
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDb() {

    //this代表当前实例对象,这里是锁当前对象。
    synchronized (this) {
        //得到锁后,我们应该再去缓存确定一次,如果没有才需要继续查询
        String catalogJSON = stringRedisTemplate.opsForValue().get("catelogJSON");
        if (!StringUtils.isEmpty(catalogJSON)) {
            Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
            });
            return result;
        }
        System.out.println("查询了数据库.....");
        // 一次性获取所有数据
        List<CategoryEntity> selectList = baseMapper.selectList(null);
        // 1)、所有1级分类
        List<CategoryEntity> level1Categorys = getParent_cid(selectList, 0L);
        // 2)、封装数据
        Map<String, List<Catelog2Vo>> collect = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), level1 -> {
            // 查到当前1级分类的2级分类
            List<CategoryEntity> category2level = getParent_cid(selectList, level1.getCatId());
            List<Catelog2Vo> catalog2Vos = null;
            if (category2level != null) {
                catalog2Vos = category2level.stream().map(level12 -> {
                    // 查询当前2级分类的3级分类
                    List<CategoryEntity> category3level = getParent_cid(selectList, level12.getCatId());
                    List<Catelog2Vo.Category3Vo> catalog3Vos = null;
                    if (category3level != null) {
                        catalog3Vos = category3level.stream().map(level13 -> {
                            return new Catelog2Vo.Category3Vo(level12.getCatId().toString(), level13.getCatId().toString(), level13.getName());
                        }).collect(Collectors.toList());
                    }
                    return new Catelog2Vo(level1.getCatId().toString(), catalog3Vos, level12.getCatId().toString(), level12.getName());
                }).collect(Collectors.toList());
            }
            return catalog2Vos;
        }));
 
        return result;
    }
}

Jmeter压测

线程组

HTTP请求

效果

出现问题 :查询了多次数据库,本地锁时序问题

问题原因:当线程1获得锁之后查询数据库,返回查询的结果并释放锁;线程1在将查询的数据存储到redis时,线程2获得锁,发现此时缓存并未命中,因此进行数据库的查询

解决方案:确认缓存没有、查询数据库、把存入缓存的操作放在锁中

锁时序问题解决

实现代码如下:将getCatalogJson()方法存储分类数据到Redis的操作放到synchronized锁中进行

@Override
public Map<String, List<Catelog2Vo>> getCatalogJson() {
    //加入缓存逻辑
    String catelogJSON = stringRedisTemplate.opsForValue().get("catelogJSON");
    if (StringUtils.isEmpty(catelogJSON)) {
        //缓存中没有,查询数据库
        System.out.println("缓存未命中.....查询数据库");
        Map<String, List<Catelog2Vo>> categoryJsonMap = getCatalogJsonFromDb();
//            String newCategoryJson = JSON.toJSONString(categoryJsonMap);
//            // 设置过期时间,解决缓存雪崩
//            stringRedisTemplate.opsForValue().set("catelogJSON", newCategoryJson, 1, TimeUnit.DAYS);
//            return categoryJsonMap;
    }
    System.out.println("缓存命中.....直接返回");
    Map<String, List<Catelog2Vo>> result = JSON.parseObject(catelogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
    });
    return result;
}
/**
 * 从数据库查询并封装分类的数据
 * @return
 */
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDb() {

    //this代表当前实例对象,这里是锁当前对象。
    synchronized (this) {
        //得到锁后,我们应该再去缓存确定一次,如果没有才需要继续查询
        String catalogJSON = stringRedisTemplate.opsForValue().get("catelogJSON");
        if (!StringUtils.isEmpty(catalogJSON)) {
            Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
            });
            return result;
        }
        System.out.println("查询了数据库.....");
        // 一次性获取所有数据
        List<CategoryEntity> selectList = baseMapper.selectList(null);
        // 1)、所有1级分类
        List<CategoryEntity> level1Categorys = getParent_cid(selectList, 0L);
        // 2)、封装数据
        Map<String, List<Catelog2Vo>> collect = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), level1 -> {
            // 查到当前1级分类的2级分类
            List<CategoryEntity> category2level = getParent_cid(selectList, level1.getCatId());
            List<Catelog2Vo> catalog2Vos = null;
            if (category2level != null) {
                catalog2Vos = category2level.stream().map(level12 -> {
                    // 查询当前2级分类的3级分类
                    List<CategoryEntity> category3level = getParent_cid(selectList, level12.getCatId());
                    List<Catelog2Vo.Category3Vo> catalog3Vos = null;
                    if (category3level != null) {
                        catalog3Vos = category3level.stream().map(level13 -> {
                            return new Catelog2Vo.Category3Vo(level12.getCatId().toString(), level13.getCatId().toString(), level13.getName());
                        }).collect(Collectors.toList());
                    }
                    return new Catelog2Vo(level1.getCatId().toString(), catalog3Vos, level12.getCatId().toString(), level12.getName());
                }).collect(Collectors.toList());
            }
            return catalog2Vos;
        }));
        //getCatalogJson()方法存储分类数据到Redis的操作移动到此处
        String catelogInfo = JSON.toJSONString(collect);
        stringRedisTemplate.opsForValue().set("catelogJSON", catelogInfo, 1, TimeUnit.DAYS);
        return collect;
    }
}

Jmeter压测

线程组

HTTP请求

效果:1000个线程访问接口,只查询了一次数据库


本地锁时序问题

测试结果发现查询了多次数据库

原因:

  • 假设现在有100W并发,进来之后这些线程先去看缓存,结果缓存里面都没有,都准备去查询数据库
  • 以其中一台机器为例,查数据库的时候,上来就加了一把锁,确保只让一个线程进来,假设为A线程,它在查询数据库之后,结果放入缓存之前,就将锁释放掉
  • 此时,锁住的B线程进来了,它进来之后,也是先确认缓存中有没有,此时A线程刚释放锁,要往缓存中放数据,然而放数据是一次网络交互,可能会很慢,包括系统刚启动、还要为redis建立连接、还要整线程池、线程池还没有初始化等等,所以第一次操作是一个很慢的过程,假设会花费30ms的时间。
  • 可能C线程刚进来,A线程的数据才放到缓存中,B线程的数据还没放完,所以C线程判断缓存有没有数据的时候,可能判断的就是A线程之前给里边放的缓存数据,所以C线程就不会查询数据库了。
  • 最终A线程查询了一次,B线程查询了一次。

分析

  • 无论我们是给方法块、还是代码块上加锁,都是将当前实例作为锁,当前实例在我们容器中是单实例;
  • 但是我们是一个服务对应一个容器,里面的每一个 this 只能代表当前实例的对象,以上图为例,八个容器就有八个锁, 每一个 this 都是不同的锁;
  • 最终导致的现象就是,第1个商品服务的一把锁,锁住了1W个请求,只放进了一个请求,第2个乃至后面的商品服务,都是如此,有几台机器,最终就会放几个线程进来,那就相当于有8个线程同时进来,去数据库查相同的数据

比如:我们可以多创建几个商品服务,来模拟这种测试,由网关负载均衡到某一个商品服务,通过Nginx-> 网关 -> 多个商品服务,来进行压测。不同端口代表不同商品服务。

测试步骤

复制cfmall-product服务,端口分别为8200820182028203,具体操作如下:

选中服务名称,鼠标右键,

Jmeter压测三级分类

测试结果