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

从.Net到Java学习系列目录html

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

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

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

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

修改AreaService接口,添加以下两个接口方法,selectAllArea2方法是可能会致使缓存穿透的方法。数据库

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

修改接口的实现类AreaServiceImplapache

  @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,第一次访问json

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是差很少的。

相关文章
相关标签/搜索