MySQL 事务处理和连接池优化

作者: uniapp | 来源:发表于2018-05-27 10:05 被阅读9次

    1 事务处理

    通过事务处理,我们可以控制一组数据库操作,这组操作要么全部成功,要么全部失败;

    比如,在 account 表中,记录了账户 a 和账户 b 均有 1000 元,账户 a 想要往账户 b 转入 100 元。通常的流程是两部操作:

    //起始
    update account set money=money-100 where name='a';
    //意外
    update account set money=money+100 where name='b';
    

    但是如果这两条 sql 语句执行过程中出现了意外,应该回到起始点。
    在 MySQL 中通过 Transaction 来实现事务处理,使用的流程为:

    1 Start Transaction 开启事务
    2 Rollback 回滚事务, 事务失效;
    3 Commit 提交事务, 事务生效;
    

    利用事务处理改造一下开头提出的转账问题后,流程就变成了:

    start transaction;
    update account set money=money-100 where name='a';
    //意外 
    rollback;
    update account set money=money+100 where name='b';
    //成功 
    commit;
    

    在 Java 中封装的 JDBC 使用事务处理通过 Connection 来实现,通过方法setAutoCommitcommit 实现事务的开启和提交。还可以通过 setSavepoint 方法设置回滚点,控制事务回滚过程,下面是一个使用的范例,不清楚 JDBCUtils 的童鞋,请参考我的上篇文章 App 开发者使用 JDBC 驱动操纵 MySql 数据库

    public void test2(){
            Connection connection = null;
            Statement stmt = null;
            Savepoint sp = null;
            try{
                connection = JDBCUtils.getConnection();
                connection.setAutoCommit(false);
                stmt = connection.createStatement();
                stmt.executeUpdate("update account set money=money-100 where name='aaa';");
                stmt.executeUpdate("update account set money=money+100 where name='bbb';");
    
                sp = connection.setSavepoint();
    
                stmt.executeUpdate("update account set money=money-100 where name='bbb';");
                stmt.executeUpdate("update account set money=money+100 where name='ccc';");
                int i = 1/0;
                connection.commit();
            }catch (Exception e){
                e.printStackTrace();
                try {
                    connection.rollback(sp);
                    connection.commit();//回滚点前的操作要生效;
    
                } catch (SQLException e1) {
                    e1.printStackTrace();
                }
            }finally {
                try {
                    connection.commit();//回滚点前的操作要生效;
                } catch (SQLException e) {
                    e.printStackTrace();
                }
                JDBCUtils.release(null, stmt, connection);
            }
        }
    

    2 数据库事务的特性

    数据库事务特性可以概括为 ACID:

    1 Atomicity:原子性;
    2 Consistency: 一致性;
    3 Isolation: 隔离性;
    4 Durable: 持久性;
    

    其中隔离性在多事务操作中显得比较重要,如果隔离性处理不好,可能会造成如下错误:

    1 脏读(dirty read): 一个事务读到了其他事务未提交的数据;
    2 不可重复读(unreatable read): 一个事务在操作时,读到其他事务已经提交的数据, 主要是 update 语句更新;
    3 虚读(phantom read):  一个事务在操作时,读到其他事务已经提交的数据;主要是 insert 语句插入记录;
    
    

    MySQL 数据库中设置了如下 4 个类型的隔离性级别,用来解决不同的多事务处理场景:

    1 read uncommitted (读未提交) 会产生脏读,不重复读,虚度可能发生;
    2 read commited (读已提交) 不会脏读,会引发不可重复读,虚读可能发生;
    3 repeatable read (可以重复读) 不会脏读,不可重复读,虚度有可能;
    4 serializable(串行化) 不会发生三类问题;事务有先后顺序;
    

    MySQL 设置事务处理隔离级别的命令是:set session transaction isolation level 隔离值;,默认的隔离级别是 repeatable read
    3 数据库连接池优化程序性能

    多事务处理中,需要反复连接,释放对数据库的操作。当数量众多时会导致数据库服务器内存溢出、宕机。Sun 公司给出的解决方案是采用连接池优化对数据库进行访问,其大概原理是:一次性创造多个连接对象放到连接池中,当需要操作数据库时,就从连接池取用,避免了不断创建和释放连接的过程,进而显著提高程序性能。

    模拟一个数据库连接池如下:

    public class MyDataSource implements DataSource {
    
        //增删操作,使用 LinkedList 效率会更高;
        private java.util.LinkedList<Connection> pool = new LinkedList<Connection>();
    
        public MyDataSource(){
            //创建连接池
            System.out.println("创建 10 个连接池");
            for (int i = 0; i < 10; i++) {
                Connection connection = JDBCUtils.getConnection();
                pool.add(connection);
            }
        }
        @Override
        public Connection getConnection() throws SQLException {
            //TODO
            if (pool.isEmpty()){
                for (int i = 0;i < 3;i++){
                    Connection connection = JDBCUtils.getConnection();
                    pool.add(connection);
                }
            }
            final Connection conn = pool.removeFirst();
            //代理对象
            Connection proxyConn = (Connection)Proxy.newProxyInstance(MyDataSource.class.getClassLoader(),
                    conn.getClass().getInterfaces(), new InvocationHandler() {
                        @Override
                        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
                            if (method.getName().equals("close")){
                                //close 放回池子
    
                                addBack2Pool(conn);
                                return null;
                            }
                            return method.invoke(conn, args);
                        }
                    });
            return conn;
        }
    
        public void addBack2Pool(Connection conn){
            System.out.println("放回");
            pool.add(conn);
        }
    
        @Override
        public Connection getConnection(String username, String password) throws SQLException {
            return null;
        }
    
        @Override
        public <T> T unwrap(Class<T> iface) throws SQLException {
            return null;
        }
    
        @Override
        public boolean isWrapperFor(Class<?> iface) throws SQLException {
            return false;
        }
    
        @Override
        public PrintWriter getLogWriter() throws SQLException {
            return null;
        }
    
        @Override
        public void setLogWriter(PrintWriter out) throws SQLException {
    
        }
    
        @Override
        public void setLoginTimeout(int seconds) throws SQLException {
    
        }
    
        @Override
        public int getLoginTimeout() throws SQLException {
            return 0;
        }
    
        @Override
        public Logger getParentLogger() throws SQLFeatureNotSupportedException {
            return null;
        }
    }
    

    使用 MyDatabase:

    public class MyDataSourceTest {
        @Test
        public void test1(){
            Connection connection = null;
            PreparedStatement stmt =null;
            MyDataSource ds = null;
            try {
                ds = new MyDataSource();
                connection = ds.getConnection();
                stmt = connection.prepareStatement("update" +
                        "account set money=? where name=?;");
                stmt.setDouble(1, 99999);
                stmt.setString(2, "aaa");
                stmt.executeUpdate();
            }catch (Exception e){
                e.printStackTrace();
            }finally {
    //            ds.addBack2Pool(connection);
                //对close方法进行加强;
                JDBCUtils.release(null, stmt , connection);
            }
        }
    }
    

    在创建 Connection 时,利用代理重写了 connection 的 close 方法,这样在使用 JDBCUtils 对 connection 释放时,能主动将其重放回重用池。
    Java 最好用的一点就是有各种成型的 API ,重用池也是。Steve Waldman 维护的开源框架 c3p0 就是非常好用的重用池,使用很方便,先导入 jar 包,使用示例如下:

    public void test1(){
            Connection conn = null;
            PreparedStatement stmt = null;
            try{
                ComboPooledDataSource cpds = new ComboPooledDataSource();
                cpds.setDriverClass( "com.mysql.jdbc.Driver" ); //loads the jdbc driver
                cpds.setJdbcUrl( "jdbc:mysql://localhost:3306/数据库名" );
                cpds.setUser("root");
                cpds.setPassword("11111111");
    
                conn = cpds.getConnection();
                stmt = conn.prepareStatement("update " +
                        "account set money=? where name=?;");
                stmt.setDouble(1, 888888);
                stmt.setString(2, "a");
                stmt.executeUpdate();
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                //对 close 方法进行了加强;
                JDBCUtils.release(null, stmt, conn);
            }
        }
    

    c3po 还能通过 xml 文件和 properties 文件实现预定义配置,感觉超级人性,有木有?更多实现请参看它的 git

    喜欢和关注都是对我的鼓励和支持~

    相关文章

      网友评论

        本文标题:MySQL 事务处理和连接池优化

        本文链接:https://www.haomeiwen.com/subject/uqcmjftx.html