从.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();
</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)">
 * 从缓存中获取区域列表
 *
 * </span><span style="color: rgba(128, 128, 128, 1)">@return</span>
 <span style="color: rgba(0, 128, 0, 1)">*/</span>
<span style="color: rgba(0, 0, 255, 1)">private</span> List&lt;Area&gt;<span style="color: rgba(0, 0, 0, 1)"> getAreaList() {
    String result </span>= redisService.get("redis_obj_area"<span style="color: rgba(0, 0, 0, 1)">);
    </span><span style="color: rgba(0, 0, 255, 1)">if</span> (result == <span style="color: rgba(0, 0, 255, 1)">null</span> || result.equals(""<span style="color: rgba(0, 0, 0, 1)">)) {
        </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">;
    } </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> {
        </span><span style="color: rgba(0, 0, 255, 1)">return</span> json.parseArray(result, Area.<span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)">);
    }
}

@Override
</span><span style="color: rgba(0, 0, 255, 1)">public</span> List&lt;Area&gt;<span style="color: rgba(0, 0, 0, 1)"> selectAllArea() {
    List</span>&lt;Area&gt; list =<span style="color: rgba(0, 0, 0, 1)"> getAreaList();
    </span><span style="color: rgba(0, 0, 255, 1)">if</span> (list == <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">) {
        </span><span style="color: rgba(0, 0, 255, 1)">synchronized</span> (<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">) {
            list </span>= getAreaList(); <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">双重检测锁</span>
            <span style="color: rgba(0, 0, 255, 1)">if</span> (list == <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">) {
                list </span>=<span style="color: rgba(0, 0, 0, 1)"> areaMapper.selectAllArea();
                redisService.set(</span>"redis_obj_area"<span style="color: rgba(0, 0, 0, 1)">, json.toJSONString(list));
                System.out.println(</span>"请求的数据库。。。。。。"<span style="color: rgba(0, 0, 0, 1)">);
            } </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> {
                System.out.println(</span>"请求的缓存。。。。。。"<span style="color: rgba(0, 0, 0, 1)">);
            }
        }
    } </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> {
        System.out.println(</span>"请求的缓存。。。。。。"<span style="color: rgba(0, 0, 0, 1)">);
    }
    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> list;
}

@Override
</span><span style="color: rgba(0, 0, 255, 1)">public</span> List&lt;Area&gt;<span style="color: rgba(0, 0, 0, 1)"> selectAllArea2() {
    List</span>&lt;Area&gt; list =<span style="color: rgba(0, 0, 0, 1)"> getAreaList();
    </span><span style="color: rgba(0, 0, 255, 1)">if</span> (list == <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">) {
        list </span>=<span style="color: rgba(0, 0, 0, 1)"> areaMapper.selectAllArea();
        redisService.set(</span>"redis_obj_area"<span style="color: rgba(0, 0, 0, 1)">, json.toJSONString(list));
        System.out.println(</span>"请求的数据库。。。。。。"<span style="color: rgba(0, 0, 0, 1)">);
    } </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> {
        System.out.println(</span>"请求的缓存。。。。。。"<span style="color: rgba(0, 0, 0, 1)">);
    }
    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> list;
}</span></pre>

运行程序,在浏览器中输入地址 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
</span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span> selectAllArea() <span style="color: rgba(0, 0, 255, 1)">throws</span><span style="color: rgba(0, 0, 0, 1)"> InterruptedException {
    </span><span style="color: rgba(0, 0, 255, 1)">final</span> CountDownLatch latch= <span style="color: rgba(0, 0, 255, 1)">new</span> CountDownLatch(4);<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">使用java并发库concurrent
    </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">启用10个线程</span>
    <span style="color: rgba(0, 0, 255, 1)">for</span>(<span style="color: rgba(0, 0, 255, 1)">int</span> i=1;i&lt;=10;i++<span style="color: rgba(0, 0, 0, 1)">){
        </span><span style="color: rgba(0, 0, 255, 1)">new</span> Thread(<span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> Runnable(){
            </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> run(){
                </span><span style="color: rgba(0, 0, 255, 1)">try</span><span style="color: rgba(0, 0, 0, 1)"> {
                    </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">Thread.sleep(100);</span>
                } <span style="color: rgba(0, 0, 255, 1)">catch</span><span style="color: rgba(0, 0, 0, 1)"> (Exception e) {
                    e.printStackTrace();
                }
                areaService.selectAllArea();
                System.out.println(String.format(</span>"子线程%s执行!"<span style="color: rgba(0, 0, 0, 1)">,Thread.currentThread().getName()));
                latch.countDown();</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">让latch中的数值减一</span>

}
}).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 是差不多的。