深入理解spring事務底層實現原理
事務
相信大家都在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<DataSource, Connection> 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<ConnectionHandler> 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> T unwrap(Class<T> 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中進行了更多的封裝。但是其實有很多時我們不需要的。只有瞭解了原理,當專案出現問題或者效能瓶頸時,才能更好的發現問題。