加锁解决缓存击穿问题

前言

最近在项目中用到了Redis缓存,然后如果是在高并发的场景下,如果我去查询一条数据,在缓存中是没有的,但是在数据库中是有数据的,那么在高并发的前提下,它会一下子把所有的请求打到数据库,这就是缓存击穿的一个经典问题了。

业务逻辑

Controller

  @ResponseBody
    @GetMapping("index/json/catalog.json")
    public Map<String, List<Catelog2Vo>> getCatlogJson(){

        return categoryService.getCatalogJson();

    }

业务逻辑了就是这样,现在我要查询所有的分类菜单,我把分类菜单的数据放到了redis中,如果redis中没有数据,则会查询数据库,这就是问题的由来了,如果这个时候我们的缓存中没有分类菜单的数据,假如这时候有100万个请求过来,所有的请求都会打到数据库,那我们该怎么解决呢???

Service

  /**
     * 使用redis缓存, 获取所有分类的json数据
     * @return
     */
    public Map<String, List<Catelog2Vo>> getCatalogJson(){
        //给缓存中存放json字符串, 取的时候拿到json字符串进行逆转,逆转为对象类型 【序列化与发序列化】

        ValueOperations<String, String> ops = redisTemplate.opsForValue();
        //从缓存中获取分类菜单
        String catalogJson = ops.get("catalogJson");

        //如果缓存中没有分类菜单
        if (StringUtils.isEmpty(catalogJson)) {

            //将从db获取的分类菜单存到缓存(json)
            System.out.println("缓存不命中...查询数据库...");
            Map<String, List<Catelog2Vo>> catalog = getCatalogJsonForDB();
            ops.set("catalogJson", JSON.toJSONString(catalog), 1 , TimeUnit.DAYS);

            return catalog;

        }
        System.out.println("缓存命中....直接返回...");
        //否则【缓存中有分类菜单】 -> 直接返回
        //把json字符串逆转为对象类型
        return JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {} );
    }


    /**
     * 从数据库中获取所有的分类
     * @return
     */
    public Map<String, List<Catelog2Vo>> getCatalogJsonForDB() {
		String catalogJson = redisTemplate.opsForValue().get("catalogJson");
		if (!StringUtils.isEmpty(catalogJson)) {
			return JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {} );
		}
		System.out.println("查询了数据库");

		//查询所有的一级分类
		// List<CategoryEntity> level1 = getLevel1Catagories();

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

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


		Map<String, List<Catelog2Vo>> collect = level1.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
			//1、每一个的一级分类,查到这个一级分类的二级分类
			List<CategoryEntity> categoryEntities = getParentCid(selectList, v.getCatId());
			//2、封装上面的结果
			List<Catelog2Vo> catalogs2Vos = null;
			if (categoryEntities != null) {
				catalogs2Vos = categoryEntities.stream().map(l2 -> {
					Catelog2Vo catelog2Vo = new Catelog2Vo(
						v.getCatId().toString(),
						null,
						l2.getCatId().toString(),
						l2.getName()
					);
					//1. 找当前二级分类的三级分类封装成vo
					List<CategoryEntity> level3Catelog = getParentCid(selectList, l2.getCatId());

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

			return catalogs2Vos;
		}));


		return collect;
	}
    

解决办法-加锁

这里我们可以用synchronized(this)加锁,但是这是不推荐的,但还是记录一下吧。
我们这里给数据库查询的这个方法getCatalogJsonForDB里面加锁,当有100万个请求时,它会锁住所有的线程。但是仅仅是加了synchronized(this)是没有实际作用的,假如前一个请求操作数据库后,所有的线程将被锁住,当前一个请求操作数据库完毕后,后面的请求还是会继续进行锁竞争进行操作数据库,那这样是肯定与我们的初衷不符的,我们想要的是如果缓存中有(前一个线程已经操作了数据库,后面的所有线程无需继续操作数据库),那么直接返回。

