事务(访问并发问题与事务隔离级别)、三层架构、Connection中的事务隔离级别(六)

事务

​ 事务指的是逻辑上的一组操作,组成这组操作的各个单元要么全都成功,要么全都失败。

​ 一组SQL,使用同一个连接对象执行的(SQL)。要么全都成功,要么全都失败。

/*
实现数据转账
tom账户-1000
jerry账户+1000
*/
update account set money = money-1000 where name = 'tom';
update account set money = money+1000 where name = 'jerry';
#如果第一个成功第二个失败了,那么就会发现钱丢了,反过来也一样,钱变多了。所以这两个sql应该放在同一个事务里,要么都成功,要么都失败。
复制代码

​ 其实数据库中是有默认事务的,他是一条一条sql语句执行的,会自动提交,但是想要实现我们自己的需求,需要我们自己开启事务,控制事务。

MySQL中的事务操作

start transaction或者begin 开启事务

commit 提交事务(不能再改变了)

rollback 回滚(执行过程中发现有问题,撤回原来的状态,全部操作宣布失败)

JDBC的Connection事务操作

setAutoCommit(false)开启事务

void commit() 提交

void rollback() 回滚

示例:

无事务的转账操作:tom转账1000到jerry账户。

public class TestTranscaction {
    public static void main(String[] args) {
        QueryRunner qr = new QueryRunner();
        Connection con = null;
        try {
            con = DruidUtils.getConnection();
           
            String sql1 = "update account set money = money - ? where name = ?";
            String sql2 = "update account set money = money + ? where name = ?";
            int i = qr.update(con,sql1,1000,"tom");
            int j = qr.update(con,sql2,1000,"jerry");

            System.out.println(i);
 System.out.println(j);

        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            DruidUtils.release(null,null,con);
        }

    }
}

复制代码

为转账案例添加事务

1 关闭事务自动提交 con.setAutoCommit(false);

2 事务提交方法 con.commit();

3 事务回滚方法 con.rollback();

public class TestTranscaction {
    public static void main(String[] args) {
        QueryRunner qr = new QueryRunner();
        Connection con = null;
        try {
            con = DruidUtils.getConnection();
            con.setAutoCommit(false);
            String sql1 = "update account set money = money - ? where name = ?";
            String sql2 = "update account set money = money + ? where name = ?";
            int i = qr.update(con,sql1,1000,"tom");
            int j = qr.update(con,sql2,1000,"jerry");
            con.commit();
            System.out.println(i);
            System.out.println(j);

        } catch (Exception e) {
            e.printStackTrace();
            try {
                con.rollback();
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            }
        }finally {
            DruidUtils.release(null,null,con);
        }

    }
}

复制代码

​ 我们可以认为制造一个异常如计算1/0在两个操作之间,可以发现数据库并没有被改变,而是进入catch块进行事务的回滚

​ 只要不遇到commit操作,事务就不会提交,那么为啥要执行rollback?

​ 我们在开启事务时,会设置setAutoCommit(false),它实际上是给Connection对象上了一个锁,只有这个连接能够修改数据,所以commit和rollback都有释放锁的操作,所以事务失败一定要回滚,不然会占用连接池中的连接,效率降低。

​ 如果停电怎么办?

​ 事务具有超时回滚操作,来电后,事务会执行自己的超时操作,自动回滚。

DbUtils释放资源

在DbUtils工具类中有CommitAndCloseQuietly(Connection con)能够执行失败回滚以及提交操作,并且除此之外还帮我们捕获了commit方法的异常。DbUtils.commitAndCloseQuietly(con);

