1 事务概述

事务的概述.png

2 转账案例的实现

2.1 mysql操作事务(了解)

sqlyog会让事务失效,所以试验的时候得用控制台。

sql语句 描述
start transaction; 开启事务
commit; 提交事务
rollback; 回滚事务

mysql事务是默认自动提交。
扩展:Oracle数据库事务不自动提交。

2.2 jdbc操作事务

  1. import java.sql.Connection;
  2. import java.sql.SQLException;
  3. import java.sql.Statement;
  4. import cn.itcast.c3p0utils.C3P0Utils;
  5. /*
  6. 使用jdbc完成转账案例。
  7. 我们要把加钱和减钱看成是同一组操作,这组操作要么都成功,要么都失败。
  8. 可以使用事务去解决这个问题。
  9. 步骤:
  10. 转账之前开启事务。
  11. 如果整个过程没有出现问题,那么就提交事务。
  12. 如果整个过程出现了问题,那么就回滚。
  13. 如果使用jdbc操作事务,需要使用连接(Connection)对象去操作事务。
  14. void setAutoCommit(boolean autoCommit):如果传递false,就是设置事务手动提交。也就是开启了事务。
  15. void commit():提交事务
  16. void rollback(): 回滚事物。
  17. */
  18. public class Demo01JDBCTransaction {
  19. public static void main(String[] args) {
  20. Connection con = null;
  21. try {
  22. // 使用工具类获取一个连接
  23. con = C3P0Utils.getConnection();
  24. //通过连接获取到sql语句执行者对象
  25. Statement st = con.createStatement();
  26. //在sql语句执行前开启事务,保证后面的所有的sql语句都是同一组操作。
  27. con.setAutoCommit(false);//false表示手动提交事务,也就是开启了事务
  28. //执行sql语句
  29. //先给jack的钱减去100
  30. int row1 = st.executeUpdate("UPDATE account SET money=money-100 WHERE name='jack'");
  31. System.out.println("row1:" + row1);
  32. //模拟停电
  33. System.out.println(10 / 0);
  34. //给rose的钱加上100
  35. int row2 = st.executeUpdate("UPDATE account SET money=money+100 WHERE name='rose'");
  36. System.out.println("row2: " + row2);
  37. //如果没有问题,就提交事务
  38. con.commit();
  39. System.out.println("转账成功");
  40. } catch (Exception e) {
  41. //如果转账过程出现问题,catch会捕获到这个异常,那么我们就在catch回滚事物
  42. try {
  43. if(con != null) {//非空判断,让程序更加的健壮(鲁棒)
  44. con.rollback();
  45. System.out.println("转账失败");
  46. }
  47. } catch (SQLException e1) {
  48. e1.printStackTrace();
  49. }
  50. }
  51. }
  52. }

2.3 dbutils操作事务

import java.sql.Connection;
import java.sql.SQLException;

import org.apache.commons.dbutils.QueryRunner;

import cn.itcast.c3p0utils.C3P0Utils;

/*
    使用DBUtils完成转账案例。

    需要把加钱和减钱看成一组操作,要么都成功要么都失败。
    还得用事务。

    DBUtils操作事务也是通过连接(Connection)对象去操作。
    DBUtils里面的QueryRunner在创建对象的时候一定要使用空参构造去创建对象。(因为另一种拿不到连接,没有连接就操作不了事务)

    开启事物,提交事务,回滚事物和jdbc的操作是一样的。

    步骤:
        1. 创建QueryRunner对象(使用空参构造创建对象)
        2. 获取到一个连接。
        3. 使用这个连接开启事务。
        4. 执行转账的sql语句。
        5. 如果整个过程没有遇到问题,那么就提交。
                如果整个过程遇到了问题,那么就回滚。

 */
