mysql中的锁
首先需要介绍一下mysql的锁。一般我们使用InnoDB
数据库引擎+行级锁,SQL为:SELECT * FROM table where id = 1 for update;
。for update
为排外锁,锁了后其他session无法再加任何锁,需要释放之后才能操作。行级锁要求where
中有唯一索引,如主键或者unique
索引。这种锁允许其他session读,但是不可写。大部分情况下,我们不会使用lock tables TABLE write;
,因为锁表的开支太大,锁行就足够了。
mysql的锁只在事务中生效。单独执行SELECT * FROM table where id = 1 for update;
并不会加锁,需要先执行START TRANSACTION;
或者set autocommit=0;
后锁才生效,并在commit;
或者rollback;
后释放锁。否则,即使执行了for update
语句,其他session也可以对这行数据随意操作。
mysql事务隔离级别
mysql事务隔离级别分为READ UNCOMMITTED
、READ COMMITTED
、REPEATABLE READ
、SERIALIZABLE
。
mysql默认的隔离级别为REPEATABLE READ
,但是阿里云的数据库隔离级别为READ COMMITTED
,可以从SELECT @@GLOBAL.transaction_isolation, @@GLOBAL.transaction_read_only;
得到当前数据库实际隔离级别。
-
READ UNCOMMITTED
事务中任意改动都会直接生效,可被其他session查到。 -
READ COMMITTED
只有commit的操作才会被其他session查到。 -
REPEATABLE READ
只有commit的操作才会被其他session查到。但是在同一个事务处理中,读取的数据是一致的。如A session开始之后,B session修改数据A为数据B并commit,A session读到的数据依旧是A。 -
SERIALIZABLE
串行操作。在事务中,所有读取操作都会加共享锁,所以必须等写操作完成之后才能读。
从上往下,隔离程度越来越高。
例子如下:
--session 1
START TRANSACTION;
SELECT * FROM table where id = 1 for update;
-- 此时字段a是1
update table set a=100 where id=1;
--session 2
SELECT * FROM table where id = 1;
--不能加for update,否则会被锁。返回值a在READ UNCOMMITTED 下结果为100,其他情况下为1。
--session 1
SELECT * FROM table where id = 1 for update;
--同session可以随便重复锁。返回值a是100
--session 2
START TRANSACTION;
SELECT * FROM table where id = 1;
--READ UNCOMMITTED 下结果为100,其他情况下为1。
--session 1
COMMIT;
--session 2
SELECT * FROM table where id = 1;
--READ UNCOMMITTED和READ COMMITTED结果为100,其他为1,这也是REPEATABLE READ 重复读的含义所在。
kotlin中的具体实践
这里使用的是kotlin+jooq。注:@Transactional
注解可以加在interface
内的方法上。
@Transactional
override fun testRollBackWithT() {
val res=dsl.selectFrom(Tables.T_TABLE)
.where(Tables.T_TABLE.ID.eq(1))
.forUpdate()
.fetchOne()
res.time=300
res.update()
throw Exception("test")
}
必须开启Transactional
注解,否则无效。此注解在这里的主要作用,是在调用的时候执行set autocommit=0;
,调用成功后执行commit;
,以及在失败后执行rollback;
。不在Transactional
作用域内进行的锁是无效的。
此注解本质是用cglib进行切片,处理事务初始化(即set autocommit=0;
)等事项。因此如果在类的内部不带注解的方法调用带注解的方法,会导致注解无效。此外,如果嵌套注解,即带注解的A调用带注解的B,整个注解执行过程中任意出错都会导致整体回滚。不管嵌套多少层注解,事务初始化只会执行一次,commit也只会执行一次。任意一个带注解的方法执行失败,都会标注整个事务失败。例子如下:
@Transactional
fun test() {
//此方法带@Transactional注解,不会抛异常
updateWithout()
try {
//此方法带@Transactional注解,会抛异常
updateWith()
}catch (e:Exception){
}
}
在例子中,updateWith()
带@Transactional注解并抛异常了,即使被上层捕获,依然会触发Transactional的回滚标记。虽然由于嵌套导致不会立即回滚,但是在上层运行结束触发commit
的时候,会出现异常最终rollback,test方法内部所有数据库操作都会无效,如updateWithout()
方法。