本文涉及到较多的数据库事务概念,阅读本文前请确保具备必要的相关知识。
事务特性
Atomicity-原子性:同一个事务的所有操作,要么全部完成,要么全部不完成
Consistency-一致性:在事务开始之前和结束之后,数据库的完整性未被破坏(符合预期)
Isolation-隔离性:可以防止多个事务交叉执行时导致数据不一致;隔离级别由低到高依次为:
读未提交(Read uncommitted)
读已提交(read committed,RC),Oracle等多数数据库的默认隔离级别
可重复读(repeatable read,RR),Mysql的默认隔离级别
串行化(Serializable)
思考:为什么Mysql的默认隔离级别和其它不一样,是否合理?
Durability-持久性:事务结束后,对数据的修改就是永久性的
分布式事务
一致性问题
CAP原则
Consistency-一致性:更新操作成功后,所有节点在同一时间的数据完全一致
Availability-可用性:用户访问数据时,系统是否能在正常响应时间返回结果
Partition tolerance-分区容错性:系统在遇到部分节点或网络分区故障的时候,仍然能够提供满足一致性和可用性的服务
当前普遍认为CAP三者只能同时满足其二,而P通常是需要保证的(因为节点故障或网络异常难以避免),Zookeeper选择的是CP(选举期间无法对外提供服务,即不保证A),Eureka选择的是AP(选举期间也可以提供服务,但不保证各节点数据完全一致)
BASE理论
Basically Available,基本可用
Soft state,软状态/柔性事务
Eventual consistency,最终一致性
CAP中一致性和可用性权衡的结果,翻译成人话就是既然强一致性难以做到,那退而求其次,只要最终数据是一致的,中间短暂的不一致通常认为是可以忍受的。
常见解决方案
2PC
3PC
TCC
基于本地消息表的最终一致性
基于事务消息的最终一致性
事务模式
AT模式
TCC模式
SAGA模式
XA模式
TM
RM
注册中心
配置中心
事务原理
基本概念
本地事务
本地事务Demo
@Service
public class StorageService {
@Autowired
private DataSource dataSource;
public void batchUpdate() throws SQLException {
Connection connection = null;
PreparedStatement preparedStatement = null;
try {
connection = dataSource.getConnection();
connection.setAutoCommit(false);
String sql = "update storage_tbl set count = ? where id = ? and commodity_code = ?";
preparedStatement = connection.prepareStatement(sql);
preparedStatement.setInt(1, 100);
preparedStatement.setLong(2, 1);
preparedStatement.setString(3, "2001");
preparedStatement.executeUpdate();
connection.commit();
} catch (Exception e) {
throw e;
} finally {
IOutils.close(preparedStatement);
IOutils.close(connection);
}
}
}
事务核心对象
public class BusinessServiceImpl implements BusinessService {
private static final Logger LOGGER = LoggerFactory.getLogger(BusinessService.class);
@Autowired
private StockService stockService;
@Autowired
private OrderService orderService;
@Override
@GlobalTransactional(name = "dubbo-demo-tx") // 该注解表明开启一个全局事务
public void purchase(String userId, String commodityCode, int orderCount) {
LOGGER.info("purchase begin ... xid: " + RootContext.getXID());
stockService.deduct(commodityCode, orderCount);
orderService.create(userId, commodityCode, orderCount);
}
}
分支事务Demo
public class StockServiceImpl implements StockService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public void deduct(String commodityCode, int count) {
jdbcTemplate.update("update stock_tbl set count = count - ? where commodity_code = ?", new Object[]{count, commodityCode});
}
}
OrderService与StockService实现类似,再来看下配置
分支事务配置文件
<-- 正常数据源 -->
<bean name="stockDataSource" class="com.alibaba.druid.pool.DruidDataSource"
init-method="init" destroy-method="close">
<property name="url" value="${jdbc.stock.url}"/>
<property name="username" value="${jdbc.stock.username}"/>
<property name="password" value="${jdbc.stock.password}"/>
</bean>
<-- 代理数据源 -->
<bean id="stockDataSourceProxy" class="io.seata.rm.datasource.DataSourceProxy">
<constructor-arg ref="stockDataSource"/>
</bean>
<-- 注入代理数据源 -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="stockDataSourceProxy"/>
</bean>
事务流程
上面借用一张网络上的总体流程图说明下大致流程,下面是完整详细的事务流程。
流程详解
undo log记录
先来看下undo log大概长啥样
undo log记录
{
"branchId": 641789253, // 分支事务id
"undoItems": [{
"afterImage": { // 后镜像
"rows": [{
"fields": [{
"name": "id",
"type": 4,
"value": 1
}, {
"name": "name",
"type": 12,
"value": "GTS"
}, {
"name": "since",
"type": 12,
"value": "2014"
}]
}],
"tableName": "product"
},
"beforeImage": { // 前镜像
"rows": [{
"fields": [{
"name": "id",
"type": 4,
"value": 1
}, {
"name": "name",
"type": 12,
"value": "TXC"
}, {
"name": "since",
"type": 12,
"value": "2014"
}]
}],
"tableName": "product"
},
"sqlType": "UPDATE"
}],
"xid": "xid:xxx" // 全局事务id
}
Q1:前后镜像会出现数据不一致问题吗?
A:由于本地事务执行时已经加过(本地)锁了,所以sql执行前后的数据是一致的。
Q2:回滚时直接恢复到前镜像就可以了,那后镜像的作用是什么?
A:保存后镜像的目的是为了在恢复数据时验证这期间数据是否被修改过(未纳入全局事务管理的其它程序或人为修改),可以配置发现不一致时的处理策略。
那么如何防止被其它程序修改呢?
全局锁
使用限制
设计总结