public class Demo01DBUtilsTransaction {
    public static void main(String[] args) {
        Connection con = null;
        try {
            //创建QueryRunner对象(使用空参构造创建对象)
            QueryRunner qr = new QueryRunner();
            //获取到一个连接。
            con = C3P0Utils.getConnection();
            //使用这个连接开启事务。
            con.setAutoCommit(false);
            //执行转账的sql语句。
            String sqlA = "UPDATE account SET money=money-100 WHERE name='jack'";
            qr.update(con, sqlA);

            //模拟停电
            //System.out.println(10 / 0);

            String sqlB = "UPDATE account SET money=money+100 WHERE name='rose'";
            qr.update(con, sqlB);

            //如果整个过程没有遇到问题,那么就提交。
            con.commit();
            System.out.println("转账成功");
        } catch (Exception e) {
            //如果遇到问题,就回滚
            try {
                if(con != null) {
                    con.rollback();
                }
            } catch (SQLException e1) {
                e1.printStackTrace();
            }
            System.out.println("转账失败");//也可以把提示放在这个位置。

            //如果需要调试错误,建议加上下面代码
            //e.printStackTrace();// 将异常信息使用标准的错误流打印到控制台(由于这个代码异常消耗性能,所以在调试阶段一般用这个直接显示出来,等到运行阶段,要把他换成日志。)
        }

    }
}

3 三层架构介绍

三层架构.png

4 转账案例用三层架构模式改写

4.1 分析

转账案例三层版.png

4.2 准备工作

数据库准备

# 创建一个表:账户表.
create database webdb;
# 使用数据库
use webdb;
# 创建账号表
create table account(
    id int primary key auto_increment,
    name varchar(20),
    money double
);
# 初始化数据
insert into account values (null,'jack',10000);
insert into account values (null,'rose',10000);
insert into account values (null,'tom',10000);

java准备
QQ截图20210323100026.png

4.3 dao层的实现

package cn.myl021.dao;

import java.sql.Connection;
import java.sql.SQLException;

import org.apache.commons.dbutils.QueryRunner;

import cn.myl021.utils.C3P0Utils;

/*
    转账案例dao层。
    这一层我们要对数据库进行增删改查。
    这一层被service层调用。

    给出两个方法:
        出账方法
        入账方法

    在一组事务操作中,必须要使用的是同一个连接。
    可以使用参数的形式把连接传递过来。
    在Service层获取连接,然后Service调用dao的时候把连接传递过来,dao层就用Service传递过来的连接就可以了
 */
public class AccountDao {

    /*
     * 出账方法
     * 参数: 出账人姓名, 金额
     * 返回值: 不需要
     */
    public void outMoney(Connection con, String name, double money) {
        try {
            //创建一个QueryRunner对象
            QueryRunner qr = new QueryRunner();
            //执行sql
            String sql = "UPDATE account SET money=money-? WHERE name=?";
            Object[] params = {money, name};
            qr.update(con, sql, params);
        } catch (Exception e) {
            //手动抛出异常
            throw new RuntimeException("出账失败");
        }
    }

    /*
     * 入账方法
     * 参数: 入账人姓名, 金额
     * 返回值: 不需要
     */
    public void inMoney(Connection con, String name, double money) {
        try {
            //创建一个QueryRunner对象
            QueryRunner qr = new QueryRunner();
            //执行sql
            String sql = "UPDATE account SET money=money+? WHERE name=?";
            Object[] params = {money, name};
            qr.update(con, sql, params);
        } catch (Exception e) {
            //手动抛出异常
            throw new RuntimeException("入账失败");
        }
    }
}

4.4 service层的实现

package cn.myl021.service;

import java.sql.Connection;
import java.sql.SQLException;

import cn.myl021.dao.AccountDao;
import cn.myl021.utils.C3P0Utils;

/*
    转账案例Service层。
    这一层用来处理业务上的数据。
    这一层会调用dao层,被view层

    定义一个转账方法步骤:
        1. 获取连接,然后开启事务。
        2. 给出账人减去对应的金额(调用dao层)
        3. 给入账人加上对应的金额(调用dao层)
        4. 如果整个过程没有问题,就提交。
           如果真个过程出现问题,那么回滚
 */
