Java进阶学习第十八天——事物与连接池

41 篇文章 1 订阅
4 篇文章 0 订阅
文档版本开发工具测试平台工程名字日期作者备注
V1.02016.05.13lutianfeinone

事务

事务的概念

  • 事务就是一个事情,组成这个事情可能有多个单元,要求这些单元,要么全都成功,要么全都不成功
  • 在开发中,有事务的存在,可以保证数据完整性

  • 例如:A——B转帐,对应于如下两条sql语句

    • update account set money=money-100 where name=‘a’;
    • update account set money=money+100 where name=‘b’;
  • 数据库默认事务是自动提交的,也就是发一条sql它就执行一条。如果想多条sql放在一个事务中执行,则需要使用如下语句。
Start transaction
...
commit

MySQL下操作事物

  • 方式1:

    • start transaction 开启事务
    • rollback 事务回滚
    • commit 事务提交
  • 方式2:

    • show variables like ‘%commit%’; 可以查看当前autocommit值
      • 在mysql数据库中它的默认值是”on”代表自动事务.
      • 自动事务的意义就是:执行任意一条sql语句都会自动提交事务.
  • 测试:将autocommit的值设置为off

    • 1.set autocommit=off 关闭自动事务。
    • 2.必须手动commit才可以将事务提交。
    • 注意:mysql默认autocommit=on oracle默认的autocommit=off;


JDBC下操作事物

  • 当Jdbc程序向数据库获得一个Connection对象时,默认情况下这个Connection对象会自动向数据库提交在它上面发送的SQL语句。若想关闭这种默认提交方式,让多条SQL在一个事务中执行,可使用下列语句:
  • java.sql.Connection接口中有几个方法是用于可以操作事务
    • 1.setAutocommit(boolean flag);
      • 如果flag=false;它就相当于start transaction;
    • 2.rollBack()
      • 事务回滚。
    • 3.commit()
      • 事务提交

银行转帐案例

  • 在JDBC代码中使如下转帐操作在同一事务中执行。
    • update from account set money=money-100 where name=‘a’;
    • update from account set money=money+100 where name=‘b’;
  • 设置事务回滚点
    • Savepoint sp = conn.setSavepoint();
    • Conn.rollback(sp);
    • Conn.commit(); //回滚后必须要提交
create table account(
   id int primary key auto_increment,
   name varchar(20),
   money double
);

insert into account values(null,'aaa',1000);
insert into account values(null,'bbb',1000);
insert into account values(null,'ccc',1000);


事务特性(重点) ACID

  • 原子性(Atomicity)
    • 原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
  • 一致性(Consistency)
    • 事务前后数据的完整性必须保持一致。
  • 隔离性(Isolation)
    • 事务的隔离性是指多个用户并发访问数据库时,一个用户的事务不能被其它用户的事务所干扰,多个并发事务之间数据要相互隔离。
  • 持久性(Durability)
    • 持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响


事务的隔离级别
  • 多个线程开启各自事务操作数据库中数据时,数据库系统要负责隔离操作,以保证各个线程在获取数据时的准确性。
如果不考虑隔离性,可能会引发的问题
  • 1.脏读 一个事务读取到另一个事务的未提交数据。

    • 这是非常危险的,假设A向B转帐100元,对应sql语句如下所示
      • 1.update account set money=money+100 while name=‘b’;
      • 2.update account set money=money-100 while name=‘a’;
    • 当第1条sql执行完,第2条还没执行(A未提交时),如果此时B查询自己的帐户,就会发现自己多了100元钱。如果A等B走后再回滚,B就会损失100元。
  • 2.不可重复读

    • 在一个事务内读取表中的某一行数据,多次读取结果不同。(update)
      • 例如银行想查询A帐户余额,第一次查询A帐户为200元,此时A向帐户存了100元并提交了,银行接着又进行了一次查询,此时A帐户为300元了。银行两次查询不一致,可能就会很困惑,不知道哪次查询是准的。
    • 和脏读的区别是,脏读是读取前一事务未提交的脏数据,不可重复读是重新读取了前一事务已提交的数据。
    • 很多人认为这种情况就对了,无须困惑,当然是后面的为准。我们可以考虑这样一种情况,比如银行程序需要将查询结果分别输出到电脑屏幕和写到文件中,结果在一个事务中针对输出的目的地,进行的两次查询不一致,导致文件和屏幕中的结果不一致,银行工作人员就不知道以哪个为准了。
  • 3.虚读(幻读)

    • 是指在一个事务内读取到了别的事务插入的数据,导致前后读取不一致。(insert)
    • 如丙存款100元未提交,这时银行做报表统计account表中所有用户的总额为500元,然后丙提交了,这时银行再统计发现帐户为600元了,造成虚读同样会使银行不知所措,到底以哪个为准。
  • 4.丢失更新

    • 两个或多个事务更新同一行,但这些事务彼此之间都不知道其它事务进行的修改,因此第二个更改覆盖了第一个修改


