事务与Mybatis一级缓存


1、事务基础知识

1、1 事务定义

  • 数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成
  • 事务的核心就是锁与并发控制

    1、2 数据库隔离级别

  • 未提交读:事务中的修改即使没有提交也对其他事务可见。
  • 提交读:事务内只能看到其他已经提交的事务结果。
  • 可重复读:只允许读取已经提交的数据,而且在一个事务两次读取一个数据项期间,其它事务不得更新该数据。
  • 序列读:最高的隔离级别,事务串行执行

1、3 事务总结

事务单元操作类型

  • 读读
  • 读写
  • 写读
  • 写写

事务并发问题

  • 更新丢失:当两个不同的事务试图同时更新数据库中同一行上的同一列时,会发生丢失更新
  • 脏读:即事务可以读取未提交的数据,写与读不互斥。
  • 不可重复读:即同一个事务多次查询得到不一致的结果,读写不互斥
  • 幻读:当某个事物在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行

事务特性关系

  • 隔离性与一致性:是一致性与并发之间的杠杆,隔离级别增加,一致性增加,并发性能降低,隔离线打破了一致性。
  • 原子性与一致性:两者没有必然联系,原子性保证的事务操作要么全做、要么全不做,对每个操作按时间做记录,以便回滚操作,一致性保持的数据状态变更,中间状态对外部不可见。

事务模型

  • 本地事务模型
    • (面向连接,不是由框架或者容器进行管理)
  • 编程事务模型
    • (从事务管理器中获取事务,之后需要自己编写事务启动、提交、异常及回滚代码)
  • 声明事务模型
    • (容器管理事务,开发者定义(声明)事务的行为和参数)

2、Mybatis

2、1 整体架构

2、2 项目结构

.
└── org
    └── apache
        └── ibatis
            ├── annotations
            ├── binding
            ├── builder
            ├── cache
            ├── cursor
            ├── datasource
            ├── exceptions
            ├── executor
            ├── io
            ├── jdbc
            ├── logging
            ├── mapping
            ├── parsing
            ├── plugin
            ├── reflection
            ├── scripting
            ├── session
            ├── transaction
            └── type

2、3 一级缓存配置介绍

    <settings>
        <setting name="localCacheScope" value="SESSION"/>//一级缓存级别为SESSION、STEMENT 
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        <setting name="logImpl" value="LOG4J"/>
    </settings>

2、4 一级缓存工作流程与原理

2、4、1 一级缓存类图

  • SqlSession
    • 1)mybatis中主要接口,该接口主要负责执行SQL并封装返回结果,可以获取mapper、管理事务;
    • 2)对外提供了用户和数据库之间交互需要的所有方法,隐藏了底层的细节,默认实现类是DefaultSqlSession
  • Executor
    • 1)每个Session都会持有一个执行器,数据库的CRUD操作都是通过执行器进行
    • 2)该接口有两个实现类一个是BaseExecutor、CachingExecutor;其中BaseExcutor有三个继承类SimpleExecutor(普通执行器)、ReuseExecutor(重用预处理执行器)、BatchExecutor(重用语句批量执行器)
    • CachingExecutor来装饰前面的三个执行器目的就是用来实现缓存
  • MappedStatement
    • MappedStatement就是用来存放我们SQL映射文件中的信息包括sql语句,输入参数,输出参数等等,一个SQL节点对应一个MappedStatement对象。

2、4、2 案例实践

案例1

    SqlSession sqlSession = factory.openSession(true); // 自动提交事务
    StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
    System.out.println(studentMapper.getStudentById(1));
    System.out.println(studentMapper.getStudentById(1));
    System.out.println(studentMapper.getStudentById(1));
    sqlSession.close();
    DEBUG [main] - Cache Hit Ratio [com.jd.jr.finetch.study.mybatis.mapper.StudentMapper]: 0.0
    DEBUG [main] - ==>  Preparing: SELECT id,name,age FROM student WHERE id = ? 
    DEBUG [main] - ==> Parameters: 1(Integer)
    TRACE [main] - <==    Columns: id, name, age
    TRACE [main] - <==        Row: 1, 小明, 16
    DEBUG [main] - <==      Total: 1
    StudentEntity{id=1, name='小明', age=16, className='null'}
    DEBUG [main] - Cache Hit Ratio [com.jd.jr.finetch.study.mybatis.mapper.StudentMapper]: 0.0
    StudentEntity{id=1, name='小明', age=16, className='null'}
    DEBUG [main] - Cache Hit Ratio [com.jd.jr.finetch.study.mybatis.mapper.StudentMapper]: 0.0
    StudentEntity{id=1, name='小明', age=16, className='null'}
  • 同一个Session,第一个打印数据查询数据库,第二、三个查询走缓存
  • 思考自动提交事务底层实现原理?

