mybatis缓存(中)--二级缓存


概念这里不赘述了,上节都已介绍过了。

前提准备:

1 create table book(
2 id int auto_increment comment '书ID',
3 name varchar(50) comment '书名',
4 primary key(id));
5 
6 
7 insert into book(name) values('三国演义');
8 insert into book(name) values('红楼梦');
9 insert into book(name) values('水浒传');

二. 二级缓存

二级缓存的作用域更广泛,它不止局限于一个sqlSession,可以在多个sqlSession之间共享,事实上,它的作用域是namespace。
mybatis的二级缓存默认也是开启的,但由于他的作用域是namespace,所以还需要在mapper.xml中开启才能生效。

缓存的执行原理和上一节提到的一级缓存是差不多的,二级缓存与一级缓存区别在于二级缓存的范围更大,多个sqlSession可以共享一个mapper中的二级缓存区域。mybatis是如何区分不同mapper的二级缓存区域呢?它是按照不同mapper有不同的namespace来区分的,也就是说,如果两个mapper的namespace相同,即使是两个mapper,那么这两个mapper中执行sql查询到的数据也将存在相同的二级缓存区域中。

1. 开启二级缓存

1)因为springboot+mybatis这种集成方式是默认开启了二级缓存,如果非要显示配置,可以在appliacation.yml中添加如下黄色部分:

mybatis:
  mapper-locations: classpath:mapper/*.xml
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    cache-enabled: true

2)配置mapper.xml

直接在标签中添加标签即可。就可以开启二级缓存了。

note:除了在cache标签中配置这些属性,还可以通过注解的形式是在Mapper接口中配置这些属性,这里就不介绍了。

cache 标签有多个属性:

  • eviction: 缓存回收策略,有这几种回收策略
    • LRU - 最近最少回收,移除最长时间不被使用的对象
    • FIFO - 先进先出,按照缓存进入的顺序来移除它们
    • SOFT - 软引用,移除基于垃圾回收器状态和软引用规则的对象
    • WEAK - 弱引用,更积极的移除基于垃圾收集器和弱引用规则的对象

默认是 LRU 最近最少回收策略

  • flushinterval 缓存刷新间隔,缓存多长时间刷新一次,默认不清空,设置一个毫秒值
  • readOnly: 是否只读;true 只读,MyBatis 认为所有从缓存中获取数据的操作都是只读操作,不会修改数据。MyBatis 为了加快获取数据,直接就会将数据在缓存中的引用交给用户。不安全,速度快。读写(默认):MyBatis 觉得数据可能会被修改
  • size : 缓存存放多少个元素
  • type: 指定自定义缓存的全类名(实现Cache 接口即可)
  • blocking: 若缓存中找不到对应的key,是否会一直blocking,直到有对应的数据进入缓存

2. 实现序列化接口

mybatis的二级缓存是属于序列化,序列化的意思就是从内存中的数据传到硬盘中,这个过程就是序列化;也就是说,mybatis的二级缓存,实际上就是将数据放进了硬盘文件中去了。

因此Book这个实体类需要实现序列化Serializable。

 1 package com.example.demo.dao;
 2 
 3 import java.io.Serializable;
 4 
 5 public class Book implements Serializable {
 6     private int id;
 7     private String name;
 8 
 9     public int getId() {
10         return id;
11     }
12 
13     public void setId(int id) {
14         this.id = id;
15     }
16 
17     public String getName() {
18         return name;
19     }
20 
21     public void setName(String name) {
22         this.name = name;
23     }
24 
25     public String toString(){
26         return this.getId()+":"+this.getName();
27     }
28 }

 note:序列化的是book这个类,如果碰到它还有父类、级联属性,它们是不会被序列化的,我们需要让这些类也实现序列化。

3. 缓存时机

前面的准备工作都做好了:缓存开启、序列化已经做了。但是实际效果发现缓存并没有起效,而是关闭SqlSession才生效。这实际上就是mybatis的一个缓存实现机制。原理如下:

比如,我现在去查询ID为1的这个书名,获取该书名之后我们需要经过序列化然后存到硬盘上,因为mybatis的二级缓存实际上就是将数据保存到硬盘上的某个文件中。每来一个新的数据,比如三国演义存进来了,水浒传也需要存,如果是存储到硬盘上,那么就会用到IO技术。可是IO也是比较费性能,所以这个机制就是当你关闭sqlSesion的时候,我们把这些三国演义等书名数据一块保存到硬盘上,而不是来一个保存一个,这样IO承受不了,所以就存在关闭SqlSession缓存才生效的机制啦。

4. 案例

(1)controller

 1 package com.example.demo.controller;
 2 
 3 import com.example.demo.dao.Book;
 4 import com.example.demo.service.BookService;
 5 import org.springframework.beans.factory.annotation.Autowired;
 6 import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
 7 import org.springframework.web.bind.annotation.GetMapping;
 8 import org.springframework.web.bind.annotation.RequestMapping;
 9 import org.springframework.web.bind.annotation.RestController;
10 
11 @RestController
12 @EnableAutoConfiguration
13 @RequestMapping("/book")
14 public class BookController {
15     @Autowired
16     BookService bookService;
17 
18     @GetMapping("/selectBookById")
19     Book selectBookById(){
20       return bookService.selectBookById();
21     }
22 }

(2)service

package com.example.demo.service.impl;

import com.example.demo.dao.Book;
import com.example.demo.dao.mapper.BookMapper;
import com.example.demo.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class BookServiceImpl implements BookService {

    @Autowired
    BookMapper bookMapper;
    @Override

    public Book selectBookById() {
        int id=1;
        Book book1=bookMapper.selectBookById(id);
System.out.println(book1); Book book2
=bookMapper.selectBookById(id); System.out.println(book2); return book2; } }

(3)mapper

 1 package com.example.demo.dao.mapper;
 2 
 3 import com.example.demo.dao.Book;
 4 import org.apache.ibatis.annotations.Mapper;
 5 
 6 @Mapper
 7 public interface BookMapper {
 8     Book selectBookById(int id);
 9 
10     int updateNameById(int id,String name);
11 }

(4)mapper.xml

 1 <?xml version="1.0" encoding="UTF-8" ?>
 2 
 3 
 4 
 5     
 6     
 9 
10     
11         update book set name=#{name} where id=#{id}
12     
13 
14 

在浏览器中输入:http://localhost:8081/book/selectBookById 后,控制台将会输出如下:

 说明:第一条红线代表缓存命中率为0,因为一开始并没有开始缓存,拿不到。

 第二条红线代表查了一次数据库,且放入二级缓存中。

第三条红线代表查了二级缓存,因为加上第一次命中概率为0的那次,总共有两次查询缓存动作,但是只有第二次是命中缓存的,所以本次命中概率为0.5。

(5)增删改操作会清空缓存

修改Service实现,增加一个更新操作:

public Book selectBookById() {
        int id=1;
        Book book1=bookMapper.selectBookById(id);
        System.out.println(book1);
        Book book2=bookMapper.selectBookById(id);
        System.out.println(book2);
        String name="三国演义3";
        bookMapper.updateNameById(id,name);
        Book book3=bookMapper.selectBookById(id);
        System.out.println(book3);
        Book book4=bookMapper.selectBookById(id);
        System.out.println(book4);
        return book2;
    }

控制台为:

Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4005023d] was not registered for synchronization because synchronization is not active
Cache Hit Ratio [com.example.demo.dao.mapper.BookMapper]: 0.0
2020-04-06 16:24:51.528  INFO 13608 --- [nio-8081-exec-1] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2020-04-06 16:24:51.733  INFO 13608 --- [nio-8081-exec-1] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
JDBC Connection [HikariProxyConnection@787513428 wrapping com.mysql.cj.jdbc.ConnectionImpl@1c908c18] will not be managed by Spring
==>  Preparing: select id,name from book where id=? 
==> Parameters: 1(Integer)
<==    Columns: id, name
<==        Row: 1, 三国演义2
<==      Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4005023d]
1:三国演义2
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6178d2d1] was not registered for synchronization because synchronization is not active
Cache Hit Ratio [com.example.demo.dao.mapper.BookMapper]: 0.5
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6178d2d1]
1:三国演义2
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@75a05846] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@1391063673 wrapping com.mysql.cj.jdbc.ConnectionImpl@1c908c18] will not be managed by Spring
==>  Preparing: update book set name=? where id=? 
==> Parameters: 三国演义3(String), 1(Integer)
<==    Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@75a05846]
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@f2aa7cb] was not registered for synchronization because synchronization is not active
Cache Hit Ratio [com.example.demo.dao.mapper.BookMapper]: 0.3333333333333333
JDBC Connection [HikariProxyConnection@2025827328 wrapping com.mysql.cj.jdbc.ConnectionImpl@1c908c18] will not be managed by Spring
==>  Preparing: select id,name from book where id=? 
==> Parameters: 1(Integer)
<==    Columns: id, name
<==        Row: 1, 三国演义3
<==      Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@f2aa7cb]
1:三国演义3
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2f985f79] was not registered for synchronization because synchronization is not active
Cache Hit Ratio [com.example.demo.dao.mapper.BookMapper]: 0.5
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2f985f79]
1:三国演义3

 说明:上面共有7个标红的地方。从上到下的解释依次如下:

  • 第一条红色代表缓存还没开始,因此命中率为0;
  • 第二条红色代表开始写入二级缓存了,因为它往数据库查询结束,会往硬盘传值。
  • 第三条红色代表命中二级缓存,因为是第二次进行缓存命中,共2次,命中了1次,所以概率为0.5。
  • 第四条红色代表一条SQL语句,进行数据的更新,更新操作会清除缓存。
  • 第五条红色可以看到缓存命中率为0.333,因为更新操作清除了缓存,但是本次去查缓存并不知道,这是第3次查询缓存了,但是只有第2次命中,所以概率为0.33。
  • 第六条红色因为第五条红色没有找到缓存,被update操作清空,所以它就像数据库发起了select查询,查询结束后将查询结果写入到硬盘,作为二次缓存数据。
  • 第七条因为有最新的缓存数据了,可以进行缓存查询,且命中了。因为总共进行了4次缓存查询,只有第二次,第四次命中,因此本次概率为0.5.

 (6)禁用二级缓存


 7         select
 8         b.id bookId,
 9         b.name bookName,
10         sale.id "saleChannel.saleId",
11         sale.name "saleChannel.saleName"
12         from book b
13         inner join book_sale bs on b.id=bs.book_id
14         inner join salechannel sale on bs.sale_id=sale.id
15         where b.id=#{id}
16 
17     
18 
19 
20 
 1 <?xml version="1.0" encoding="UTF-8" ?>
 2 
 3 
 4 
 5     
 6 
 7 
 8     
 9         update salechannel set name=#{name} where id=#{id}
10     
11 
12 

浏览器中输入:http://localhost:8081/book/selectBookById 后,控制台输出如下:

Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6c7335f8] was not registered for synchronization because synchronization is not active
Cache Hit Ratio [com.example.demo.dao.mapper.BookMapper]: 0.0
2020-04-06 18:48:00.849  INFO 9652 --- [nio-8081-exec-3] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2020-04-06 18:48:00.931  INFO 9652 --- [nio-8081-exec-3] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
JDBC Connection [HikariProxyConnection@694526659 wrapping com.mysql.cj.jdbc.ConnectionImpl@a315a04] will not be managed by Spring
==>  Preparing: select b.id bookId, b.name bookName, sale.id "saleChannel.saleId", sale.name "saleChannel.saleName" from book b inner join book_sale bs on b.id=bs.book_id inner join salechannel sale on bs.sale_id=sale.id where b.id=? 
==> Parameters: 3(Integer)
<==    Columns: bookId, bookName, saleChannel.saleId, saleChannel.saleName
<==        Row: 3, 水浒传, 3, 淘宝
<==      Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6c7335f8]
bookId:3,bookName:水浒传,saleChannel的销售渠道为:淘宝
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4c883888] was not registered for synchronization because synchronization is not active
Cache Hit Ratio [com.example.demo.dao.mapper.BookMapper]: 0.5
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4c883888]
bookId:3,bookName:水浒传,saleChannel的销售渠道为:淘宝
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5a227dd8] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@1152655662 wrapping com.mysql.cj.jdbc.ConnectionImpl@a315a04] will not be managed by Spring
==>  Preparing: update salechannel set name=? where id=? 
==> Parameters: 当当(String), 3(Integer)
<==    Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5a227dd8]
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@349526fa] was not registered for synchronization because synchronization is not active
Cache Hit Ratio [com.example.demo.dao.mapper.BookMapper]: 0.6666666666666666
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@349526fa]
bookId:3,bookName:水浒传,saleChannel的销售渠道为:淘宝
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@79a8f236] was not registered for synchronization because synchronization is not active
Cache Hit Ratio [com.example.demo.dao.mapper.BookMapper]: 0.75
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@79a8f236]
bookId:3,bookName:水浒传,saleChannel的销售渠道为:淘宝

说明:查看上面结果,发现更新前和更新后查询的结果一样。说明缓存联表查询的时候出现了脏数据。

3. 祛除脏数据

该如何避免脏数据的出现呢?这时就需要用到参照缓存了。 当某几个表可以作为一个业务整体时,通常是让几个会关联的 ER 表同时使用同一个二级缓存,这样就能解决脏数据问题。

在上面那个例子中,将BookMapper.xml中的缓存配置改成参照缓存:



    
    

再次运行:

Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@42514c0e] was not registered for synchronization because synchronization is not active
Cache Hit Ratio [com.example.demo.dao.mapper.SaleChannelMapper]: 0.0
2020-04-06 18:51:31.685  INFO 14292 --- [nio-8081-exec-1] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2020-04-06 18:51:31.768  INFO 14292 --- [nio-8081-exec-1] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
JDBC Connection [HikariProxyConnection@1658628143 wrapping com.mysql.cj.jdbc.ConnectionImpl@b85bd7d] will not be managed by Spring
==>  Preparing: select b.id bookId, b.name bookName, sale.id "saleChannel.saleId", sale.name "saleChannel.saleName" from book b inner join book_sale bs on b.id=bs.book_id inner join salechannel sale on bs.sale_id=sale.id where b.id=? 
==> Parameters: 3(Integer)
<==    Columns: bookId, bookName, saleChannel.saleId, saleChannel.saleName
<==        Row: 3, 水浒传, 3, 淘宝
<==      Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@42514c0e]
bookId:3,bookName:水浒传,saleChannel的销售渠道为:淘宝
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1a584f9a] was not registered for synchronization because synchronization is not active
Cache Hit Ratio [com.example.demo.dao.mapper.SaleChannelMapper]: 0.5
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1a584f9a]
bookId:3,bookName:水浒传,saleChannel的销售渠道为:淘宝
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3bd2cdea] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@1663984021 wrapping com.mysql.cj.jdbc.ConnectionImpl@b85bd7d] will not be managed by Spring
==>  Preparing: update salechannel set name=? where id=? 
==> Parameters: 当当(String), 3(Integer)
<==    Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3bd2cdea]
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@534d3e7c] was not registered for synchronization because synchronization is not active
Cache Hit Ratio [com.example.demo.dao.mapper.SaleChannelMapper]: 0.3333333333333333
JDBC Connection [HikariProxyConnection@2128417890 wrapping com.mysql.cj.jdbc.ConnectionImpl@b85bd7d] will not be managed by Spring
==>  Preparing: select b.id bookId, b.name bookName, sale.id "saleChannel.saleId", sale.name "saleChannel.saleName" from book b inner join book_sale bs on b.id=bs.book_id inner join salechannel sale on bs.sale_id=sale.id where b.id=? 
==> Parameters: 3(Integer)
<==    Columns: bookId, bookName, saleChannel.saleId, saleChannel.saleName
<==        Row: 3, 水浒传, 3, 当当
<==      Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@534d3e7c]
bookId:3,bookName:水浒传,saleChannel的销售渠道为:当当
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1c85a76e] was not registered for synchronization because synchronization is not active
Cache Hit Ratio [com.example.demo.dao.mapper.SaleChannelMapper]: 0.5
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1c85a76e]
bookId:3,bookName:水浒传,saleChannel的销售渠道为:当当

说明:可以发现更新后,查询的数据变了,由淘宝变成当当啦。这个脏数据解决啦。