• 从.Net到Java学习第七篇——SpringBoot Redis 缓存穿透


    从.Net到Java学习系列目录

    场景描述:我们在项目中使用缓存通常都是先检查缓存中是否存在,如果存在直接返回缓存内容,如果不存在就直接查询数据库然后再缓存查询结果返回。这个时候如果我们查询的某一个数据在缓存中一直不存在,就会造成每一次请求都查询DB,这样缓存就失去了意义,在流量大时,可能DB就挂掉了。

    穿透:频繁查询一个不存在的数据,由于缓存不命中,每次都要查询持久层。从而失去缓存的意义。

    常用解决办法:
    ①用一个bitmap和n个hash函数做布隆过滤器过滤没有缓存的键。
    ②持久层查询不到就缓存空结果,有效时间为数分钟。

    我这里使用的是双重检测同步锁方式。

    修改AreaService接口,添加如下两个接口方法,selectAllArea2方法是可能会导致缓存穿透的方法。

        List<Area> selectAllArea();
        List<Area> selectAllArea2();

    修改接口的实现类AreaServiceImpl

      @Autowired
        private RedisService redisService;
        private JSONObject json = new JSONObject();
    
        /**
         * 从缓存中获取区域列表
         *
         * @return
         */
        private List<Area> getAreaList() {
            String result = redisService.get("redis_obj_area");
            if (result == null || result.equals("")) {
                return null;
            } else {
                return json.parseArray(result, Area.class);
            }
        }
    
        @Override
        public List<Area> selectAllArea() {
            List<Area> list = getAreaList();
            if (list == null) {
                synchronized (this) {
                    list = getAreaList(); //双重检测锁
                    if (list == null) {
                        list = areaMapper.selectAllArea();
                        redisService.set("redis_obj_area", json.toJSONString(list));
                        System.out.println("请求的数据库。。。。。。");
                    } else {
                        System.out.println("请求的缓存。。。。。。");
                    }
                }
            } else {
                System.out.println("请求的缓存。。。。。。");
            }
            return list;
        }
    
        @Override
        public List<Area> selectAllArea2() {
            List<Area> list = getAreaList();
            if (list == null) {
                list = areaMapper.selectAllArea();
                redisService.set("redis_obj_area", json.toJSONString(list));
                System.out.println("请求的数据库。。。。。。");
            } else {
                System.out.println("请求的缓存。。。。。。");
            }
            return list;
        }

    运行程序,在浏览器中输入地址http://localhost:8083/boot/getAll,第一次访问

    2018-06-22 10:21:24.730  INFO 10436 --- [nio-8083-exec-1] com.alibaba.druid.pool.DruidDataSource   : {dataSource-1} inited
    请求的数据库。。。。。。

    刷新浏览器地址,第二次访问

    请求的缓存。。。。。。

    再打开我们的redis可视化管理工具

    在之前配置mysql数据库连接的时候,由于没有指定是否采用SSL,所以控制台会有一个警告信息,如下所示:

    这个是因为使用的mysql版本比较高,要求开启SSL,所以控制台会有一个警告,当然,你也可以忽略,如果要去除这个警告,可以在之前的mysql连接配置后面添加:&useSSL=false

      datasource:
        url: jdbc:mysql://localhost:3306/demo?&useSSL=false

    删除redis中的这个key值,我们通过使用一个并发测试工具来模拟缓存穿透的现象,这里使用到了jmeter这个并发测试工具。jmeter官网:  https://jmeter.apache.org/。jmeter更多使用教程:https://www.yiibai.com/jmeter/

    将jmter下载到本地,然后解压,双击jmeter.bat运行

    (1)右键单击“测试计划”,新建测试组

    (2)新建HTTP请求

    (3)保存并运行测试,这是时候其实已经在开始运行了,我们可以通过“选项"——“Log Viewer",来查看运行日志。

    此时再查看IDEA中的控制台运行情况如下:

    我们看到有四次进行了数据库查询,而我们想要的其实是只进行一次数据库查询,其它的都是直接从缓存中进行查询。

    重新删除redis中的key值redis_obj_area,我们再来测试一下采用了双重检测同步锁的方法selectAllArea2

    修改jmeter中的请求路径

    然后运行,我们再看下IDEA中控制台中的记录:

    现在只有第一次是从数据库中读取了。

    当然,如果我们不采用测试工具的话,我们也可以自己写一个单元测试,来进行并发测试。

     单元测试类AreaServiceImplTest的代码:

    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class AreaServiceImplTest {
        @Autowired
        public AreaService areaService;
        @Before
        public void setUp() throws Exception {
    
        }
    
        @Test
        public void selectAllArea() throws InterruptedException {
            final CountDownLatch latch= new CountDownLatch(4);//使用java并发库concurrent
            //启用10个线程
            for(int i=1;i<=10;i++){
                new Thread(new Runnable(){
                    public void run(){
                        try {
                            //Thread.sleep(100);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                        areaService.selectAllArea();
                        System.out.println(String.format("子线程%s执行!",Thread.currentThread().getName()));
                        latch.countDown();//让latch中的数值减一
                    }
                }).start();
            }
            //主线程
            latch.await();//阻塞当前线程直到latch中数值为零才执行
            System.out.println("主线程执行!");
        }
        @Test
        public void selectAllArea2() throws InterruptedException {
            final CountDownLatch latch= new CountDownLatch(4);//使用java并发库concurrent
            //启用10个线程
            for(int i=1;i<=10;i++){
                new Thread(new Runnable(){
                    public void run(){
                        try {
                            //Thread.sleep(100);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                        areaService.selectAllArea2();
                        System.out.println(String.format("子线程%s执行!",Thread.currentThread().getName()));
                        latch.countDown();//让latch中的数值减一
                    }
                }).start();
            }
            //主线程
            latch.await();//阻塞当前线程直到latch中数值为零才执行
            System.out.println("主线程执行!");
        }
        @Test
        public void selectAllArea3(){
            Runnable runnable=new Runnable() {
                @Override
                public void run() {
                    areaService.selectAllArea2();
                }
            };
            ExecutorService executorService=Executors.newFixedThreadPool(4);
            for (int i=0;i<10;i++){
                executorService.submit(runnable);
            }
        }
    }

    运行结果,和使用jmeter是差不多的。

  • 相关阅读:
    初始线程(相关理论)
    python并发编程之多进程2-(数据共享及进程池和回调函数)
    python并发编程之多进程1--(互斥锁与进程间的通信)
    Cpython支持的进程与线程
    网络编程之进程理论简介
    python之网络socket编程
    Gray码 (格雷码) 【二进制】
    [BZOJ 2350] [Poi2011] Party 【Special】
    [BZOJ 1033] [ZJOI2008] 杀蚂蚁antbuster 【模拟!】
    [BZOJ 3209] 花神的数论题 【数位统计】
  • 原文地址:https://www.cnblogs.com/jiekzou/p/9212114.html
Copyright © 2020-2023  润新知