案例2

    SqlSession sqlSession = factory.openSession(true); // 自动提交事务
    StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
    System.out.println(studentMapper.getStudentById(1));
    System.out.println("增加了" + studentMapper.addStudent(buildStudent()) + "个学生");
    System.out.println(studentMapper.getStudentById(1));

    sqlSession.close();
    DEBUG [main] - Cache Hit Ratio [com.jd.jr.finetch.study.mybatis.mapper.StudentMapper]: 0.0
    DEBUG [main] - ==>  Preparing: SELECT id,name,age FROM student WHERE id = ? 
    DEBUG [main] - ==> Parameters: 1(Integer)
    TRACE [main] - <==    Columns: id, name, age
    TRACE [main] - <==        Row: 1, 小明, 16
    DEBUG [main] - <==      Total: 1
    StudentEntity{id=1, name='小明', age=16, className='null'}
    DEBUG [main] - ==>  Preparing: INSERT INTO student(name,age) VALUES(?, ?) 
    DEBUG [main] - ==> Parameters: 张三疯(String), 20(Integer)
    DEBUG [main] - <==    Updates: 1
    增加了1个学生
    DEBUG [main] - Cache Hit Ratio [com.jd.jr.finetch.study.mybatis.mapper.StudentMapper]: 0.0
    DEBUG [main] - ==>  Preparing: SELECT id,name,age FROM student WHERE id = ? 
    DEBUG [main] - ==> Parameters: 1(Integer)
    TRACE [main] - <==    Columns: id, name, age
    TRACE [main] - <==        Row: 1, 小明, 16
    DEBUG [main] - <==      Total: 1
    StudentEntity{id=1, name='小明', age=16, className='null'}
  • 同一个Session,第一个查询走数据库
  • 先做增加再做查询,直接查询数据库
  • 思考有insert会清零缓存,如何清理?

案例3

    SqlSession sqlSession1 = factory.openSession(true); // 自动提交事务
    SqlSession sqlSession2 = factory.openSession(true); // 自动提交事务

    StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
    StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);

    System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1));
    System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1));
    System.out.println("studentMapper2更新了" + studentMapper2.updateStudentName("李四",1) + "个学生的数据");
    System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1));
    System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1));

    DEBUG [main] - Cache Hit Ratio [com.jd.jr.finetch.study.mybatis.mapper.StudentMapper]: 0.0
    DEBUG [main] - ==>  Preparing: SELECT id,name,age FROM student WHERE id = ? 
    DEBUG [main] - ==> Parameters: 1(Integer)
    TRACE [main] - <==    Columns: id, name, age
    TRACE [main] - <==        Row: 1, 小明, 16
    DEBUG [main] - <==      Total: 1
    studentMapper读取数据: StudentEntity{id=1, name='小明', age=16, className='null'}
    DEBUG [main] - Cache Hit Ratio [com.jd.jr.finetch.study.mybatis.mapper.StudentMapper]: 0.0
    studentMapper读取数据: StudentEntity{id=1, name='小明', age=16, className='null'}
    DEBUG [main] - ==>  Preparing: UPDATE student SET name = ? WHERE id = ? 
    DEBUG [main] - ==> Parameters: 李四(String), 1(Integer)
    DEBUG [main] - <==    Updates: 1
    studentMapper2更新了1个学生的数据
    DEBUG [main] - Cache Hit Ratio [com.jd.jr.finetch.study.mybatis.mapper.StudentMapper]: 0.0
    studentMapper读取数据: StudentEntity{id=1, name='小明', age=16, className='null'}
    DEBUG [main] - Cache Hit Ratio [com.jd.jr.finetch.study.mybatis.mapper.StudentMapper]: 0.0
    DEBUG [main] - ==>  Preparing: SELECT id,name,age FROM student WHERE id = ? 
    DEBUG [main] - ==> Parameters: 1(Integer)
    TRACE [main] - <==    Columns: id, name, age
    TRACE [main] - <==        Row: 1, 李四, 16
    DEBUG [main] - <==      Total: 1
    studentMapper2读取数据: StudentEntity{id=1, name='李四', age=16, className='null'}

  • 分别创建两个Session
  • Session1 第一次查询数据直接走数据库,第二次查询数据走缓存
  • Session2 更新数据,更新ID为1的学生的姓名
  • Session1 第三次读取数据,读取的数据还是缓存的数据
  • Session2 查询数据,读取的是更新过姓名的数据
  • 缓存在Session隔离,相互不影响,此案例出现脏数据