public class AccountService {
    /*
     * 转账方法。
     * 参数: 出账人姓名, 入账人姓名, 金额。
     * 返回值:void
     */
    public void transfer(String outName, String inName, double money) {
        Connection con = null;
        try {
            //获取连接,然后开启事务。
            con = C3P0Utils.getConnection();
            con.setAutoCommit(false);

            //给出账人减去对应的金额(调用dao层)
            AccountDao dao = new AccountDao();
            dao.outMoney(con, outName, money);

            //模拟错误
            //System.out.println(10/0);

            //给入账人加上对应的金额(调用dao层)
            dao.inMoney(con, inName, money);

            //如果整个过程没有问题,就提交。
            con.commit();
        } catch (Exception e) {
            //回滚事务
            try {
                if(con != null) {
                    con.rollback();
                }
            } catch (SQLException e1) {
                e1.printStackTrace();
            }

            //如果转账失败,那么就手动抛出一个异常,告诉调用者我出现问题了。
            throw new RuntimeException(e.getMessage());
        }

    }
}

4.5 view层的实现

package cn.myl021.view;

import cn.myl021.service.AccountService;

/*
    转账案例的view
    用来给用户展示,或者让用户录入信息。


    可以让用户录入入账人姓名,出账人姓名, 金额,然后调用Service层把这些数据传递过去,然后Service层进行一个转账处理。
 */
public class AccontView {
    public static void main(String[] args) {
        String outName = "jack";
        String inName = "rose";
        double money = 100;

        //调用Service层进行转账操作
        AccountService service = new AccountService();
        try {
            service.transfer(outName, inName, money);
            System.out.println("转账成功");
        } catch (Exception e) {
            System.out.println("转账失败");
            System.out.println(e.getMessage());
        }
    }
}

5 ThreadLocal介绍

ThreadLocal.png

/*
    ThreadLocal 叫做线程局部变量。

    作用:可以向当前线程上面绑定数据, 然后在当前线程的所有位置共享。

    void set(T value):将数据添加(绑定)到当前线程上面,可以在当前线程的所有位置共享。
    T get():获取当前线程上面绑定的数据。 这个方法是通过哪个线程调用的,获取的就是哪个线程上的数据
    void remove():移除当前线程上绑定的数据。

    一个ThreadLocal对象只能在一个线程上面绑定一个数据。 如果要绑定多个数据,需要创建多个ThreadLocal对象。

    在ThreadLocal数据使用完之后要remove掉。否则会引发内存泄漏。因为ThreadLocal内部结构非常复杂,内部是map集合,叫ThreadLocalMap,key是当前的ThreadLocal对象value是线程上面绑定的数据。而ThreadLocalMap里面是一个Entry[]数组,这个Entry是他的一个内部类,继承了WeakRefence是弱引用(比如平常的Student s = new Student(); 是强引用,只要这个对象被强引用关联着,他就永远不会被回收,java中的四种引用的回收时机是不一样的。)

 */
public class Demo01ThreadLocal {
    public static void main(String[] args) {
        //创建ThreadLocal对象
        final ThreadLocal<String> local = new ThreadLocal<>();  //加final的原因是局部内部类访问外部类的成员要加final,而匿名内部类是局部内部类的一种。

        //添加数据
        local.set("狗蛋"); //这个方法是在main线程中运行的,所以是把这个数据绑定到了main线程上面
        //local.remove();


        //创建一个新的线程
        new Thread() {
            @Override
            public void run() {
                local.set("旺财");
                System.out.println("新线程:" + local.get());//获取到的就是新线程上面绑定的数据
            }
        }.start();

        System.out.println("main:" + local.get());
    }
}
// 强引用, 软引用, 弱引用, 虚引用
public class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("新线程执行了");
    }
}

6 用ThreadLocal改写转账案例

创建一个工具类,一个用户一个线程,把连接绑定到当前线程上。

6.1 添加一个工具类

package cn.myl021.utils;
/*
    连接管理类,可以从当前线程上面获取连接。
    所以需要ThreadLocal

    定义以下方法:
        1. 获取连接(从线程上面获取)
        2. 开启事务。
        3. 提交事务
        4. 回滚事务。
 */

