SSM框架之Mybatis(5)数据库连接池及事务

Mybatis(5)数据库连接池及事务

1、Mybatis连接池

​ Mybatis 中也有连接池技术,但是它采用的是自己的连接池技术。在 Mybatis 的 SqlMapConfig.xml 配置文件中,通过 dataSource type=”pooled” 来实现 Mybatis 中连接池的配置。使用连接池可以减少因为获取连接而浪费的时间。

1.1、mybatis中的三种连接池配置方式

配置位置:

​ 主配置文件SqlMapConfig.xml中的dataSource标签,其中type属性就是表示采用何种连接池配置方式

type的三种类型

​ POOLEND:采用传统的javax.sql.DataSource规范中的连接池,mybatis中有针对规范的具体实现。

​ UNPOOLED:采用传统的连接获取方式,虽然也用javax.sql.DataSource规范,但它没有池的概念,有请求一个连接,才创建一个连接。

​ JNDI:采用服务器提供的JNDI技术实现,来获取DataSource对象,不同的服务器所能拿到DataSource是不一样。 注意:如果不是web或者maven的war工程,是不能使用的。 我们开发中中使用的是tomcat服务器,采用连接池就是dbcp连接池。

1.2、Mybatis中数据库的配置

在 SqlMapConfig.xml 文件中,具体配置如下:


<dataSource type="POOLED">
    <property name="driver" value="${jdbc.driver}"/>
    <property name="url" value="${jdbc.url}"/>
    <property name="username" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
dataSource>

MyBatis 在初始化时,根据 dataSource 的 type 属性来创建相应类型的的数据源 DataSource,即:
type=”POOLED”:MyBatis 会创建 PooledDataSource 实例
type=”UNPOOLED” : MyBatis 会创建 UnpooledDataSource 实例
type=”JNDI”:MyBatis 会从 JNDI 服务上查找 DataSource 实例,然后返回使用

1.3、关于POOLED与UNPOOLED两种连接池源码分析

1.3.1 、对UNPOOLED的源码分析

type=”UNPOOLED” : MyBatis 会创建 UnpooledDataSource 实例

对UNPOOLED的实现类:

public class UnpooledDataSource implements DataSource

具体分析

首先找到getConnection

public Connection getConnection() throws SQLException {
    return this.doGetConnection(this.username, this.password);
}

public Connection getConnection(String username, String password) throws SQLException {
    return this.doGetConnection(username, password);
}

可以看到两个方法都调用doGetConnection

private Connection doGetConnection(String username, String password) throws SQLException {
    Properties props = new Properties();
    if (this.driverProperties != null) {
        props.putAll(this.driverProperties);
    }

    if (username != null) {
        props.setProperty("user", username);
    }

    if (password != null) {
        props.setProperty("password", password);
    }

    return this.doGetConnection(props);
}

该方法生成Properties props = new Properties();对象

该对象传给doGetConnection(props)

private Connection doGetConnection(Properties properties) throws SQLException {
    this.initializeDriver();
    Connection connection = DriverManager.getConnection(this.url, properties);
    this.configureConnection(connection);
    return connection;
}

首先调用initializeDriver();

**Connection connection = DriverManager.getConnection(this.url, properties);**这就是我们最开始创建连接的方式

关于initializeDriver();

private synchronized void initializeDriver() throws SQLException {
    if (!registeredDrivers.containsKey(this.driver)) {
        try {
            Class driverType;
            if (this.driverClassLoader != null) {
                driverType = Class.forName(this.driver, true, this.driverClassLoader);
            } else {
                driverType = Resources.classForName(this.driver);
            }

            Driver driverInstance = (Driver)driverType.newInstance();
            DriverManager.registerDriver(new UnpooledDataSource.DriverProxy(driverInstance));
            registeredDrivers.put(this.driver, driverInstance);
        } catch (Exception var3) {
            throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + var3);
        }
    }

}