2、4、3一级缓存工作流程

2、5 一级缓存总结

  • 1、一级缓存支持两种级别,分别是SESSION、STATEMENT;STATEMENT相当于不使用一级缓存,默认是SESSION级别;
  • 2、一级缓存设计比较简单,针对缓存的功能比较弱,只是简单的用Map来存储,它的生命周期同SQL Session

    2、6 二级缓存配置介绍

    <settings>
        <setting name="localCacheScope" value="SESSION"/>//一级缓存级别为SESSION、STEMENT
        <setting name="cacheEnabled" value="true"/>//二级缓存
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        <setting name="logImpl" value="LOG4J"/>
    </settings>
  

案例1

    SqlSession sqlSession1 = factory.openSession(true); // 自动提交事务
    SqlSession sqlSession2 = factory.openSession(true); // 自动提交事务
    SqlSession sqlSession3 = factory.openSession(true); // 自动提交事务

    StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
    StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
    StudentMapper studentMapper3 = sqlSession3.getMapper(StudentMapper.class);

    System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1));
    sqlSession1.close();

    System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1));

    studentMapper3.updateStudentName("方方",1);
    sqlSession3.commit();
    System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1));

执行结果

    DEBUG [main] - Cache Hit Ratio [com.jd.jr.finetch.study.mybatis.mapper.StudentMapper]: 0.0
    DEBUG [main] - ==>  Preparing: SELECT id,name,age FROM student WHERE id = ? 
    DEBUG [main] - ==> Parameters: 1(Integer)
    TRACE [main] - <==    Columns: id, name, age
    TRACE [main] - <==        Row: 1, 方方, 16
    DEBUG [main] - <==      Total: 1
    studentMapper读取数据: StudentEntity{id=1, name='方方', age=16, className='null'}
    DEBUG [main] - Cache Hit Ratio [com.jd.jr.finetch.study.mybatis.mapper.StudentMapper]: 0.5
    studentMapper2读取数据: StudentEntity{id=1, name='方方', age=16, className='null'}
    DEBUG [main] - ==>  Preparing: UPDATE student SET name = ? WHERE id = ? 
    DEBUG [main] - ==> Parameters: 方方(String), 1(Integer)
    DEBUG [main] - <==    Updates: 1
    DEBUG [main] - Cache Hit Ratio [com.jd.jr.finetch.study.mybatis.mapper.StudentMapper]: 0.3333333333333333
    DEBUG [main] - ==>  Preparing: SELECT id,name,age FROM student WHERE id = ? 
    DEBUG [main] - ==> Parameters: 1(Integer)
    TRACE [main] - <==    Columns: id, name, age
    TRACE [main] - <==        Row: 1, 方方, 16
    DEBUG [main] - <==      Total: 1
    studentMapper2读取数据: StudentEntity{id=1, name='方方', age=16, className='null'}
  • 分别创建三个Session
  • Session1查询学生Mapper ID为1的数据,首次查询所以走数据库,并且查完数据手动关闭连接
  • Session2查询学生Mapper ID为1的数据,首次查询但是可以看出没有走数据库,为什么?Session隔离的

2、7 二级缓存工作流程与原理

2、8 二级缓存总结

3、Spring事务

3、1 Spring事务管理接口介绍

3、2 Spring声明事务案例与最佳实践

3、3 Spring事务执行原理

参考资料

  • http://www.mybatis.org/mybatis-3/zh
  • https://docs.spring.io/spring/docs/3.0.x/spring-framework-reference/html/transaction.html
  • https://github.com/mybatis/mybatis-3


Comments