1. 程式人生 > >深入理解spring事務底層實現原理

深入理解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中進行了更多的封裝。但是其實有很多時我們不需要的。只有瞭解了原理,當專案出現問題或者效能瓶頸時,才能更好的發現問題。