五一假期快到了,决定趁着假期出去玩玩。我和女朋友商量好,我负责制定行程,她负责采购旅游用品。一切都很好,我在各个公司比较价格,不知道怎么回事,我女朋友买的时候很不高兴。并发控制当程序中可能存在并发时,我们需要通过一定的手段来保证并发情况下数据的准确性。这种方法保证了用户与其他用户一起操作时,得到的结果和他单独操作时祈祷的结果是一样的。这种方法称为并发控制。并发控制的目的是确保一个用户的工作不会不合理地影响另一个用户的工作。没有做好并发控制,可能会导致脏读、幻读、不可重复读等问题。我们常说的并发控制,一般与数据库管理系统(DBMS)有关。DBMS中并发控制的任务是在多个事务同时访问数据库中的同一数据时,保证事务的隔离性和统一性不被破坏。数据库统一性。实现并发控制的主要手段大致可以分为两种:乐观并发控制和悲观并发控制。在开始介绍之前,先明确一下:无论是悲观锁还是乐观锁,都是人们定义的一个概念,可以看作是一种思想。事实上,不仅关系数据库系统中存在乐观锁和悲观锁的概念,Memcache、Hibernate、Tair等都有类似的概念。因此,不要拿乐观锁、悲观锁和其他数据库锁进行比较。悲观锁当我们要修改数据库中的一条数据时,为了避免被其他人同时修改,最好的办法就是直接对数据加锁,防止并发。这种利用数据库锁机制在修改数据之前先加锁,再修改的方式称为悲观并发控制(也称为“悲观锁”,PessimisticConcurrencyControl,简称“PCC”)。之所以叫悲观锁,是因为它是一种对数据修改持悲观态度的并发控制方式。我们一般认为数据被并发修改的概率比较高,所以在修改前需要加锁。悲观并发控制实际上是一种“先获取锁再访问”的保守策略,为数据处理的安全性提供了保障。但是从效率上来说,加锁机制会给数据库带来额外的开销,增加死锁的几率。此外,它还会降低并行度。如果一个事务锁定了一行数据,其他事务必须等待该事务处理完才能处理该行数据。乐观锁乐观锁(OptimisticLocking)是相对于悲观锁而言的。乐观锁假设数据在正常情况下不会引起冲突。因此,当数据提交更新时,会正式检测数据是否冲突。如果发现冲突,它会返回用户的错误信息,让用户决定如何处理。与悲观锁相比,乐观锁在处理数据库时不使用数据库提供的锁机制。实现乐观锁的一般方式是记录数据版本。乐观并发控制认为事务之间数据竞争的概率比较小,所以尽量直接做,提交之前不加锁,所以不会出现锁死锁的情况。悲观锁的实现悲观锁的实现往往依赖于数据库提供的锁机制。在数据库中,悲观锁的过程是这样的:在修改记录之前,先尝试给记录加排他锁(exclusivelocking)。如果加锁失败,说明记录正在被修改,当前查询可能要等待或者抛出异常。具体响应方式由开发者根据实际需要确定。如果锁成功锁定,则可以修改记录,事务完成后解锁。其间如果有其他操作对记录进行修改或加排它锁,会等待我们解锁或直接抛出异常。下面以比较常用的MySQLInnodb引擎为例,来说明如何在SQL中使用悲观锁。要使用悲观锁,我们必须关闭MySQL数据库的autocommit属性,因为MySQL默认使用Autocommit模式。也就是说,当你执行更新操作时,MySQL会立即提交结果。设置自动提交=0。下面举个简单的例子,比如用淘宝的下单流程来说明悲观锁的使用方法://0.开始交易开始;//1。查询商品库存信息selectquantityfromitemswhereid=1forupdate;//2.修改商品库存为2updateitemssetquantity=2whereid=1;//3.提交事务commit;上面,在修改id=1的记录之前,先通过forupdate加锁,然后再修改。这是典型的悲观锁策略。如果上面修改库存的代码并发,只有一个线程可以启动事务并同时获取id=1的锁,其他事务必须等待事务提交后才能执行。这样我们就可以保证当前数据不会被其他事务修改。上面我们提到使用select...forupdate会对数据进行加锁,但是需要注意一些加锁级别。MySQLInnoDB默认为行级锁。行级锁基于索引。如果SQL语句不使用索引,则不会使用行级锁。相反,它将使用表级锁来锁定整个表。这需要注意。乐观锁实现方法不需要使用数据库锁机制就可以使用乐观锁。乐观锁的概念其实已经解释了它的具体实现细节,主要是两个步骤:冲突检测和数据更新。典型的实现方法是比较和交换(CAS)。CAS是一种乐观锁技术。当多个线程同时尝试使用CAS更新同一个变量时,只有一个线程可以更新变量的值,而其他线程则失败。失败的线程不会被挂起,而是被锁定。通知本次比赛失败,可以重试。比如前面扣库存的问题,可以通过乐观锁实现如下://查询商品库存信息,quantity=3selectquantityfromitemswhereid=1//修改商品库存为2updateitemssetquantity=2whereid=1andquantity=3;上面,在更新之前,我们先在inventory表中查询当前库存数量(quantity),然后在更新时以库存数量作为修改条件。当我们提交更新时,判断是将数据库表中记录对应的当前存货编号与第一次取出的存货编号进行比较。如果数据库表中的当前库存数量等于第一次取出的库存数量,则更新,否则认为是过期数据。上面的update语句还有一个比较重要的问题,就是传说中的ABA问题。比如一个线程一从数据库中取出库存编号3,此时另一个线程二也从数据库中取出库存编号3,二执行一些操作变成2。然后二将库存编号改为3.此时线程一进行CAS操作,发现数据库中还有3,然后一号操作成功。虽然线程一的CAS操作成功了,但不代表过程没有问题。?有一个更好的方法来解决ABA问题,那就是通过一个单独的可以顺序递增的version字段。改成如下方法即可://查询商品信息,version=1selectversionfromitemswhereid=1//修改商品库存为2updateitemssetquantity=2,version=3whereid=1andversion=2;乐观锁每次进行数据修改操作时,都会带上一个版本号。一旦版本号和数据的版本号一致,就可以进行修改操作,对版本号进行+1操作,否则执行失败。因为每次操作都会增加版本号,所以不存在ABA问题,因为版本号只能增加不能减少。除了版本之外,还可以使用时间戳,因为时间戳本质上是顺序递增的。其实上面的SQL还是有一些问题的,就是一旦发送高并发,只能有一个线程修改成功,会出现很多失败的情况。对于淘宝这样的电商网站,高并发是常事,用户感知失败显然是不合理的。因此,还是需要想办法降低乐观锁的粒度。有更好的建议,可以降低乐观锁的强度,最大程度的提高吞吐率,提高并发能力!如下://修改商品库存updateitemsetquantity=quantity-1whereid=1andquantity-1>0在上面的SQL语句中,如果用户下单数量为1,则通过quantity-1进行乐观锁控制>0。在上面的update语句执行过程中,会以原子操作查询quantity的值,并从中减1。高并发环境下的锁粒度控制是一门重要的知识。选择一把好的锁可以在保证数据安全的同时极大地提高吞吐量和性能。乐观锁和悲观锁如何取舍,主要看两者的区别和适用场景:乐观锁不真正加锁,效率高。一旦锁的粒度把握不好,更新失败的概率会比较高,容易出现业务故障。悲观锁依赖于数据库锁,效率低下。更新失败的概率比较低。随着互联网三高架构(高并发、高性能、高可用)的引入,悲观锁在生产环境中的使用越来越少,尤其是在并发量比较大的业务场景中。