我们需要在同步代码块里面再次进行对缓存中是否有数据进行判断,如果前面的线程操作后缓存中有数据了,后面的线程无需在操作数据库。
image

   /**
     * 使用redis缓存, 获取所有分类的json数据
     * @return
     */
    public Map<String, List<Catelog2Vo>> getCatalogJson(){
        //给缓存中存放json字符串, 取的时候拿到json字符串进行逆转,逆转为对象类型 【序列化与发序列化】

        ValueOperations<String, String> ops = redisTemplate.opsForValue();
        //从缓存中获取分类菜单
        String catalogJson = ops.get("catalogJson");

        //如果缓存中没有分类菜单
        if (StringUtils.isEmpty(catalogJson)) {

            //将从db获取的分类菜单存到缓存(json)
            System.out.println("缓存不命中...查询数据库...");
            Map<String, List<Catelog2Vo>> catalog = getCatalogJsonForDB();
            ops.set("catalogJson", JSON.toJSONString(catalog), 1 , TimeUnit.DAYS);

            return catalog;

        }
        System.out.println("缓存命中....直接返回...");
        //否则【缓存中有分类菜单】 -> 直接返回
        //把json字符串逆转为对象类型
        return JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {} );
    }


    /**
     * 从数据库中获取所有的分类
     * @return
     */
    public Map<String, List<Catelog2Vo>> getCatalogJsonForDB() {
        //只要是同一把锁, 就能锁住这个锁的所有线程
        //sysnchronized(this) , SpringBoot的所有的组件在容器中都是单例的

        //TODO 本地锁: synchronized, JUC包(Lock) ,在分布式情况下,想要锁住所有,必须使用分布式锁
        synchronized (this) {
            String catalogJson = redisTemplate.opsForValue().get("catalogJson");
            //我们现在要做的就是 第一次锁完之后,后面应该去确认一下缓存, 如果没有才需要继续查询
            if (!StringUtils.isEmpty(catalogJson)) {
                return JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {} );
            }
            System.out.println("查询了数据库");

            //查询所有的一级分类
            // List<CategoryEntity> level1 = getLevel1Catagories();

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

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


            Map<String, List<Catelog2Vo>> collect = level1.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
                //1、每一个的一级分类,查到这个一级分类的二级分类
                List<CategoryEntity> categoryEntities = getParentCid(selectList, v.getCatId());
                //2、封装上面的结果
                List<Catelog2Vo> catalogs2Vos = null;
                if (categoryEntities != null) {
                    catalogs2Vos = categoryEntities.stream().map(l2 -> {
                        Catelog2Vo catelog2Vo = new Catelog2Vo(
                                v.getCatId().toString(),
                                null,
                                l2.getCatId().toString(),
                                l2.getName()
                        );
                        //1. 找当前二级分类的三级分类封装成vo
                        List<CategoryEntity> level3Catelog = getParentCid(selectList, l2.getCatId());

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

                return catalogs2Vos;
            }));


            return collect;
        }
    }

这里我们用apifox模拟一下压力测试, 直接同时创建20个线程访问同一个接口,看操作了几次数据库。可以看到我么20次并发的访问了这个接口还是访问了3次数据库这是为什么呢?
image

image
这里导致查询了3次数据库的原因是锁的时序问题。
举个例子:

时序问题就是在线程1查询出结果,并且释放锁之后,将结果放入缓存之前,线程2获取到锁,又重新去查询了数据库,就会导致重复的数据库查询。
image
举个例子:
线程1查数据库后放到缓存中,这个放缓存的操作需要与网络进行交互、线程池、Redis建立连接等一系列耗时操作导致线程2没有反应过来误判了缓存中没有数据又去查了一遍数据库
image

解决办法

就是把放入缓存这一操作和前面的两个操作放到一起,做原子操作,不要分开。
image

最终代码

   /**
     * 使用redis缓存, 获取所有分类的json数据
     * @return
     */
    public Map<String, List<Catelog2Vo>> getCatalogJson(){
        //给缓存中存放json字符串, 取的时候拿到json字符串进行逆转,逆转为对象类型 【序列化与发序列化】

        //从缓存中获取分类菜单
        String catalogJson =  redisTemplate.opsForValue().get("catalogJson");

        //如果缓存中没有分类菜单
        if (StringUtils.isEmpty(catalogJson)) {

            //将从db获取的分类菜单存到缓存(json)
            System.out.println("缓存不命中...查询数据库...");
            Map<String, List<Catelog2Vo>> catalog = getCatalogJsonForDB();

            return catalog;

        }
        System.out.println("缓存命中....直接返回...");
        //否则【缓存中有分类菜单】 -> 直接返回
        //把json字符串逆转为对象类型
        return JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {} );
    }


    /**
     * 从数据库中获取所有的分类
     * @return
     */
    public Map<String, List<Catelog2Vo>> getCatalogJsonForDB() {
        //只要是同一把锁, 就能锁住这个锁的所有线程
        //sysnchronized(this) , SpringBoot的所有的组件在容器中都是单例的

        //TODO 本地锁: synchronized, JUC包(Lock) ,在分布式情况下,想要锁住所有,必须使用分布式锁
        synchronized (this) {
            String catalogJson = redisTemplate.opsForValue().get("catalogJson");
            //我们现在要做的就是 第一次锁完之后,后面应该去确认一下缓存, 如果没有才需要继续查询
            if (!StringUtils.isEmpty(catalogJson)) {
                return JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {} );
            }
            System.out.println("查询了数据库");

            //查询所有的一级分类
            // List<CategoryEntity> level1 = getLevel1Catagories();

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

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


            Map<String, List<Catelog2Vo>> collect = level1.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
                //1、每一个的一级分类,查到这个一级分类的二级分类
                List<CategoryEntity> categoryEntities = getParentCid(selectList, v.getCatId());
                //2、封装上面的结果
                List<Catelog2Vo> catalogs2Vos = null;
                if (categoryEntities != null) {
                    catalogs2Vos = categoryEntities.stream().map(l2 -> {
                        Catelog2Vo catelog2Vo = new Catelog2Vo(
                                v.getCatId().toString(),
                                null,
                                l2.getCatId().toString(),
                                l2.getName()
                        );
                        //1. 找当前二级分类的三级分类封装成vo
                        List<CategoryEntity> level3Catelog = getParentCid(selectList, l2.getCatId());

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

                return catalogs2Vos;
            }));

            redisTemplate.opsForValue().set("catalogJson", JSON.toJSONString(collect), 1 , TimeUnit.DAYS);
            return collect;
        }
    }

还是20个并发请求, 可以看到最后只操作了一次数据库
image

posted @ 2022-11-14 21:10  长情c  阅读(85)  评论(0)    收藏  举报