import java.sql.Connection;
import java.sql.SQLException;

public class ConnectionManager {
    //定义ThreadLocal,用来给每个线程保存对应的数据库连接
    private static ThreadLocal<Connection> local = new ThreadLocal<>();

    /*
     * 获取连接(从线程上面获取)
     */
    public static Connection getConnection() throws Exception {
        //从当前线程上面获取连接
        Connection con = local.get();
        //如果之前我们没有往这个线程上面放过连接,那么con就是null
        if(con == null) {
            //如果之前没有放过连接,那么就往里面添加一个连接。
            con = C3P0Utils.getConnection(); //从连接池中获取一个新的连接
            local.set(con);//把这个连接绑定到当前线程上
        }
        return con;
    }

    /*
     * 开启事务。
     */
    public static void startTransaction() throws Exception {
        getConnection().setAutoCommit(false);//从当前线程上面获取连接,然后开启事务
    }

    /*
     * 提交事务
     */
    public static void commitTransaction() throws Exception {
        getConnection().commit();
    }

    /*
     * 回滚事物
     */
    public static void rollbackTransaction() throws Exception {
        getConnection().rollback();
    }

}

6.1 service层

package cn.myl021.service;

import java.sql.Connection;
import java.sql.SQLException;

import cn.myl021.dao.AccountDao;
import cn.myl021.utils.C3P0Utils;


import cn.myl021.utils.ConnectionManager;

/*
    转账案例Service层。
    这一层用来处理业务上的数据。
    这一层会调用dao层,被view层

    定义一个转账方法步骤:
        1. 获取连接,然后开启事务。
        2. 给出账人减去对应的金额(调用dao层)
        3. 给入账人加上对应的金额(调用dao层)
        4. 如果整个过程没有问题,就提交。
           如果真个过程出现问题,那么回滚
 */
public class AccountService {
    /*
     * 转账方法。
     * 参数: 出账人姓名, 入账人姓名, 金额。
     * 返回值:void
     * 
     * outName:jack
     * inName:rose
     * money:100
     */
    public void transfer(String outName, String inName, double money) {
        try {
            //直接调用ConnectionManger里面的
            ConnectionManager.startTransaction();

            //给出账人减去对应的金额(调用dao层)
            AccountDao dao = new AccountDao();
            dao.outMoney(outName, money);

            //模拟错误
            System.out.println(10/0);

            //给入账人加上对应的金额(调用dao层)
            dao.inMoney(inName, money);

            //如果整个过程没有问题,就提交。
            ConnectionManager.commitTransaction();
        } catch (Exception e) {
            //回滚事务
            try {
                ConnectionManager.rollbackTransaction();
            } catch (Exception e1) {
                e1.printStackTrace();
            }

            //如果转账失败,那么就手动抛出一个异常,告诉调用者我出现问题了。
            throw new RuntimeException(e.getMessage());
        }

    }
}

6.2 dao层

package cn.myl021.dao;

import java.sql.Connection;
import java.sql.SQLException;

import org.apache.commons.dbutils.QueryRunner;

import cn.myl021.utils.C3P0Utils;
import cn.myl021.utils.ConnectionManager;

/*
    转账案例dao层。
    这一层我们要对数据库进行增删改查。
    这一层被service层调用。

    给出两个方法:
        出账方法
        入账方法

    在一组事务操作中,必须要使用的是同一个连接。
    可以使用参数的形式把连接传递过来。
    在Service层获取连接,然后Service调用dao的时候把连接传递过来,dao层就用Service传递过来的连接就可以了
 */
public class AccountDao {

    /*
     * 出账方法
     * 参数: 出账人姓名, 金额
     * 返回值: 不需要
     */
    public void outMoney(String name, double money) {
        try {
            //创建一个QueryRunner对象
            QueryRunner qr = new QueryRunner();
            //执行sql
            String sql = "UPDATE account SET money=money-? WHERE name=?";
            Object[] params = {money, name};
            qr.update(ConnectionManager.getConnection(), sql, params);
        } catch (Exception e) {
            //手动抛出异常
            throw new RuntimeException("出账失败");
        }
    }

