保存点
在MySQL中, 保存点SAVEPOINT属于事务控制处理部分。利用SAVEPOINT可以回滚指定部分事务,从而使事务处理更加灵活和精细。SAVEPOINT相关的SQL语句如下
SAVEPOINT identifier
设置SAVEPOINT。如果重复设置同名savepoint,新的会覆盖老的.
RELEASE SAVEPOINT identifier
释放SAVEPOINT。
ROLLBACK [WORK] TO [SAVEPOINT] identifier
回滚到指定的SAVEPOINT。
InnoDB内部实现
保存点跟事务有关,因此我们这里只讨论事务引擎InnoDB的savepoint实现。
首先看server层保存点结构:
struct st_savepoint {
struct st_savepoint *prev;
char *name; /* 名字 */
uint length;
Ha_trx_info *ha_list; /* 设置savepoint时已注册的插件 */
/** State of metadata locks before this savepoint was set. */
MDL_savepoint mdl_savepoint;
};
InnoDB层保存点结构:
保存点结构:
struct trx_named_savept_t{
char* name; /*!< savepoint name */
trx_savept_t savept; /*!< the undo number corresponding to
the savepoint */
ib_int64_t mysql_binlog_cache_pos;
/*!< the MySQL binlog cache position
corresponding to this savepoint, not
defined if the MySQL binlogging is not
enabled */
UT_LIST_NODE_T(trx_named_savept_t)
trx_savepoints; /*!< the list of savepoints of a
transaction */
};
链表存储事务上的所有保存点:
trx_t
{
UT_LIST_BASE_NODE_T(trx_named_savept_t)
trx_savepoints;
…….
}
保存点最重要的信息,事务回滚日志的序号:
struct trx_savept_t{
undo_no_t least_undo_no; /*!< least undo number to undo */
};
SAVEPOINT与UNDO日志
事务回滚通过回滚UNDO日志来实现,同样,回滚至保存点也是通过应用UNDO日志来实现。
InnoDB事务在每次修改操作时都会记录UNDO日志,参见函数trx_undo_report_row_operation,每次操作都会记录UNDO日志序号记为undo_no,每次操作undo_no都会递增。回滚只需要反向应用UNDO日志即可。 SAVEPOINT与undo_no是一一对应的。
create table t1(c1 int primary key);
begin;
insert into t1 values(1);
savepoint a;
insert into t1 values(2);
savepoint b;
insert into t1 values(3);
rollback to savepoint a;
commit;
SAVEPOINT与BINLOG
InnoDB开启binlog的情况下,savepoint回滚的那段操作不应记录binlog. 我们知道,事务执行过程中产生的binlog先写入cache中,提交时再将cache中的数据写binlog文件中。 然而,savepoint回滚时,binlog还在cache中,那么被回滚的那段操作的binlog需要从cache中清理掉。
设置savepoint时,记录binlog在cache中起始位置。
trans_savepoint
->ha_savepoint
->binlog_savepoint_set
->binlog_trans_log_savepos
回滚至savepoint时,从保存的起始位置清理cache
trans_rollback_to_savepoint
->ha_rollback_to_savepoint
->binlog_savepoint_rollback
->binlog_trx_cache_data::restore_savepoint
->binlog_cache_data::truncate
->reinit_io_cache
SAVEPOINT与锁
回滚保存点以后,此保存点以后的保存点都会释放,但此保存点以后InnoDB层操作加的锁不会释放。这里不释放锁,是为了不破坏两阶段锁协议,减少死锁的发生。
而对于MDL(metadate lock)锁,在binlog关闭的情况下可以提前释放。 而binlog开启的情况下,需考虑如下情况:
如果操作的仅是InnoDB表且InnoDB层没有加锁,则MDL锁可以释放,否则,不能释放。 InnoDB层持有锁,如果释放MDL可能出现死锁。考虑如下情况: trx 1: rollback to savepoint xxx; InnoDB层持有t1的行锁,释放t1的MDL trx 2: 操作t1; 持有t1的MDL, 等待t1行锁 trx 1: 再次操作t1; 等待t1的MDL锁,从而构成死锁。
如果操作中有非事务引擎,则不能释放MDL锁。 如果是非事务引擎,例如t1为MyiSAM表。 ... begin; insert into t1 values(1); savepoint a; insert into t1 values(2); rollback to savepoint a; commit; ... savepoint和rollback to savepoint之间的sql都会写入binlog. 如果提前释放MDL,其他会话drop table t1可以成功,这样会导致应用binlog时,执行insert into t1 values(2);会找不到表t1。
SAVEPOINT作用域
按官方文档 中,store function和trigger会重新开启新的savepoint作用域, store function和trigger完成后老的savepoint作用域重新可用。
A new savepoint level is created when a stored function is invoked or a trigger is activated. The savepoints on previous levels become unavailable and thus do not conflict with savepoints on the new level. When the function or trigger terminates, any savepoints it created are released and the previous savepoint level is restored.
delimiter //
drop procedure if exists p1//
create procedure p1()
begin
release savepoint a;
end//
delimiter ;
begin;
savepoint a;
call p1();
rollback to savepoint a;
ERROR 1305 (42000): SAVEPOINT a does not exist
从结果来看与官方文档描述并不一致。
实际从代码中上看,stored function和trigger并没有开启独立的事务,而是与调用着共用同一事务。savepoint都在同一事务的链表中,因此store function和trigger中的savepoint作用域和调用者相同。
官方对savepoint的实现并不彻底。
匿名SAVEPOINT
实际上,InnoDB事务中每个语句执行前都会记录一个匿名savepoint;如果当前语句执行失败,不会回滚整个事务,而是利用这个匿名savepoint回滚失败的语句。
struct trx_t{
trx_savept_t last_sql_stat_start; //匿名savepoint
......