一、事务

1. 事务介绍

事务可以包含多个操作步骤 , 如果有一个步骤失败,那么这一组都以失败告终。

事务是指包含多个微小逻辑单元的一组操作, 只要其中有一个逻辑失败了,那么这一组操作就全部以失败告终,不存在一半成功,一半不成功的状况。

事务在平常的CRUD当中也许不太常用, 但是如果我们有一种需求,要求,一组操作中,必须全部成功执行,才算完成任务,只要有一个出错了,那么所有的任务都将回到最初的状况,恢复原样。那么这就可以使用事务了。如: 银行的转账例子 , 又如一次性往两张表添加记录,需要确保这两张表都能全部成功添加,不允许一张表成功,一张表失败这种情况出现。

2. 事务入门

1. 命令行演示

  • 方式一

img02

-- 开启事务
start transaction;

-- 执行操作 
update student set age = 28 where id = 6; 

-- 提交事务,只有提交事务,数据才会真的保存到底层设备上。
commit;

-- 如果不想提交,想回到最初的状态,那么可以回滚事务.
rollback; 
  • 方式二

mysql 的事务设置是自动提交,我们可以关闭掉自动提交开关,然后手动提交事务。

img02

-- 显示有关提交的变量信息
show variables like '%commit%';

-- 关闭自动提交
set autocommit = off  ;  或者写成  set autocommit = 0 ; 

-- 执行操作,可以不用开启事务
update student set age = 28 where id = 6; 

-- 必须提交,才能看到结果
commit;

2. 代码演示

 @Test
    public void testTransaction(){
        Connection conn = null;
        try {
            ComboPooledDataSource dataSource = new ComboPooledDataSource();

            conn = dataSource.getConnection();

            //开启事务 关闭自动提交,
            conn.setAutoCommit(false);

            //3. 执行语句
            String sql = "insert into student values(null ,?,?)";
            PreparedStatement ps = conn.prepareStatement(sql);


            ps.setString(1,"lisi2");
            ps.setInt(2,19);

            ps.executeUpdate();
            
            //提交事务
            conn.commit();


            //释放资源..回收连接对象
            ps.close();
            conn.close();

        } catch (Exception e) {
            e.printStackTrace();
            
            //如果出现了异常,那么回滚事务
            try {
                conn.rollback();
            } catch (SQLException e1) {
                e1.printStackTrace();
            }
        }

    }

3. 事务特性 ACID

  • 原子性

原子性(Atomicity) : 事务中的逻辑要全部执行,不可分割。(原子是物理中最小单位)

  • 一致性

一致性(Consistency):事务执行的结果必须是使数据库数据从一个一致性状态变到另外一种一致性状态

  • 隔离性(isolation)

一个事务的执行过程中不能影响到其他事务的执行,即一个事务内部的操作及使用的数据对其他事务是隔离的,并发执行各个事务之间无不干扰。

  • 持久性()

即一个事务一旦提交,它对数据库数据的改变是永久性的。之后的其它操作不应该对其执行结果有任何影响。

4. 事务隔离级别

主要是用来解决事务并发执行,引发的问题。

如果两个事务同时,或者交错执行,那么他们的执行结果可能会受对方影响,这会导致数据的前后显示不一致。所以为了保证并发操作数据的正确性及一致性,SQL规范于1992年提出了数据库事务隔离级别

事务的并发主要有两个方面的问题 : 读的问题 | 写的问题 , 相对于写的问题,读的问题出现的几率更高些。

1. 安全隐患

如果事务没有任何隔离设置,那么在并发情况会出现以下问题。

a. 读的问题
  • 脏读

脏读: 指 一个事务 读到了另一个事务还未提交的数据 :读已提交

  • 不可重复读

一个事务读到了另一个事务提交的更新的数据, 导致多次查询结果不一致。(针对update)

  • 虚读|幻读

一个事务读到了另一个事务已提交的插入的数据,导致多次查询结果不一致。(针对 insert)

b. 写的问题

写的问题其实就只有一个,就是丢失更新

丢失更新:指一个事务去修改数据库, 另一个事务也修改数据库,最后的那个事务,不管是提交还是回滚都会造成前面一个事务的数据更新丢失

  • 解决办法:

    1. 悲观锁

    还没干活就想着会丢失更新。 查询数据的后面跟上关键字 for update

    1. 乐观锁

    程序员自己控制,在表里面增加一个字段version (版本的意思), 默认是0 。 只要有一次操作,这个版本就递增。下次来操作,先对版本号,如果对不上,表示是旧的数据,得先查询。

2. 隔离级别

a. 读未提交
  1. 打开两个dos终端, 设置A窗口的隔离级别为 读未提交

img03

  1. 两个窗口都分别开启事务

img03

读未提交: 一个事务可以读取到另一个事务没有提交的数据

待解决: 3个(脏读、不可重复读、虚读)

解决:0个

b. 读已提交
  1. 设置A窗口的隔离级别为 读已提交

icon

  1. A B 两个窗口都开启事务, 在B窗口执行更新操作。

icon

  1. 在A窗口执行的查询结果不一致。 一次是在B窗口提交事务之前,一次是在B窗口提交事务之后。

