高并发与锁
DRP学习中,我们对可能引起并发操作的情况使用了锁,这次先理论上看看并发控制与锁的一些内容吧。
并发控制
在多用户环境中,在同一时间可能会有多个用户更新相同的记录,这会产生冲突。这就是并发性。典型的冲突有:
1、丢失更新(Lost updates)
一个事务的更新覆盖了其它事务的更新结果,就是所谓的更新丢失。例如:用户A把值从6改为2,用户B把值从2改为6,则用户A丢失了他的更新。
2、脏读(Dirty reads)
当一个事务读取其它完成一半事务的记录时,就会发生脏读取。例如:用户A,B看到的值都是6,用户B把值改为2,用户A读到的值仍为6。
3、不可重复读(Non-repeatable reads)
当一个进程读取了一笔数据后,另一个进程更新了同一笔数据,然后第一个进程再次读取同一笔数据,却得到了 与第一次读取不同的结果。
在事务A更新记录之后(update Customers set Name = 'B' where Name = 'A'),事务B读取相同记录(select Name form Customers where Name = 'A'),但事务B拿到的是事务A更新之后的数据(Customers.Name的值为'B'),在事务B读取记录之后,事务A进行了事务回滚(Customers.Name的值为'A'),导致事务B的数据是不真实的。
4、幻读(Phantoms)
幻读与脏读的相似之处在于:两者都是两次读取的结果不一致。
不同之处在于:幻读是两次读取的记录数量不一致,而脏读是两次读取的记录的数据不一致。
事务A读取记录之后(select * from Customers where Name like 'A%'),事务B又插入了符合事务A读取条件的新记录(insert into Customers(Name) values('AAA')),那么当事务A再用相同条件读取记录时,得到的集合却与上一次读取不同(多了记录)。
解决方案:线程同步和加锁
通常我们解决并发问题使用同步和加锁两种方式。
同步中,我们通过Java中的synchronized关键字来实现,但这样通常会带来性能和效率上的一些问题。
这次,我们主要讲解锁。锁机制中,我们可以根据不同情况使用悲观锁或乐观锁。
悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
对于悲观锁,我们可以提供一个值作为锁的参数。比如一个键默认会被锁定15秒。15秒过后,如果你还没有手动的释放锁,那么使程序自动的为你释放。
悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。
与之对应的--乐观锁:
乐观锁(Optimistic Lock), 就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。
而乐观锁机制在一定程度上解决了数据库性能开销这个问题。它大多是基于数据版本( Version )记录机制实现。
可以使用版本号、时间戳等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。
比较
1、两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行retry,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。
2、悲观锁和乐观锁最大的区别是是否一直锁定资源,悲观锁在事物的全流程锁定数据,乐观锁不锁定数据(用读写锁是阻塞事物,而用乐观锁则会导致回滚,这个是一种事物冲突后的不同锁的表象)。
3、悲观锁和乐观锁都是为了解决丢失更新问题或者是脏读。悲观锁和乐观锁的重点就是是否在读取记录的时候直接上锁。悲观锁的缺点很明显,需要一个持续的数据库连接,这在web应用中已经不适合了。