/**
 *  为转账案例添加事务:使用QueryRunner
 *
 *  如何使用事务控制程序: 事务的关键对象:Connection
 *  Connection连接对象:
 *    方法  void setAutoCommit(boolean autoCommit)
 *      设置事务的提交模式,参数写false,开启事务,阻止事务自动提交,必须手控
 *
 *    方法  void commit() 提交事务,事务一旦提交,数据持久化,不可改变
 *
 *    方法 void rollback() 回滚事务,数据退回,和没有开启事务时候的数据一致
 *
 *   开发数据表:遇到同时执行多个SQL,是insert update delete 必须跟随事务
 *
 *   QueryRunner : 执行SQL语句的对象,构造方法有2中
 *     new QueryRunner() 无参数
 *     new QueryRunner(DataSource ds)有参数,传递连接池对象
 *       传递连接池对象,自己获取连接对象,连接对象的控制权,不在我们手里
 *       事务控制,也就没有了
 *    执行SQL语句的方法: update(传递连接对象)
 *
 *   本案例异常不能 throws 抛出异常
 *   执行SQL语句,两次执行,SQL语句执行成功,没有异常
 *   SQL语句执行失败,抛出异常,catch抓
 *
 *   DButils : 事务的操作
 */
public class TestAccount {
    public static void main(String[] args) {
        //创建对象QueryRunner,无参数构造器
        QueryRunner qr = new QueryRunner();
        Connection con = null;
        try {
            //获取连接对象
            con = DruidUtils.getConnection();
            //开启事务,阻止事务自动提交
            con.setAutoCommit(false);
            //拼写SQL语句,tom账户-1000
            String sql1 = "update account set money = money - ? where name = ?";
            //拼写SQL语句,jerry账户+1000
            String sql2 = "update account set money = money + ? where name = ?";
            //执行SQL语句,tom账户-1000
            int i = qr.update(con, sql1, 1000, "tom");
            //执行SQL语句,jerry账户+1000
            System.out.println(1/0);
            int j = qr.update(con, sql2, 1000, "jerry");
            System.out.println("i = " + i);
            System.out.println("j = " + j);
            //SQL语句执行成功,提交事务,事务结束,数据持久存储
            //con.commit();

            //Apache的工具类:Dbutils
            DbUtils.commitAndCloseQuietly(con);
        }catch (Exception ex){
            ex.printStackTrace();
            //出现异常,SQL语句执行失败,回滚事务
            /*try {
                con.rollback();
            }catch (SQLException e){
                e.printStackTrace();
            }*/
            DbUtils.rollbackAndCloseQuietly(con);
        }finally {
            //释放资源
            //DruidUtils.release(null,null,con);
        }
    }
}

复制代码

经典三层架构

三层架构图.JPG

分层中的框架技术

表现层:Struts、SpringMVC

业务层:事实上没有统一的框架,因为每个系统的业务方向千差万别。为了和表现层,业务层统一,所以赋予了它Spring,这个框架事实上的作用是统筹和集成表现层和业务层的框架的。

持久层:Hibernate、MyBatis、Spring Data Jpa(新)

分层的转账案例

分析

三层架构应用.JPG

实现

pojo类:

@Data
public class Account {
    private Integer id;
    private String name;
    private Double money;
}

复制代码

dao层(对数据库操作):

工具类提取重复代码(此处使用的是Druid数据库连接池)

/**
 * 实现自定义的德鲁伊连接池工具类: 取出连接
 * 德鲁伊连接池的配置文件,采用properties,文件名随意
 *
 * 我们要自己读取配置
 *
 * 德鲁伊连接池的接口实现类:
 *  DataSource接口实现类:DruidDataSource
 *
 * 德鲁伊连接池,提供了工具类,工厂类 (创建对象)
 * 使用工厂类,获取 DruidDataSource对象
 * DruidDataSourceFactory工厂类,静态方法 createDataSource(Properties p) 返回值就是接口实现类对象DruidDataSource
 *
 * 自己读取配置,键值对,存储在集合Properties中, 集合对象传递到createDataSource方法中
 */
