在最近的开发中,碰到一个需求签到,每个用户每天只能签到一次,那么怎么去判断某个用户当天是否签到呢?因为当属表设计的时候,每个用户签到一次,即向表中插入一条记录,根据记录的数量和时间来判断用户当天是否签到。
这样的话就会有一个问题,如果是在网速过慢的情况下,用户多次点击签到按钮,那么变会发送多次请求,可能会导致一天多次签到,重复提交的问题,那么很自然的想到用事务。这次用的是spring + mybtais的框架,一开始设计的代码大致如下:
public boolean signIn(SignInHistory signInHistory) { //编程式开启事务 TransactionTemplate template = new TransactionTemplate(transactionManager); boolean result = (boolean) template.execute(new TransactionCallback<Object>() { public Object doInTransaction(TransactionStatus transactionStatus) { try { //获取用户所有签到记录 List<SignInHistory> SignInHistoryList = signInMapper.select(signInHistory); //如果当前时间和List中某条签到时间相同,则当天已签到,代码略去 //插入签到历史表 signInMapper.insert(signInHistory); } catch (Exception e) { transactionStatus.setRollbackOnly(); logger.error(e); return false; } return true; } }); }
但是在测试中,发现还是会发生重复提交。
那么看mysql文档
Consistent read is the default mode in which InnoDB processes SELECT statements in READ COMMITTED andREPEATABLE READ isolation levels. A consistent read does not set any locks on the tables it accesses, and therefore other sessions are free to modify those tables at the same time a consistent read is being performed on the table.
Mysql文档中也有相关说明:如果是在read committed和repeatab read 下,普通的select语句并不会进行锁操作。其它session可以照常更新或插入操作。
所以在这里面就可以发现,如果只是普通select,不管在不在事务中,mysql都不会将select加锁,所以根本无法阻止其它事务插入记录。
由此可以得出一个理解,
事务隔离级别
数据库事务隔离级别,只是针对一个事务能不能读取其它事务的中间结果。
Read Uncommitted(读取未提交内容)
在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为脏读(Dirty Read)。
Read Committed(读取提交内容)
这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别也支持所谓的不可重复读(Nonrepeatable Read),因为同一事务的其他实例在该实例处理其间可能会有新的commit,所以同一select可能返回不同结果。
Repeatable Read(可重读)
这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读(Phantom Read)。简单的说,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的"幻影" 行。InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题。
Serializable(可串行化)
这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。
这四种隔离级别采取不同的锁类型来实现,若读取的是同一个数据的话,就容易发生问题。例如:
脏读(Drity Read):某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的。
不可重复读(Non-repeatable read):在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新的原有的数据。
幻读(Phantom Read):在一个事务的两次查询中数据笔数不一致,例如有一个事务查询了几列(Row)数据,而另一个事务却在此时插入了新的几列数据,先前的事务在接下来的查询中,就会发现有几列数据是它先前所没有的。
事务传播级别
数据库事务传播级别,指的是事务嵌套时,应该采用什么策略,即在一个事务中调用别的事务,该怎么办
假如有一下两个事务:
ServiceA {
void methodA() {
ServiceB.methodB();
}
}
ServiceB {
void methodB() {
}
}
1: PROPAGATION_REQUIRED
加入当前正要执行的事务不在另外一个事务里,那么就起一个新的事务
比如说,ServiceB.methodB的事务级别定义为PROPAGATION_REQUIRED, 那么由于执行ServiceA.methodA的时候,
ServiceA.methodA已经起了事务,这时调用ServiceB.methodB,ServiceB.methodB看到自己已经运行在ServiceA.methodA
的事务内部,就不再起新的事务。而假如ServiceA.methodA运行的时候发现自己没有在事务中,他就会为自己分配一个事务。
这样,在ServiceA.methodA或者在ServiceB.methodB内的任何地方出现异常,事务都会被回滚。即使ServiceB.methodB的事务已经被
提交,但是ServiceA.methodA在接下来fail要回滚,ServiceB.methodB也要回滚
2: PROPAGATION_SUPPORTS
如果当前在事务中,即以事务的形式运行,如果当前不再一个事务中,那么就以非事务的形式运行
3: PROPAGATION_MANDATORY
必须在一个事务中运行。也就是说,他只能被一个父事务调用。否则,他就要抛出异常
4: PROPAGATION_REQUIRES_NEW
这个就比较绕口了。比如我们设计ServiceA.methodA的事务级别为PROPAGATION_REQUIRED,ServiceB.methodB的事务级别为PROPAGATION_REQUIRES_NEW,
那么当执行到ServiceB.methodB的时候,ServiceA.methodA所在的事务就会挂起,ServiceB.methodB会起一个新的事务,等待ServiceB.methodB的事务完成以后,
他才继续执行。他与PROPAGATION_REQUIRED 的事务区别在于事务的回滚程度了。因为ServiceB.methodB是新起一个事务,那么就是存在
两个不同的事务。如果ServiceB.methodB已经提交,那么ServiceA.methodA失败回滚,ServiceB.methodB是不会回滚的。如果ServiceB.methodB失败回滚,
如果他抛出的异常被ServiceA.methodA捕获,ServiceA.methodA事务仍然可能提交。
5: PROPAGATION_NOT_SUPPORTED
当前不支持事务。比如ServiceA.methodA的事务级别是PROPAGATION_REQUIRED ,而ServiceB.methodB的事务级别是PROPAGATION_NOT_SUPPORTED ,
那么当执行到ServiceB.methodB时,ServiceA.methodA的事务挂起,而他以非事务的状态运行完,再继续ServiceA.methodA的事务。
6: PROPAGATION_NEVER
不能在事务中运行。假设ServiceA.methodA的事务级别是PROPAGATION_REQUIRED,而ServiceB.methodB的事务级别是PROPAGATION_NEVER ,
那么ServiceB.methodB就要抛出异常了。
7: PROPAGATION_NESTED
理解Nested的关键是savepoint。他与PROPAGATION_REQUIRES_NEW的区别是,PROPAGATION_REQUIRES_NEW另起一个事务,将会与他的父事务相互独立,
而Nested的事务和他的父事务是相依的,他的提交是要等和他的父事务一块提交的。也就是说,如果父事务最后回滚,他也要回滚的。
而Nested事务的好处是他有一个savepoint。
行级锁
如果有两个事务A,B 都有 read 和write操作,如果逻辑是如果表中没有记录则插入,那么因为read操作并没有加锁,A,B进行read操作时,有可能表中都没有记录,那么事务A,B都会进行插入操作,表中将会有两条记录。
如果要保证在事务并发时,每条事务读取到的数据都是最新的,那么只能采用锁。
在select语句后加上 FOR UPDATE,再测试,重复提交的问题被解决了。
但是问题又来了,如果在select语句后加上LOCK IN SHARE MODE,那么会报死锁的错误。
查看mysql文档:
LOCK IN SHARE MODE会在读取的行上加共享锁,其他session只能读不能修改或删除,如果有其他事务修改了记录,那么会等待事务提交后,再读取。
FOR UPDATE在读取行上设置一个排他锁。阻止其他session读取或者写入行数据
这样看起来似乎就能解释为什么使用LOCK IN SHARE MODE会产生死锁了,假如两个事务A、B都读取同一行记录,那么在这一行就加上了共享锁,但是A和B事务中都需要修改这一行,那么都要等待对方释放共享锁才能进行,结果造成了死锁。
只能使用for update 来防止死锁和重复插入。
这就是mysql的两种行级锁的区别。