先看没有事务的时候,导致的数据不一致问题。
准备数据:
-- 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(); ... } }