事务隔离性的设置语句
  • 数据库共定义了四种隔离级别:

    • Serializable:可避免脏读不可重复读虚读情况的发生。(串行化)
    • Repeatable read:可避免脏读不可重复读情况的发生。不可以避免虚读
    • Read committed:可避免脏读情况发生(读已提交)
    • Read uncommitted:最低级别,以上情况均无法保证。(读未提交)
  • 安全性:serializable > repeatable read > read committed > read uncommitted

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

  • 结论: 实际开发中,通常不会选择 serializableread uncommitted

  • 怎样设置事务的隔离级别?

  • 1.mysql中设置

    • 1.查看事务隔离级别
      • select @@tx_isolation 查询当前事务隔离级别
      • mysql中默认的事务隔离级别是 Repeatable read.
      • oracle 中默认的事务隔离级别是 Read committed
    • 2.mysql中怎样设置事务隔离级别
      • set session transaction isolation level 设置事务隔离级别
  • 2.jdbc中设置

    • 在jdbc中设置事务隔离级别
    • 使用java.sql.Connection接口中提供的方法
      • void setTransactionIsolation(int level) throws SQLException
      • 参数level可以取以下值:
        • TRANSACTION_READ_UNCOMMITTED,指示可以发生脏读不可重复读虚读
        • TRANSACTION_READ_COMMITTED:指示不可以发生脏读,但是不可重复读虚读可以发生。
        • TRANSACTION_REPEATABLE_READ:指示不可以发生脏读不可重复读,但是虚读可以发生。
        • TRANSACTION_SERIALIZABLE:指示不可以发生脏读不可重复读虚读的常量。
        • TRANSACTION_NONE,一般不使用,因为它指定了不受支持的事务。)


隔离级别方案演示
  • 1.脏读

    • 一个事务读取到另一个事务的为提交数据
    • 设置A,B事务隔离级别为 Read uncommitted
      • set session transaction isolation level read uncommitted;
    • 1.在A事务中
      • start transaction;
      • update account set money=money-500 where name=’aaa’;
      • update account set money=money+500 where name=’bbb’;
    • 2.在B事务中
      • start transaction;
      • select * from account;
    • 这时,B事务读取时,会发现,钱已经汇完。那么就出现了脏读。
    • 当A事务提交前,执行rollback,在commit, B事务在查询,就会发现,钱恢复成原样。
    • 也出现了两次查询结果不一致问题,出现了不可重复读.
  • 2.解决脏读问题

    • 将事务的隔离级别设置为 read committed来解决脏读
    • 设置A,B事务隔离级别为 Read committed
    • set session transaction isolation level read committed;
    • 1.在A事务中
      • start transaction;
      • update account set money=money-500 where name=’aaa’;
      • update account set money=money+500 where name=’bbb’;
    • 2.在B事务中
      • start transaction;
      • select * from account;
    • 这时B事务中,读取信息时,是不能读到A事务未提交的数据的,也就解决了脏读。
    • 让A事务,提交数据 commit;
    • 这时,在查询,这次结果与上一次查询结果又不一样了,还存在不可重复读。
  • 3.解决不可重复读

    • 将事务的隔离级别设置为Repeatable read来解决不可重复读。
    • 设置A,B事务隔离级别为 Repeatable read;
    • set session transaction isolation level Repeatable read;
    • 1.在A事务中
      * start transaction;
      * update account set money=money-500 where name=’aaa’;
      * update account set money=money+500 where name=’bbb’;
    • 2.在B事务中
      * start transaction;
      * select * from account;
    • 当A事务提交后commit;B事务在查询,与上次查询结果一致,解决了不可重复读。
  • 4.设置事务隔离级别 Serializable ,它可以解决所有问题

    • set session transaction isolation level Serializable;
    • 如果设置成这种隔离级别,那么会出现锁表。也就是说,一个事务在对表进行操作时,其它事务操作不了。