**driverType = Class.forName(this.driver, true, this.driverClassLoader);**加载驱动

所以由此可看出UNPOOLED建立连接的方式就是我们最基础的方式获取连接,即通过加载驱动等方式来获取连接。

1.3.2、对POOLED源码分析

type=”POOLED”:MyBatis 会创建 PooledDataSource 实例

public class PooledDataSource implements DataSource 

首先找到getConnection

public Connection getConnection() throws SQLException {
    return this.popConnection(this.dataSource.getUsername(), this.dataSource.getPassword()).getProxyConnection();
}

public Connection getConnection(String username, String password) throws SQLException {
    return this.popConnection(username, password).getProxyConnection();
}

转到popConnection(username, password)

private PooledConnection popConnection(String username, String password) throws SQLException {
    boolean countedWait = false;
    PooledConnection conn = null;
    long t = System.currentTimeMillis();
    int localBadConnectionCount = 0;

    while(conn == null) {
        PoolState var8 = this.state;
        synchronized(this.state) {
            if (!this.state.idleConnections.isEmpty()) {
                conn = (PooledConnection)this.state.idleConnections.remove(0);
                if (log.isDebugEnabled()) {
                    log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
                }
            } else if (this.state.activeConnections.size() < this.poolMaximumActiveConnections) {
                conn = new PooledConnection(this.dataSource.getConnection(), this);
                if (log.isDebugEnabled()) {
                    log.debug("Created connection " + conn.getRealHashCode() + ".");
                }
            } else {
                PooledConnection oldestActiveConnection = (PooledConnection)this.state.activeConnections.get(0);
                long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
                if (longestCheckoutTime > (long)this.poolMaximumCheckoutTime) {
                    ++this.state.claimedOverdueConnectionCount;
                    this.state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
                    this.state.accumulatedCheckoutTime += longestCheckoutTime;
                    this.state.activeConnections.remove(oldestActiveConnection);
                    if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
                        try {
                            oldestActiveConnection.getRealConnection().rollback();
                        } catch (SQLException var15) {
                            log.debug("Bad connection. Could not roll back");
                        }
                    }

对上述代码进行分析:protected final List idleConnections = new ArrayList();protected final List activeConnections = new ArrayList();

上述两个集合对象就是创建出来的两个连接池,一个是空闲池存放空闲的连接,一个是活动池存放被使用的连接。当一个请求进来时首先判断空闲池中是否为空(if (!this.state.idleConnections.isEmpty()) ),若不为空就分配一个连接;若为空查看活动池中是否达到最大数量,达到最大数量就把最先进入活动池的连接即使用时间最长的连接(long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();),进行一系列操作后清空返回。

                    conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
                    conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());
                    conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());
                    oldestActiveConnection.invalidate();
                    if (log.isDebugEnabled()) {
                        log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
                    }
                } else {
                    try {
                        if (!countedWait) {
                            ++this.state.hadToWaitCount;
                            countedWait = true;
                        }

                        if (log.isDebugEnabled()) {
                            log.debug("Waiting as long as " + this.poolTimeToWait + " milliseconds for connection.");
                        }

                        long wt = System.currentTimeMillis();
                        this.state.wait((long)this.poolTimeToWait);
                        this.state.accumulatedWaitTime += System.currentTimeMillis() - wt;
                    } catch (InterruptedException var16) {
                        break;
                    }
                }
            }

            if (conn != null) {
                if (conn.isValid()) {
                    if (!conn.getRealConnection().getAutoCommit()) {
                        conn.getRealConnection().rollback();
                    }

                    conn.setConnectionTypeCode(this.assembleConnectionTypeCode(this.dataSource.getUrl(), username, password));
                    conn.setCheckoutTimestamp(System.currentTimeMillis());
                    conn.setLastUsedTimestamp(System.currentTimeMillis());
                    this.state.activeConnections.add(conn);
                    ++this.state.requestCount;
                    this.state.accumulatedRequestTime += System.currentTimeMillis() - t;
                } else {
                    if (log.isDebugEnabled()) {
                        log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
                    }

                    ++this.state.badConnectionCount;
                    ++localBadConnectionCount;
                    conn = null;
                    if (localBadConnectionCount > this.poolMaximumIdleConnections + this.poolMaximumLocalBadConnectionTolerance) {
                        if (log.isDebugEnabled()) {
                            log.debug("PooledDataSource: Could not get a good connection to the database.");
                        }

                        throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
                    }
                }
            }
        }
    }

    if (conn == null) {
        if (log.isDebugEnabled()) {
            log.debug("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
        }

        throw new SQLException("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
    } else {
        return conn;
    }
}

2、事务

2.1、什么是事务

是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。

数据库事务通常包含了一个序列的对数据库的读/写操作。包含有以下两个目的:

  1. 为数据库操作序列提供了一个从失败中恢复到正常状态的方法,同时提供了数据库即使在异常状态下仍能保持一致性的方法。
  2. 当多个应用程序在并发访问数据库时,可以在这些应用程序之间提供一个隔离方法,以防止彼此的操作互相干扰。

2.2、事务的四大特性ACID

并非任意的对数据库的操作序列都是数据库事务。数据库事务拥有以下四个特性,习惯上被称之为ACID特性

  • 原子性(Atomicity):事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。
  • 一致性(Consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态的含义是数据库中的数据应满足完整性约束。
  • 隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行。
  • 持久性(Durability):已被提交的事务对数据库的修改应该永久保存在数据库中。

2.3、不考虑隔离性会产生的3个问题

并发事务导致的问题

第一类丢失更新:撤销一个事务时,把其他事务已提交的更新数据覆盖。

脏读:脏读是指在一个事务处理过程里读取了另一个未提交的事务中的数据。

**幻读也叫虚读:一个事务执行两次查询,第二次结果集包含第一次中没有或某些行已经被删除的数据,造成两次结果不一致,只是另一个事务在这两次查询中间插入或删除了数据造成的。**幻读是事务非独立执行时发生的一种现象。

**不可重复读:**一个事务两次读取同一行的数据,结果得到不同状态的结果,中间正好另一个事务更新了该数据,两次结果相异,不可被信任。

**Tips:**不可重复读和脏读的区别:脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了前一事务提交的数据。

**Tips:**幻读和不可重复读都是读取了另一条已经提交的事务(这点就脏读不同),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据整体(比如数据的个数)。

第二类丢失更新:是不可重复读的特殊情况。如果两个事物都读取同一行,然后两个都进行写操作,并提交,第一个事物所做的改变就会丢失。

2.4、数据库事务的隔离级别

事务的隔离级别有4种,由低到高分别为Read uncommittedRead committedRepeatable readSerializable 。而且,在事务的并发操作中可能会出现脏读不可重复读幻读。下面通过事例一一阐述它们的概念与联系。

**Read uncommitted(最低级别,任何情况都无法保证。)**读未提交,

顾名思义,就是一个事务可以读取另一个未提交事务的数据。

那怎么解决脏读呢?Read committed!读提交,能解决脏读问题。

**Read committed(可避免脏读的发生。)**读提交

顾名思义,就是一个事务要等另一个事务提交后才能读取数据。

**Analyse:**这就是读提交,若有事务对数据进行更新(UPDATE)操作时,读操作事务要等待这个更新操作事务提交后才能读取数据,可以解决脏读问题。但在这个事例中,出现了一个事务范围内两个相同的查询却返回了不同数据,这就是不可重复读。

那怎么解决可能的不可重复读问题?Repeatable read !

**Repeatable read(可避免脏读、不可重复读的发生。)**重复读,

就是在开始读取数据(事务开启)时,不再允许修改操作

**Analyse:**重复读可以解决不可重复读问题。写到这里,应该明白的一点就是,不可重复读对应的是修改,即UPDATE操作。但是可能还会有幻读问题。因为幻读问题对应的是插入INSERT操作,而不是UPDATE操作。

那怎么解决幻读问题?Serializable!

Serializable(可避免脏读、不可重复读、幻读的发生。) 序列化

Serializable 是最高的事务隔离级别,在该级别下,事务串行化顺序执行,可以避免脏读、不可重复读与幻读。但是这种事务隔离级别效率低下,比较耗数据库性能,一般不使用。

Tips:大多数数据库默认的事务隔离级别是Read committed,比如 Sql Server , Oracle。     Mysql 的默认隔离级别是Repeatable read。

**Tips:**隔离级别的设置只对当前链接有效。对于使用MySQL命令窗口而言,一个窗口就相当于一个链接,当前窗口设置的隔离级别只对当前窗口中的事务有效;对于JDBC操作数据库来说,一个Connection对象相当于一个链接,而对于Connection对象设置的隔离级别只对该Connection对象有效,与其他链接Connection对象无关。

**Tips:**设置数据库的隔离级别一定要是在开启事务之前。

2.5、Mybatis中有关事务的操作

可通过SqlSession session = factory.openSession(true);

openSession(true);给其传入一个布尔值进行决定是否是自动提交事务,还是手动提交事务,

该值默认false

2.5.1需要手动提交的分析

dao实现类中的操作

public void addUser(User user) {
    SqlSession session = factory.openSession(true);
    int i = session.insert("dao.IUserDao.addUser", user);
    session.commit();

    session.close();
}

对应测试类操作

@Test
public void testAddUser(){
    //添加用户
    User user = new User();
    user.setUsername("wf");
    user.setAddress("中国");
    user.setSex("男");
    user.setBirthday(new Date());

    System.out.println(user);

    uesrdao.addUser(user);

    System.out.println(user);

}

输出结果:

Opening JDBC Connection
Created connection 1075738627.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@401e7803]
==> Preparing: select * from user where id=?
> Parameters: 48(Integer)
<
Total: 1
Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@401e7803]
Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@401e7803]
Returned connection 1075738627 to pool.

