事务
相信大家都在ATM机取过钱,但是是否有人考虑过它的流程是怎样的呢?
我们都知道,假如我们取300块钱,那么当这三百块钱从ATM机出来时,我们的账户相应的会减少300。这两个过程一定是要同时成功才算成功的。否则就会出现账户少了300.但是钱没出来,对于我们来说,我们损失了300.而如果钱出来了但是账户钱没扣掉的话,银行就会损失300.这两种情况都是不可取的。所以就需要保证要么大家一起成功,有一个失败即表示这个过程失败了,就需要还原现场(钱没出来,就需要把账户扣掉的钱给补上)。这,就是事务。
事务都具有以下四个基本特点:
事务的4个属性
Atomic(原子性):事务中包含的操作被看做一个逻辑单元,这个逻辑单元中的操作要么全部成功,要么全部失败(减款,增款必须一起完成)。
● Consistency(一致性):只有合法的数据可以被写入数据库,否则事务应该将其回滚到最初状态。
● Isolation(隔离性):事务允许多个用户对同一个数据进行并发访问,而不破坏数据的正确性和完整性。同时,并行事务的修改必须与其他并行事务的修改相互独立。
● Durability(持久性):事务完成之后,它对于 系统的影响是永久的,该修改即使出现系统故障也将一直保留,真实的修改了数据库
上面说的是现实生活中的例子,那么在JAVA编程中如何保证对数据库的操作是在一个事务下呢?
首先,要让插入或更新操作在同一事务中,那么就需要保证每次只需数据库操作时使用的必须为同一个连接。有人可能会猜到了,用单例,是的,就是单例模式。
spring的事务的确是很强大,且做好了很多封装。如打开连接,关闭连接这种操作我们都不用做了。但是俗话说万变不离其宗,再牛的框架也是要走底层的。原理弄懂了剩下的还难么?
今天我们就来手写一个Spring的底层事务管理。
先看看工程结构。其实也就三个类搞定。ConnectionHandler.java,SingleConnectHandler.java,TransactionManager.java。我待会儿会根据代码一个个讲解一下。
首先是连接ConnectionHandler.java
package spring;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
public class ConnectionHandler {
private Map map = new HashMap<>();//将数据库连接保存在map中, 以DataSource为键
public Connection getConnectionByDatabase(DataSource dataSource) throws SQLException{
Connection conn = map.get(dataSource);
if(conn == null) {
conn = dataSource.getConnection();
map.put(dataSource, conn);
}
return conn;
}
public void openConnection(DataSource dataSource) throws SQLException {
Connection conn = map.get(dataSource);
if(conn.isClosed()) {
conn = dataSource.getConnection();
map.put(dataSource, conn);
}
}
}
我们都知道,springMVC是可以配置多个数据源的,所以这里我们直接以数据源作为map的键。连接作为值保存起来,每次获取连接时,如果map中不存在连接,则重新获取一个连接。这样就保证了在程序运行周期内,同一个数据源获取到的连接都为同一个。也就完成了我们最重要的第一步,每次进行数据库操作时,使用的都是同一个连接。
如果是在单线程模式下,其实这个类已经足够了的。spring可没那么简单,它是一个多线程的。也就是说在并发的情况下,map是线程不安全的。所以这里我们还得再封装一下。
SingleConnectHandler.java
package spring;
import java.sql.Connection;
import java.sql.SQLException;
import javax.sql.DataSource;
public class SingleConnectHandler {
private static ThreadLocal localThread = new ThreadLocal<>();
private static ConnectionHandler getConnectionHahdler() {
ConnectionHandler ch = localThread.get();
if(ch == null) {
ch = new ConnectionHandler();
localThread.set(ch);
}
return ch;
}
public static Connection getConnection(DataSource dataSource) throws SQLException {
return getConnectionHahdler().getConnectionByDatabase(dataSource);
}
public static void openConnection(DataSource dataSource) throws SQLException {
getConnectionHahdler().openConnection(dataSource);
}
}
这个类我们需要一个ThreadLocal类来帮我们保证在多线程下。每个线程能拿到自己变量副本ConnectionHandler。也就是说并发下其实每个线程其实获取到的连接都是不一样的,ThreadLocal这个类。是jdk1.2就有的,我就不多介绍了,有兴趣的朋友可以参考一下这篇文章,我觉得说的蛮好的:
http://www.cnblogs.com/dolphin0520/p/3920407.html
这样我们的项目就可以支持多线程下操作了,完了之后是TransactionManager.java这个类。spring中这个类的功能可强大了。不过我们这个项目毕竟是阉割版的,我就只写了几个常规的方法。如开启事务,关闭事务,提交事务与回滚事务。
package spring;
import java.sql.Connection;
import java.sql.SQLException;
import javax.sql.DataSource;
public class TransactionManager {
private static DataSource dataSource;
private Connection getConnection() throws SQLException {
return SingleConnectHandler.getConnection(dataSource);
}
public TransactionManager(DataSource dataSource) {
TransactionManager.dataSource = dataSource;
}
public void start() throws SQLException {
Connection conn = getConnection();
conn.setAutoCommit(false);
}
public void close() throws SQLException {
Connection conn = getConnection();
conn.close();
}
public void commit() throws SQLException {
Connection conn = getConnection();
conn.commit();
}
public void rollBack() throws SQLException {
Connection conn = getConnection();
conn.rollback();
}
public boolean isAutoCommit() throws SQLException {
return getConnection().isClosed();
}
public void openConnection() throws SQLException{
SingleConnectHandler.openConnection(dataSource);
}
}
这个类完成后到我们的业务层了。userService.java和userDao.java。
userService
package spring;
import java.sql.SQLException;
public interface UserService {
public void buy() throws SQLException;
public void addShops() throws SQLException;
}
userDao
package spring;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Date;
import javax.sql.DataSource;
public class UserDao implements UserService{
private String sql = "insert into user(account,password) values(?,?)";
private DataSource dataSource;
public UserDao(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public void buy() throws SQLException {
Connection conn = SingleConnectHandler.getConnection(dataSource);
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, Thread.currentThread().getName()+"buy");
ps.setString(2, new Date().toString());
ps.execute();
System.out.println("方法buy,---当前线程:"+Thread.currentThread()+"-------使用的Connection:"+conn.hashCode());
}
@Override
public void addShops() throws SQLException {
Connection conn = SingleConnectHandler.getConnection(dataSource);
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, Thread.currentThread().getName()+"addShops");
ps.setString(2, new Date().toString());
ps.execute();
System.out.println("方法addShops,当前线程:"+Thread.currentThread()+"----使用的Connection:"+conn.hashCode());
}
}
业务层我模拟了用户添加商品与购买商品操作。我们将这两个数据库操作放在同一个事务中。注意我后面打印的当前线程与连接的hash值。我们都知道,在程序运行周期内,每个对象都有自己唯一的hash。如果两个hash值相等的话,那么这两个对象则相等。
哦,对了,差点忘了我们还有一个自定义的MyDatasource.java
package spring;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;
import javax.sql.DataSource;
public class MyDatasource implements DataSource{
public static final String driverClassName = "com.mysql.jdbc.Driver";
public static final String password = "root";
public static final String username = "root";
public static final String url = "jdbc:mysql://localhost:3306/qq";
@Override
public PrintWriter getLogWriter() throws SQLException {
// TODO Auto-generated method stub
return null;
}
@Override
public void setLogWriter(PrintWriter out) throws SQLException {
// TODO Auto-generated method stub
}
@Override
public void setLoginTimeout(int seconds) throws SQLException {
// TODO Auto-generated method stub
}
@Override
public int getLoginTimeout() throws SQLException {
// TODO Auto-generated method stub
return 0;
}
@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
// TODO Auto-generated method stub
return null;
}
@Override
public T unwrap(Class iface) throws SQLException {
// TODO Auto-generated method stub
return null;
}
@Override
public boolean isWrapperFor(Class> iface) throws SQLException {
// TODO Auto-generated method stub
return false;
}
@Override
public Connection getConnection() throws SQLException {
Connection conn = null;
try {
Class.forName("com.mysql.jdbc.Driver");
conn = DriverManager.getConnection(url, username, password);
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return conn;
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
// TODO Auto-generated method stub
return null;
}
}
这个类实现了DataSource接口,里面的方法我们就直接重写一个getConnection就好了。这里面是JDBC获取数据库连接的,都是格式固定的,没什么好说的。
完成后进入测试:
package spring;
import java.sql.SQLException;
public class Test {
public static void main(String[] args) throws SQLException, InterruptedException {
MyDatasource b = new MyDatasource();
for(int i=0;i<3;i++) {
new Thread(new Runnable() {
@Override
public void run() {
UserService u = new UserDao(b);
TransactionManager t = new TransactionManager(b);
try {
t.start();
u.buy();
u.addShops();
t.commit();
t.close();
} catch (Exception e) {
try {
t.rollBack();
} catch (SQLException e1) {
e1.printStackTrace();
}
e.printStackTrace();
}
}
}).start();
}
}
}
这个类我新建了三个线程。表示三个用户同时执行添加商品与购买操作。完成后直接run。看结果
总结:
可以看到每个线程的connection都不一样,但是单个线程下使用的connection,包括TransactionManager中的connection都是一致的。这就是一个小型的spring事务管理,其实你翻开spring的源码,基本和这个差不多的。只不过是spring中进行了更多的封装。但是其实有很多时我们不需要的。只有了解了原理,当项目出现问题或者性能瓶颈时,才能更好的发现问题。