案例:转账汇款—-使用事务

  • 问题:service调用了dao中两个方法完成了一个业务操作,如果其中一个方法执行失败怎样办?

    • 需要事务控制
  • 问题:怎样进行事务控制?

    • 我们在service层进行事务的开启,回滚以及提交操作。
  • 问题:进行事务操作需要使用Connection对象,怎样保证,在service中与dao中所使用的是同一个Connection.

    • 在service层创建出Connection对象,将这个对象传递到dao层.
  • 注意:Connecton对象使用完成后,在service层的finally中关闭

    • 而每一个PreparedStatement它们在dao层的方法中用完就关闭.
  • 关于程序问题

    • 1.对于转入与转出操作,我们需要判断是否成功,如果失败了,可以通过抛出自定义异常在servlet中判断,进行信息展示 。
  • 问题:

    • 在设置dao层时,
        public interface AccountDao {
            public void accountOut(String accountOut, double money) throws Exception;

            public void accountIn(String accountIn, double money) throws Exception;

        }
  • 那么我们自己去实现这个接口时,怎样处理,同一个Connection对象问题?


使用ThreadLocal
  • 将一个值或对象,绑定在一个线程当中,在这个线程的任何位置都可以获取这个值或对象。
  • ThreadLocal可以理解成是一个Map集合,Map<Thread,Object>
  • set方法是向ThreadLocal中存储数据,那么当前的key值就是当前线程对象
  • get方法是从ThreadLocal中获取数据,它是根据当前线程对象来获取值。

  • 如果我们是在同一个线程中,只要在任意的一个位置存储了数据,在其它位置上,就可以获取到这个数据。

bcUtils中使用ThreadLocal
  • 1.声明一个ThreadLocal
    • private static final ThreadLocal<Connection> tl = new ThreadLocal<Connection>();
  • 2.在getConnection()方法中操作
    • Connection con = tl.get(); 直接从ThreadLocal中获取,第一次返回的是null
    if (con == null) {
        // 2.获取连接
        con = DriverManager.getConnection(URL, USERNAME, PASSWORD);
        tl.set(con); //将con装入到ThreadLocal中。
    }


丢失更新
  • 多个事务对同一条记录进行了操作,后提交的事务将先提交的事务操作覆盖了。

解决丢失更新可以采用两种方式
  • 1.悲观锁

    • 悲观锁: 假设丢失更新一定会发生 ,利用数据库内部锁机制,管理事务
    • 提供的锁机制

      • 1.共享锁:在此记录上可以添加多个锁
        • select * from table lock in share mode(读锁、共享锁)
      • 2.排它锁:在此记录上只可以加一个锁
        • select * from table for update (写锁、排它锁)
      • update语句默认添加排它锁
    • 2.乐观锁( Optimistic Locking )

      • 乐观锁 假设丢失更新不会发生 ,采用程序中添加版本字段解决丢失更新问题

  • 解决丢失更新:在数据表添加版本字段(通过时间戳字段),每次修改过记录后,版本字段都会更新,如果读取是版本字段,与修改时版本字段不一致,说明别人进行修改过数据 (重改)
     create table product (
        id int,
        name varchar(20),
        updatetime timestamp
     );

     insert into product values(1,'冰箱',null);
     update product set name='洗衣机' where id = 1;