这是我们的 Connection 的整个变化过程,通过分析我们能够发现之前的 CUD 操作过程中,我们都要手动进
行事务的提交,原因是 setAutoCommit()方法,在执行时它的值被设置为 false 了,所以我们在 CUD 操作中,
必须通过 sqlSession.commit()方法来执行提交操作。

2.5.2、需要自动提交的分析

dao实现类中的操作

public void addUser(User user) {
    SqlSession session = factory.openSession(true);
    int i = session.insert("dao.IUserDao.addUser", user);
    session.commit();

    session.close();
}

对应测试类操作

@Test
public void testAddUser(){
    //添加用户
    User user = new User();
    user.setUsername("wf");
    user.setAddress("中国");
    user.setSex("男");
    user.setBirthday(new Date());

    System.out.println(user);

    uesrdao.addUser(user);

    System.out.println(user);

}

输出结果:

Opening JDBC Connection

Created connection 633075331.

==> Preparing: insert into user (username,address,sex,birthday) value (?,?,?,?)
> Parameters: wf(String), 中国(String), 男(String), 2019-04-17 16:55:58.177(Timestamp)
<
Updates: 1
==> Preparing: select last_insert_id();
> Parameters:
<
Total: 1
Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@25bbf683]
Returned connection 633075331 to pool.

我们发现,此时事务就设置为自动提交了,同样可以实现CUD操作时记录的保存。虽然这也是一种方式,但就
编程而言,设置为自动提交方式为 false再根据情况决定是否进行提交,这种方式更常用。因为我们可以根据业务
情况来决定提交是否进行提交。

你可能感兴趣的:(SSM框架之Mybatis(5)数据库连接池及事务)