问题描述: 一个优惠券活动,用户可以领取优惠券,但是一个优惠券活动领取数量有限制,所以用户在领取的时候就需要先统计一下以领用的优惠券数量。 然后在生成这张优惠券领取记录。那么此时就会出现并发问题,当多个用户领取同一个优惠券活动的时候,他们统计的优惠券已领数量小于限定可领取数量,所以都可以执行生成 优惠券领取记录的操作,但是剩下的可领取数量可能小于这些用户数量。

如何来解决这个问题呢,首先我们会想到,在程序中使用synchronized关键字来锁住领取优惠券的方法,那么就可以实现,当一个人在领取优惠券的时候其他人等待,但是程序 都是分发到多台服务器的,在分布式的情况下,这种方法并没有效果,因为程序中所能做到的只是锁住单台服务器上面的操作。

既然程序中无法实现,那么我们就想到利用数据库来实现,因为我现在做的这个项目,数据库是没用从库的,只有一台数据库服务器,如果数据库也分布式了,那就要另求它法了。

想到用数据库解决后,首先想到的是利用事务的原子性来解决,就是统计已领用数量的操作和生成优惠券记录的操作合成为一个事务,表面上感觉可以了,但其实这样做也是错误的。 因为多个事务同时处理时,情况也是一样的,同样会出现并发问题。

所以单单利用事务还是不行的,还必须使用数据库的锁机制。

InnoDB实现了以下两种类型的行锁。

  • 共享锁(S):允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁(可以读不能写)。
  • 排他锁(X):允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁(不能读也不能写)。

锁的范围:行锁: 对某行记录加上锁 ; 表锁: 对整个表加上锁

此处我们需要使用的是排它锁,就是当一个事物在进行操作的时候,其他事物不能去统计已经领取优惠券的数量,只有领取完成之后才能统计。锁的范围是行。

使用排他锁的方法其实是悲观锁机制,我们还有乐观锁,接下来就先介绍下二者区别。

悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。

悲观锁假定其他用户企图访问或者改变你正在访问、更改的对象的概率是很高的,因此在悲观锁的环境中,在你开始改变此对象之前就将该对象锁住,并且直到你提交了所作的更改之后才释放锁。 悲观的缺陷是不论是页锁还是行锁,加锁的时间可能会很长,这样可能会长时间的限制其他用户的访问,也就是说悲观锁的并发访问性不好。

乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。

乐观锁不能解决脏读的问题。 乐观锁则认为其他用户企图改变你正在更改的对象的概率是很小的,因此乐观锁直到你准备提交所作的更改时才将对象锁住,当你读取以及改变该对象时并不加锁。 可见乐观锁加锁的时间要比悲观锁短,乐观锁可以用较大的锁粒度获得较好的并发访问性能。但是如果第二个用户恰好在第一个用户提交更改之前读取了该对象,那么当他完成了自己的更改进行提交时, 数据库就会发现该对象已经变化了,这样,第二个用户不得不重新读取该对象并作出更改。这说明在乐观锁环境中,会增加并发用户读取对象的次数。

如果使用乐观锁的方式,我们就需要给数据库有当前优惠券活动已领取数量的字段,每次我们修改这个字段的值时对其就行判断,如果他小于可领取数量,那么久可以插入 这条优惠券记录。这样判断和更新是在同一条sql中,这样就利用一条sql的原子性,避免并发冲突。

reference: