简书 賈小強
转载请注明原创出处,谢谢!
情况1:事务a往表X写入1 2 3
由于事务的原子性,要么成功要么失败,所以要么写入1 2 3,要么一个也没有写入
情况2:事务a往表X写入1 2 3,同时事务b进行相同操作
由于事务的隔离性,事务a和事务b互相独立
情况3:事务a往表X写入1 2 3,同时事务b往表Y写入 1 2 3
不论是对同一张表还是同一个数据库的两张表操作,事务的隔离性体现在事务,所以并不会出现所谓的事务a提交了,然后影响事务b的情况
情况4:事务a查找表X没有1则写入1,同时事务b进行相同操作
悲剧:事务a查找发现没有,于是打算写入,事务b在a事务提交前查找也发现没有,于是也认为应该写入,最后都写入导致重复写问题
分析:数据库有两种内部锁:单个SQL语句比如select和insert而言,对于select为读锁,这种锁允许多个事务同时查询一张表,对于insert为写锁,这种锁只允许同一时间只有一个事务能够操作,由于select是幂等的,而insert是原子性的,也就说可以认为单条SQL语句是原子性的,但是select...insert形式两个SQL语句的组合,数据库自身并没有维护其原子性,这在并发的时候很容易出现重复写问题,对于select...update则是丢失修改问题
-
解决方式:
- 采用select...for update,比如Oracle数据库提供了这种形式的语句,在某个事务执行这条SQL语句的时候将阻塞其它事务,所以是安全的,不过由于是悲观锁会阻塞其他所有事务,所以可能导致性能问题
- 采用redis显式构造一个悲观锁,其效果和方式一的一样,安全但是效率不高,相比方式一优点是可以将多个SQL语句写在一个锁之间
- 可以换一个角度改进设计方案,避免这种需要悲观锁的方式
- 改用redis中形式的乐观锁,并发的时候都可以修改,谁先修改成功就算谁的,其他同时修改的发现提交时已经被别的修改了则回退,乐观锁的优点就是让耗时少的先通过,不过并不是所有情况都适合乐观锁,在竞争激烈的情况下可能导致很多的无效尝试
结论:select...insert操作需要加锁同步
情况5:事务a查找表X没有1写入1,查找表Y没有2写入2,同时事务b进行相同操作
- 问题:通过select...for insert或者显式悲观锁给每个表的select...insert加悲观锁,期望缩小锁导致的同步代码范围,提升程序并发效率,是否没问题?
- 悲剧:虽然保证了对某张表的select...insert原子性,但比如事务a查找表X没有1写入1,然后对表Y处理,但是事务还没提交,而事务b在事务a释放表X锁后,获取到表X的锁可以操作表X,进行查找表X没有1写入1的操作,同样导致了重复写问题,或者可能事务a现提交成功写入了id=1的数据,到事务b的时候提交结果发现有的id=1的已经存在了,导致违反数据库完整性约束
- 结论:如果打算对一系列表的select...insert操作看作一个整体事务,那么应该对整个事务加一个显式外部悲观锁,只有这一系列表并不需要看作一个整体事务,那么才可以增多锁和事务,达到减少同步代码的目的,但是锁和事务总是维持同范围的
Happy learning !!