icon

读已提交:一个事务能读取到另一个事务已经提交的数据,但是未提交的数据读取不到了。

解决: 脏读

待解决: 不可重复 , 虚读

c. 可重复读

Repeatable Read set session transaction isolation level repeatable read;

解决: 脏读 、不可重复读

待解决: 虚读(但是其实mysql已经在底层把这个问题给避免了。)

d. 序列化 | 串行化

serializable set session transaction isolation level serializable;

  • 按功能大小 排序 : 序列化 > 可重复读 > 读已提交 > 读未提交
  • 按效率来排序: 序列化 < 可重复读 < 读已提交 < 读未提交

MySql: 默认隔离级别: 可重复读

Oracle: 读已提交

5. 事务管理

该小节讲述的是在三层结构里,如何优雅的声明事务和管理事务。

1. 三层结构介绍

三层结构其实是指把项目代码按照不同的逻辑、功能划分出来的层级,包含 控制层、业务逻辑层 、 数据访问层 ,对应着我们平常见到的 controller | service | dao 。 每个层级有各自的功能作用,

controller : 负责接收处理页面请求 和 响应

service : 负责处理业务逻辑(如: 数据封装、逻辑判断等)

dao: 负责和数据库打交道(增删改查)

  • 此处以用户注册来演示三层代码结构

    • controller
    public class UserController {
        private static final String TAG = "UserController";
    
        @PostMapping("/users")
        public String register(User user){
            try {
                UserService userService = new UserServiceImpl();
                userService.register(user);
            } catch (SQLException e) {
                e.printStackTrace();
            }
            return "注册成功";
        }
    }
    
    • service
    public interface UserService {
        void register(User user) throws SQLException;
    }
    
    ------------------下面是具体的实现----------------------
    public class UserServiceImpl implements UserService {
        @Override
        public void register(User user) throws SQLException {
            UserDao userDao = new UserDaoImpl();
    
            //可以在这里对注册的密码进行加密, 逻辑上的校验
            userDao.save(user);
        }
    }
    
    • dao
    public interface UserDao {
        void save(User user) throws SQLException;
    }
    
    --------------------下面是具体的实现----------------------------
    public class UserDaoImpl implements UserDao {
        private static final String TAG = "UserDaoImpl";
    
        @Override
        public void save(User user) throws SQLException {
    
            QueryRunner runner = new QueryRunner(C3P0Util.getDataSource());
            String sql = "insert into user values(null , ? ,? ,?,?)";
            runner.update(sql , 
                          user.getUsername() , 
                          user.getPassword(),
                          user.getEmail(),
                          user.getPhone());
        }
    }
    
    

2. 三层结构中的事务管理

上面已经给大家演示过了未来写代码的一个雏形, 那么在这种三层体系中,我们该如何管理事务呢? 要想解答这个疑问,首先得想想,有关事务的代码应该在哪一层编写?

  • 事务应该位于哪一个层级?

事务操作应该在service层中实现, controller是用于接收和响应请求的,至于这个请求中间是怎么运作的,它其实是不关心的。 那么为什么不能是dao层呢? 因为dao层的每一个动作,仅仅是表示和数据库的一次交互而已,如果我们一个请求需要完成两个或者两个以上的数据库操作,那么在dao层里面事务就无法处理了。反之,service层主要适用于表示业务,那么这个业务可以调用调用一次dao层的方法,也可以调用多次方法,它们都可以看成是一个整体,不可分割。那么在service层囊括起来正好。 这有一个必须要注意的点: 要求service层和dao层使用的连接对象是同一个。

img13

a. 传递Connection

上面已经说明了,需要在service层开启事务,那么如何确保dao层使用的连接对象和开启事务的连接对象是同一个,可以通过参数传递connection 对象

  • service
public class UserServiceImpl implement UserService{
    
    /**
     * 转账
     */
    public void transfer()  {

        Connection conn = null;
        try {
            //1. 获取连接对象
           conn = C3P0Util.getConn();

            //2. 开启事务,其实就是关闭自动提交
            conn.setAutoCommit(false);

            //3. 调用dao层方法
            UserDao userDao = new UserDaoImpl();

            userDao.outMoney(conn,"zhangsan",100);
            userDao.inMoney(conn,"lisi",100);

            //4. 提交事务
            conn.commit();
        } catch (SQLException e) {
            e.printStackTrace();

            //5. 如果有异常,那么回滚事务
            try {
                conn.rollback();
            } catch (SQLException e1) {
                e1.printStackTrace();
            }
        }

    }
}
  • dao

public  class UserDaoImpl implement UserDao{
    //转入
    @Override
    public void inMoney(Connection conn , String name, int money) throws SQLException {
        QueryRunner runner = new QueryRunner();
        String sql = "update user set money = money + ? where name = ?";
        runner.update(conn , sql , money , name);

    }

    //转出
    @Override
    public void outMoney(Connection conn , String name, int money) throws SQLException {
        QueryRunner runner = new QueryRunner();
        String sql = "update user set money = money - ? where name = ?";
        runner.update(conn , sql , money , name);
    }
}
b. 使用ThreadLocal