public class DruidUtils {
    private static DataSource dataSource;
    static {
        try {
            //类的加载器中的字节输入流,读取配置文件
            InputStream in =
                    DruidUtils.class.getClassLoader().getResourceAsStream("druid.properties");
            Properties prop = new Properties();
            //集合中,存储键值对
            prop.load(in);
            in.close();
            //工厂类的静态方法,创建DataSource接口实现类对象
             dataSource = DruidDataSourceFactory.createDataSource(prop);
        }catch (Exception ex){
            ex.printStackTrace();
        }
    }
    /**
     * 定义方法,返回DataSource接口实现类
     */
    public static DataSource getDataSource(){
        return dataSource;
    }

    /**
     * 定义静态方法,返回数据库连接对象
     */
    public static Connection getConnection() throws SQLException {
       return dataSource.getConnection();
    }

    /**
     * 静态方法:释放资源
     * 释放连接对象,结果集对象,SQL语句执行对象
     */
    public static void release(ResultSet rs, Statement stat, Connection con){
        if (rs!=null)
            try {
                rs.close();
            }catch (SQLException ex){}

        if(stat!=null)
            try {
                stat.close();
            }catch (SQLException ex){}

        if(con!=null)
            try {
                con.close();
            }catch (SQLException ex){}
    }
}

复制代码

调用工具类连接数据库,对数据库进行操作

/**
 * 操作account数据表的类
 * 业务层调用,结果返回业务层
 */
public class AccountDao {
    private QueryRunner qr = new QueryRunner();

    /**
     * 更新数据表,接收业务层的参数:Account对象
     */
    public int updateAccount(Connection con,Account account) throws SQLException {
        String sql = "update account set money = ? where name = ?";
        //执行SQL语句
        return qr.update(con,sql,account.getMoney(),account.getName());
    }


    /**
     *
     * @param con   数据库连接对象
     * @param name  账户名
     * @return  查询到的账户数据
     * @throws SQLException
     */
    public Account queryAccountByName(Connection con,String name) throws SQLException {
        //查询账户的SQL
        String sql = "select * from account where name = ?";
        //执行查询语句,结果集BeanHandler
        return qr.query(con,sql,new BeanHandler<Account>(Account.class),name);
    }
}

复制代码

Service层(业务):

转账案例分层.JPG

/**
 * 实现转账案例的业务逻辑
 * 接收表现层的参数: 付款人,收款人,金额
 * 调用持久层,传递相关参数,完成转账逻辑
 */
public class AccountService {

    private AccountDao accountDao = new AccountDao();

    /**
     *
     * @param toName 付款人
     * @param inName 收款人
     * @param money  转账金额
     */
    public void transfer(String toName,String inName,double money){
        Connection con =  null;
        try {
            //获取连接对象
            con = DruidUtils.getConnection();
            //开启事务
            con.setAutoCommit(false);

            //根据付款人的名字,查询付款人的账户信息
            Account toAccount = accountDao.queryAccountByName(con, toName);
            //根据收款人的名字,查询收款人的账户信息
            Account inAccount = accountDao.queryAccountByName(con, inName);

            //转账逻辑计算,toAccount对象的余额-参数money
            toAccount.setMoney( toAccount.getMoney() - money );
            //转账逻辑计算,inAccount对象的余额+参数money
            inAccount.setMoney( inAccount.getMoney() + money );

            //调用持久层的方法,更新数据,传递参数 Account对象
            int i = accountDao.updateAccount(con, toAccount);
            int a = 1 / 0;
            int j = accountDao.updateAccount(con, inAccount);
            if( i > 0 && j > 0)
                System.out.println("转账成功");

            //所有的SQL语句已经执行完毕,没有出现异常,提交事务
            DbUtils.commitAndCloseQuietly(con);
        }catch (Exception ex){
            ex.printStackTrace();
            //出现异常,事务回滚
           DbUtils.rollbackAndCloseQuietly(con);
        }
    }
}

复制代码

(Web层)

//javaweb之后再讲
复制代码

事务的特点(ACID)