连接池

  • 连接池:就是创建一个容器,用于装入多个Connection对象,在使用连接对象时,从容器中获取一个Connection, 使用完成后,在将这个Connection重新装入到容器中。这个容器就是连接池。(DataSource)也叫做数据源.
  • 作用:我们可以通过连接池获取连接对象。

  • 优点:

    • 节省创建连接与释放连接造成的性能消耗 —- 连接池中连接起到复用的作用 ,提高程序性能
  • 应用程序直接获取链接

  • 应用程序直接获取链接的缺点

    • 用户每次请求都需要向数据库获得链接,而数据库创建连接通常需要消耗相对较大的资源,创建时间也较长。假设网站一天10万访问量,数据库服务器就需要创建10万次连接,极大的浪费数据库的资源,并且极易造成数据库服务器内存溢出、拓机。

数据库连接池编写原理分析

  • 编写连接池需实现javax.sql.DataSource接口。DataSource接口中定义了两个重载的getConnection方法:

    • Connection getConnection()
    • Connection getConnection(String username, String password)
  • 实现DataSource接口,并实现连接池功能的步骤:

    • 在DataSource构造函数中批量创建与数据库的连接,并把创建的连接保存到一个集合对象中
    • 实现getConnection方法,让getConnection方法每次调用时,从集合对象中取一个Connection返回给用户。
    • 当用户使用完Connection,调用Connection.close()方法时,Collection对象应保证将自己返回到连接池的集合对象中,而不要把conn还给数据库。
  • 原来由jdbcUtil创建连接,现在由dataSource创建连接,为实现不和具体数据为绑定,因此datasource也应采用配置文件的方法获得连接。



自定义连接池

  • 1.创建一个MyDataSource类,在这个类中创建一个LinkedList<Connection>
  • 2.在其构造方法中初始化List集合,并向其中装入5个Connection对象。
  • 3.创建一个public Connection getConnection();从List集合中获取一个连接对象返回.
  • 4.创建一个 public void readd(Connection) 这个方法是将使用完成后的Connection对象重新装入到List集合中.

  • 代码问题:

  • 1.连接池的创建是有标准的.

    • javax.sql包下定义了一个接口 DataSource
    • 所有的连接池必须实现javax.sql.DataSource接口
  • 2.我们操作时,要使用标准,怎样可以让 con.close()它不是销毁,而是将其重新装入到连接池.

    • 要解决这个问题,其本质就是将Connection中的close()方法的行为改变。
    • 怎样可以改变一个方法的行为(对方法功能进行增强)
      • 1.继承
      • 2.装饰模式
        • 1.装饰类与被装饰类要实现同一个接口或继承同一个父类
        • 2.在装饰类中持有一个被装饰类引用
        • 3.对方法进行功能增强。
      • 3.动态代理
        • 可以对行为增强
        • Proxy.newProxyInstance(ClassLoacer ,Class[],InvocationHandler);
    • 结论:Connection对象如果是从连接池中获取到的,那么它的close方法的行为已经改变了,不在是销毁,而是重新装入到连接池。

    • 1.连接池必须实现javax.sql.DataSource接口。

    • 2.要通过连接池获取连接对象 DataSource接口中有一个 getConnection方法.
    • 3.将Connection重新装入到连接池 使用Connection的close()方法。
  • 动态代理增强close示例

    • 使用动态代理技术构建连接池中的connection
proxyConn = (Connection) Proxy.newProxyInstance(this.getClass()
            .getClassLoader(), conn.getClass().getInterfaces(),
            new InvocationHandler() {
        //此处为内部类,当close方法被调用时将conn还回池中,其它方法直接执行
            public Object invoke(Object proxy, Method method,
                      Object[] args) throws Throwable {
                if (method.getName().equals("close")) {
                    pool.addLast(conn);
                    return null;
            }
            return method.invoke(conn, args);
        }
    });


  • 补充:装饰类例子
//使用装饰进行增强
public class Demo2 {
    public static void main(String[] args) {

        Car car=new Bmw();

        //给车增强
        CarDerector cd=new CarDerector(car);

        cd.run();

    }
}

interface Car {
    void run();
}

class Bmw implements Car {
    public void run() {
        System.out.println("bmw run....");
    }
}

class Benz implements Car {
    public void run() {
        System.out.println("benz run....");
    }
}

// 使用装饰来完成
class CarDerector implements Car {

    private Car car;

    public CarDerector(Car car) {
        this.car = car;
    }

    public void run() {
        System.out.println("添加导航");
        car.run();

    }
}


开源连接池

  • 现在很多WEB服务器(Weblogic, WebSphere, Tomcat)都提供了DataSoruce的实现,即连接池的实现。通常我们把DataSource的实现,按其英文含义称之为数据源,数据源中都包含了数据库连接池的实现。
  • 也有一些开源组织提供了数据源的独立实现:
    • DBCP 数据库连接池
    • C3P0 数据库连接池
    • Apache Tomcat内置的连接池(apache dbcp)
  • 实际应用时不需要编写连接数据库代码,直接从数据源获得数据库的连接。程序员编程时也应尽量使用这些数据源的实现,以提升程序的数据库访问性能。
DBCP数据源(了解)
  • DBCP 是 Apache 软件基金组织下的开源连接池实现
  • 使用DBCP数据源,应用程序应在系统中增加如下两个 jar 文件:
    • Commons-dbcp-1.4.jar:连接池的实现
    • Commons-pool-1.5.6.jar:连接池实现的依赖库
  • Tomcat 的连接池正是采用该连接池来实现的。该数据库连接池既可以与应用服务器整合使用,也可由应用程序独立使用。
dbcp连接池使用
  • 1.手动配置(手动编码)
    // 1.手动配置
    @Test
    public void test1() throws SQLException {

        BasicDataSource bds = new BasicDataSource();

        // 需要设置连接数据库最基本四个条件
        bds.setDriverClassName("com.mysql.jdbc.Driver");
        bds.setUrl("jdbc:mysql:///day18");
        bds.setUsername("root");
        bds.setPassword("abc");

        // 得到一个Connection
        Connection con = bds.getConnection();

        ResultSet rs = con.createStatement().executeQuery(
                "select * from account");

        while (rs.next()) {

            System.out.println(rs.getInt("id") + "  " + rs.getString("name"));
        }

        rs.close();
        con.close(); // 将Connection对象重新装入到连接池.

    }


  • 2.自动配置(使用配置文件)
    // 2.自动配置
    @Test
    public void test2() throws Exception {

        Properties props = new Properties();

        // props.setProperty("driverClassName", "com.mysql.jdbc.Driver");
        // props.setProperty("url", "jdbc:mysql:///day18");
        // props.setProperty("username", "root");
        // props.setProperty("password", "abc");
        FileInputStream fis = new FileInputStream(
                "D:\\java1110\\workspace\\day18_2\\src\\dbcp.properties");
        props.load(fis);

        DataSource ds = BasicDataSourceFactory.createDataSource(props);

        // 得到一个Connection
        Connection con = ds.getConnection();

        ResultSet rs = con.createStatement().executeQuery(
                "select * from account");

        while (rs.next()) {

            System.out.println(rs.getInt("id") + "  " + rs.getString("name"));
        }

        rs.close();
        con.close(); // 将Connection对象重新装入到连接池.
    }


C3P0数据源(必会)
  • C3P0是一个开源的JDBC连接池,它实现了数据源和JNDI绑定,支持JDBC3规范JDBC2的标准扩展
  • 目前使用它的开源项目有HibernateSpring等。

  • c3p0与dbcp区别

    • dbcp没有自动回收空闲连接的功能
    • c3p0有自动回收空闲连接功能
  • 1.导包

    • c3p0-0.9.1.2.jar
  • 1.手动使用

ComboPooledDataSource cpds = new ComboPooledDataSource();
cpds.setDriverClass("com.mysql.jdbc.Driver");
cpds.setJdbcUrl("jdbc:mysql:///day18");
cpds.setUser("root");
cpds.setPassword("abc");
  • 2.自动(使用配置文件)
    • c3p0的配置文件可以是properties也可以是xml
    • c3p0的配置文件如果名称叫做 c3p0.properties or c3p0-config.xml 并且放置在classpath路径下(对于web应用就是classes目录)那么c3p0会自动查找
    • 注意:我们其时只需要将配置文件放置在src下就可以
    • 使用:
      • ComboPooledDataSource cpds = new ComboPooledDataSource();
      • 它会在指定的目录下查找指定名称的配置文件,并将其中内容加载。
<?xml version="1.0" encoding="UTF-8"?>
<c3p0-config>
    <default-config>
        <property name="driverClass">com.mysql.jdbc.Driver</property>
        <property name="jdbcUrl">jdbc:mysql:///day18</property>
        <property name="user">root</property>
        <property name="password">abc</property>
    </default-config>

</c3p0-config>


    @Test
    public void test2() throws PropertyVetoException, SQLException {
        ComboPooledDataSource cpds = new ComboPooledDataSource();

        // 得到一个Connection
        Connection con = cpds.getConnection();

        ResultSet rs = con.createStatement().executeQuery(
                "select * from account");

        while (rs.next()) {

            System.out.println(rs.getInt("id") + "  " + rs.getString("name"));
        }

        rs.close();
        con.close(); // 将Connection对象重新装入到连接池.

        // String path = this.getClass().getResource("/").getPath();
        // System.out.println(path);
    }


配置Tomcat数据源
  • tomcat服务器内置连接池 (使用Apache DBCP)
  • 配置tomcat 内置连接池,通过JNDI方式 去访问tomcat的内置连接池
JNDI技术简介
  • JNDI(Java Naming and Directory Interface),Java**命名目录接口,它对应于**J2SE中的javax.naming包。是JavaEE一项技术,允许将一个Java对象绑定到一个JNDI容器(tomcat)中,并且为对象指定一个名称,通过javax.naming 包 Context 对JNDI 容器中绑定的对象进行查找,通过指定名称找到绑定Java对象。

  • 这套API的主要作用在于:它可以把Java对象放在一个容器中(支持JNDI容器 Tomcat),并为容器中的java对象取一个名称,以后程序想获得Java对象,只需通过名称检索即可。

  • 其核心API为Context,它代表JNDI容器,其lookup方法为检索容器中对应名称的对象。
配置操作步骤
  • 1、配置使用tomcat 内置连接池 配置<context> 元素
  • context元素有三种常见配置位置
    • 1) tomcat/conf/context.xml 所有虚拟主机,所有工程都可以访问该连接池
    • 2) tomcat/conf/Catalina/localhost/context.xml 当前虚拟主机(localhost)下所有工程都可以使用该连接池
    • 3) 当前工程/META-INF/context.xml 只有当前工程可以访问该连接池
<Context>
  <Resource name="jdbc/EmployeeDB" auth="Container"
            type="javax.sql.DataSource" username="root" password="abc"
            driverClassName="com.mysql.jdbc.Driver" url="jdbc:mysql:///day14"
            maxActive="8" maxIdle="4"/>
</Context>
  • 必须先将mysql驱动jar包 复制tomcat/lib下
  • 在tomcat启动服务器时,创建连接池对象,绑定 jdbc/EmployeeDB 指定名称上

  • 2、通过运行在JNDI容器内部的程序(Servlet/JSP)去访问tomcat内置连接池

public class DataSourceServlet extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        try {
            Context context = new InitialContext();
            Context envCtx = (Context) context.lookup("java:comp/env"); // 固定路径
            DataSource datasource = (DataSource) envCtx
                    .lookup("jdbc/EmployeeDB");//自己定义的数据库名字

            Connection con = datasource.getConnection();
            ResultSet rs = con.createStatement().executeQuery(
                    "select * from account");

            while (rs.next()) {
                System.out.println(rs.getInt("id") + "  "
                        + rs.getString("name"));
            }

        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        doGet(request, response);
    }

}
  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值