本地事务即对一个数据源进行操作。大多数数据库支持事务。
先看没有事务的时候,导致的数据不一致问题。
准备数据:
-- MySQL
-- Create the database
DROP DATABASE IF EXISTS spring;
CREATE DATABASE spring
-- Drop three tables if exist
DROP TABLE IF EXISTS FRUIT;
DROP TABLE IF EXISTS FRUIT_STOCK;
DROP TABLE IF EXISTS ACCOUNT;
-- 水果表
CREATE TABLE FRUIT (
ID INT NOT NULL,
FRUIT_NAME VARCHAR(100) NOT NULL,
PRICE INT,
PRIMARY KEY (ID)
);
-- 水果存货表
CREATE TABLE FRUIT_STOCK (
ID INT NOT NULL,
STOCK INT NOT NULL,
PRIMARY KEY (ID),
CHECK (STOCK >= 0) -- analyzed but ignored by MySQL
);
-- 账户表
CREATE TABLE ACCOUNT (
USERNAME VARCHAR(50) NOT NULL,
BALANCE INT NOT NULL,
PRIMARY KEY (USERNAME),
CHECK (BALANCE >= 0)
);
-- Add initial data
INSERT INTO FRUIT(ID, FRUIT_NAME, PRICE) VALUES(1, 'Apple', 10);
INSERT INTO FRUIT_STOCK(ID, STOCK) VALUES(1, 10);
INSERT INTO ACCOUNT(USERNAME, BALANCE) VALUES('user1', 20);
DELIMITER $$
-- MySQL不支持check,使用触发器来检查约束,不满足时触发异常:
CREATE TRIGGER ACCOUNT_BALANCEGT0 BEFORE UPDATE ON account
FOR EACH ROW
BEGIN
IF NEW.balance < 0 THEN -- NEW代表更新后的记录
CALL xxx_yyy();
UPDATE _xxx_yyy SET X = 1; -- 引发异常
END IF;
END$$
DELIMITER ;
Maven依赖:
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.26</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>3.1.2.RELEASE</version>
</dependency>
接口:
public interface FruitShop {
// fruitId - 水果ID, userName - 用户号, count - 购买数量
boolean purchase(int fruitId, String userName, int count);
}
实现类:
public class JdbcFruitShop implements FruitShop {
static final Logger LOGGER = LoggerFactory.getLogger(JdbcFruitShop.class);
@Override
public boolean purchase(int fruitId, String userName, int count) {
Connection conn = null;
try {
Class.forName("com.mysql.jdbc.Driver"); // Load the driver
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/spring?characterEncoding=utf8", "spring",
"123456"); // Get the connection
// Query the price
PreparedStatement ps1 = conn.prepareStatement("SELECT PRICE FROM FRUIT WHERE ID = ?");
ps1.setInt(1, fruitId);
ResultSet rs = ps1.executeQuery();
int price = 0;
if (rs.next()) {
price = rs.getInt(1);
}
ps1.close();
// Update the stock
PreparedStatement ps2 = conn.prepareStatement("UPDATE FRUIT_STOCK SET STOCK = STOCK - ? WHERE ID = ?");
ps2.setInt(1, count);
ps2.setInt(2, fruitId);
ps2.executeUpdate();
ps2.close();
// Update the balance
PreparedStatement ps3 = conn
.prepareStatement("UPDATE ACCOUNT SET BALANCE = BALANCE - ? WHERE USERNAME = ?");
ps3.setInt(1, price * count);
ps3.setString(2, userName);
ps3.executeUpdate();
ps3.close();
} catch (SQLException e) {
LOGGER.error("Purchase error:", e);
} catch (ClassNotFoundException e) {
LOGGER.error("driver Loading error:", e);
} finally {
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
LOGGER.error("Connection closing error:", e);
}
}
}
return true;
}
}
Spring配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<bean id="fruitShop" class="com.john.tx.service.impl.JdbcTxFruitShop"/>
</beans>
测试:
@Resource
FruitShop fruitShop;
@Test
public void test() {
int fruitId = 1;
String userName = "user1";
int count = 3;
fruitShop.purchase(fruitId, userName, count);
}
用户user1的余额是20,买了3个单价为10的苹果,余额不够支付,报错。但是数据处于不一致状态:fruit_stock表的苹果存量由10个减为7个,而账户表的余额还是20,需要使用事务。
使用JDBC的事务操作。
public class JdbcTxFruitShop implements FruitShop {
@Override
public boolean purchase(int fruitId, String userName, int count) {
...
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/spring?characterEncoding=utf8", "spring", "123456"); // Get the connection
conn.setAutoCommit(false); // 取消自动提交
...
ps3.close();
conn.commit(); // 提交事务
...
}
}
上面的数据源是写在代码里的,每次修改都需要重新编译,可以将其放在Spring配置中:
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/spring?characterEncoding=utf8" />
<property name="username" value="spring" />
<property name="password" value="123456" />
</bean>
<bean id="fruitShop" class="com.john.tx.service.impl.JdbcTxFruitShop">
<property name="dataSource" ref="dataSource" />
</bean>
public class JdbcTxFruitShop implements FruitShop {
private DataSource dataSource;
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public boolean purchase(int fruitId, String userName, int count) {
...
conn = dataSource.getConnection();
...
}
}