A:原子性,是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。

C:一致性,事务前后数据的完整性必须保持一致。

I:隔离性,事务的隔离性是指多个用户并发访问数据库时,一个用户的事务不能被其他的事物所干扰,多个并发事务之间数据要相互隔离,正常情况下数据库是做不到这一点的,可以手动设置隔离级别,但是效率会特别低。

D:持久性,持久性是指一个事务一旦被提交,他对数据库的改变就是永久的,接下来即使是数据库发生古筝也不应该对其有任何影响。

并发访问问题

如果不考虑隔离性,事务存在三种并发访问问题。

1 脏读:一个事务读取到了另一个事务未提交的数据。

2 不可重复读:一个事务读到了另一个事务已经提交(update)的数据。引发另一个事务,在事务中的多次查询结果不一致。

3 幻读/虚读:一个事务读到了另一个事务已经提交(insert)的数据,导致另一个事务,在事务中多次查询结果不一致。

事务的隔离级别——用来解决并发访问问题

数据库规定了4种隔离级别,分别用于描述两个事务并发的所有情况。

1 read uncommitted:读未提交

​ 存在问题:3个问题(脏读、不可重复读、幻读)

​ 解决问题:无

2 read committed:读已提交

​ 存在问题:2个问题(不可重复读、幻读)

​ 解决问题:1个问题(脏读)

3 repeatable read:可重复读,在一个事务种读到的数据始终保持一致,无论另一个事务是否提交。

​ 存在问题:1个问题(幻读)

​ 解决问题:2个问题(脏读、不可重复读)

4 serializable:串行化,同时只能执行一个事务(相当于单线程)

​ 存在问题:无

​ 解决问题:3个问题(脏读、不可重复读、幻读)

隔离级别 和 并发访问问题之间的关系 脏读 不可重复读 幻读
read uncommitted(读未提交)
read committed(读已提交)
repeatable read(可重复读)
serializable(串行化)

安全性和性能对比:

安全性:serializable > repeatable read > read committed > read uncommitted

性能:serializable < repeatable read < read committed < read uncommitted

常见数据库的默认隔离级别:

MySQL:repeatable read(可重复读)

Oracle:read committed(读已提交)

Connection接口中设置事务的隔离性

注意:实际开发中一般采用默认的隔离级别,由项目经理负责。

/**
 * Java程序,控制事务的隔离级别
 * 采用默认的隔离级别
 *
 * 方法参数int类型: 1,2,4,8 数字越低,隔离级别越低
 * Connection接口中,定义了静态成员变量
 */
public class A {
    public static void main(String[] args) throws SQLException {
        //连接对象接口 Connection
        //指针对这次连接有效
        Connection con = null;
        con.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); //设置当前连接对象的事务隔离级别,方法的参数是int类型
    }
}

复制代码

这个方法是Connection接口中的

void setTransactionIsolation(int level)throws SQLException
复制代码

这个形参level的值应该取下列之一

Connection.TRANSACTION_READ_UNCOMMITTED(读未提交)

Connection.TRANSACTION_READ_COMMITTED(读已提交)

Connection.TRANSACTION_REPEATABLE_READ(可重复读)

Connection.TRANSACTION_SERIALIZABLE(串行化)

可以显而易见的是,这四个都是Connection接口中规定的成员变量值(static int),因为我们引用时使用的是 “接口名.成员变量名”。

在Connection接口中还规定了一个静态int变量TRANSACTION_NONE 它表示一个不支持事务的常量,这个值并不能用来指定连接的事务隔离级别。

//connection接口中成员变量的定义的值
 int TRANSACTION_NONE             = 0;

    
    int TRANSACTION_READ_UNCOMMITTED = 1;

    
    int TRANSACTION_READ_COMMITTED   = 2;

    
    int TRANSACTION_REPEATABLE_READ  = 4;

   
    int TRANSACTION_SERIALIZABLE     = 8;

复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享