前面使用参数形式来传递Conenction,也不是不行。但是如果想让代码写得更加优雅,那么还是得使用ThreadLocal 。 ThreadLocal 直译过来的意思是: 线程本地。她可以用来存储数据,供当前的线程调用,底层其实使用Map集合来存数据。map的Key 是当前现成对象, value 值就是我们存储的数据了。ThreadLocal 不能用来做线程间通讯或者共享内容 ,它所存的值只针对当前线程有效。

上面的例子,从service开始,直到两个dao方法的执行完毕,都是在一个线程内工作,那么我们可以在service层就把connection对象存储到ThradLocal里面,在dao层取出来使用即可。

一句话概括这个ThreadLocal的作用: 就是可以往里面存东西,然后可以在整个线程里面取出来。

  • C3P0Util

    private static  ThreadLocal<Connection> threadLocal  = new ThreadLocal<Connection>();

    public static Connection getConnTL(){
        try {

            Connection conn = threadLocal.get();
            if(conn == null){
                conn = dataSource.getConnection();
                threadLocal.set(conn);
            }
            //2. 获取连接
            return conn;
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return null;
    }
  • service
/**
     * 转账
     */
    public void transfer()  {

        Connection conn = null;
        try {
            //1. 获取连接对象  , 当第一次调用该方法的时候,Connction已经存储到ThreadLocal中去了。
           conn = C3P0Util.getConnTL();

            //2. 开启事务,其实就是关闭自动提交
            conn.setAutoCommit(false);

            //3. 调用dao层方法
            UserDao userDao = new UserDaoImpl();

            userDao.outMoney("zhangsan",100);
            userDao.inMoney("lisi",100);

            //4. 提交事务
            conn.commit();
        } catch (SQLException e) {
            e.printStackTrace();

            //5. 如果有异常,那么回滚事务
            try {
                conn.rollback();
            } catch (SQLException e1) {
                e1.printStackTrace();
            }
        }

    }
  • dao
  //转入
    @Override
    public void inMoney( String name, int money) throws SQLException {
        QueryRunner runner = new QueryRunner();
        String sql = "update user set money = money + ? where name = ?";
        runner.update(C3P0Util.getConnTL() , sql , money , name);
    }

    //转出
    @Override
    public void outMoney( String name, int money) throws SQLException {
        QueryRunner runner = new QueryRunner();
        String sql = "update user set money = money - ? where name = ?";
        runner.update(C3P0Util.getConnTL() , sql , money , name);
    }

二、H2数据库

1. H2介绍

h2是一个开源的纯java编写的轻量级数据库,是一个用Java开发的嵌入式数据库,只有一个jar文件,可以直接嵌入到应用项目中。
h2最大的用途在于可以同应用程序打包在一起发布,这样可以非常方便地存储少量结构化数据,它的另一个用途是用于单元测试。
启动速度快,而且可以关闭持久化功能,每一个用例执行完随即还原到初始状态。h2的第三个用处是作为缓存,作为NoSQL的一个补充。

  • 产品优势
    • 纯Java编写,不受平台的限制;
    • 只有一个jar文件,适合作为嵌入式数据库使用;
    • h2提供了一个十分方便的web控制台用于操作和管理数据库内容;
    • 功能完整,支持标准SQL和JDBC。麻雀虽小五脏俱全;
    • 支持内嵌模式、服务器模式和集群。

2. 下载 安装

  • 地址

http://www.h2database.com/html/download.html

  • 目录介绍
 h2
  |---bin
  |    |---h2-1.1.116.jar   //H2数据库的jar包(驱动也在里面)
  |    |---h2.bat              //Windows控制台启动脚本
  |    |---h2.sh                  //Linux控制台启动脚本
  |    |---h2w.bat              //Windows控制台启动脚本(不带黑屏窗口)
  |---docs                       //H2数据库的帮助文档(内有H2数据库的使用手册)
  |---service //通过wrapper包装成服务。
  |---src //H2数据库的源代码
  |---build.bat //windows构建脚本
  |---build.sh //linux构建脚本
  • 启动连接

3. H2运行模式

  1. 内嵌模式

内嵌模式下,应用和数据库同在一个JVM中,通过JDBC进行连接。 可持久化,但同时只能一个客户端连接。内嵌模式性能会比较好

img14

  1. 服务器模式

使用服务器模式和内嵌模式一样,只不过它可以跑在另一个进程里。

img14

  1. 混合模式

第一个应用以内嵌模式启动它,对于后面的应用来说它是服务器模式跑着的

img14

3. java 操作

  • 嵌入式连接
 Connection conn = DriverManager.getConnection("jdbc:h2:D:/aa/test", "sa", "");
  • 远程连接
 Connection conn = DriverManager.
       getConnection("jdbc:h2:tcp://localhost/D:/aa/test", "sa", "");
  • 内存数据库
   Connection conn = DriverManager.
       getConnection("jdbc:h2:tcp://localhost/mem:test2", "sa", "");

https://www.jianshu.com/p/2953c64761aa MySql 避免幻读