    /*
     * 入账方法
     * 参数: 入账人姓名, 金额
     * 返回值: 不需要
     */
    public void inMoney(String name, double money) {
        try {
            //创建一个QueryRunner对象
            QueryRunner qr = new QueryRunner();
            //执行sql
            String sql = "UPDATE account SET money=money+? WHERE name=?";
            Object[] params = {money, name};
            qr.update(ConnectionManager.getConnection(), sql, params);
        } catch (Exception e) {
            //手动抛出异常
            throw new RuntimeException("入账失败");
        }
    }
}

7 事务的四个特性

原子性<br />  事务里面所有的操作是不可分割的,并且这组操作要么都成功,要么都失败。<br />    一致性<br />  事务操作前后数据要是一致的。<br />    隔离性<br />  多个事务之间应该互不干扰。<br />    持久性<br />  事务一旦提交,就永远的生效了。

8 并发访问问题以及隔离级别

事务的特点.png

8.1 如果不考虑隔离性,会有3种并发访问问题

如果不考虑隔离性,事务存在3中并发访问问题。
1. 脏读:一个事务读到了另一个事务未提交的数据.
2. 不可重复读:一个事务读到了另一个事务已经提交(update)的数据。引发另一个事务在事务中的多次查询结果不一致。
3. 虚读 /幻读:一个事务读到了另一个事务已经提交(insert)的数据。导致另一个事在事务中多次查询的结果不一致。

8.2 数据库的4种隔离级别

(1)read uncommitted 读取未提交
什么都不能解决。会引发脏读,不可重复读、幻读。
1)所有事务都可以看到其他未提交事务的执行结果
2)本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少
3)该级别引发的问题是——脏读(Dirty Read):读取到了未提交的数据
(2)read committed(oracle默认)读取已提交
能解决脏读。会引发不可重复读、幻读。
1)这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)
2)它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变
3)这种隔离级别出现的问题是——不可重复读(Nonrepeatable Read):不可重复读意味着我们在同一个事务中执行完全相同的select语句时可能看到不一 样的结果。导致这种情况的原因可能有:
a.有一个交叉的事务有新的commit,导致了数据的改变;
b.一个数据库被多个实例操作时,同一事务的其他实例在该实例处理其间可能会有新的commit
(3)repeatable read(mysql默认)可重复读
能解决脏读、不可重复读。会引发幻读。
1)这是MySQL的默认事务隔离级别
2)它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行
3)此级别可能出现的问题——幻读(Phantom Read):当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行
(4)serializable 串行化
能解决所有问题。但是同时只能执行一个事务,相当于事务的单线程。效率最低。
(1)这是最高的隔离级别
(2)它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。
(3)在这个级别,可能导致大量的超时现象和锁竞争

演示

 读未提交:read uncommitted
 A窗口设置隔离级别
 AB同时开始事务
 A 查询
 B 更新,但不提交
 A 再查询?— 查询到了未提交的数据
 B 回滚
 A 再查询?— 查询到事务开始前数据
 读已提交:read committed
 A窗口设置隔离级别
 AB同时开启事务
 A查询
 B更新、但不提交
 A再查询?—数据不变,解决问题【脏读】
 B提交
 A再查询?—数据改变,存在问题【不可重复读】
 可重复读:repeatable read
 A窗口设置隔离级别
 AB 同时开启事务
 A查询
 B更新, 但不提交
 A再查询?—数据不变,解决问题【脏读】
 B提交
 A再查询?—数据不变,解决问题【不可重复读】
 A提交或回滚
 A再查询?—数据改变,另一个事务
 串行化:serializable
 A窗口设置隔离级别
 AB同时开启事务
 A查询
 B更新?—等待(如果A没有进一步操作,B将等待超时)
 A回滚
 B 窗口?—等待结束,可以进行操作