前言
以前一直用redis作为分布式锁的实现,也知道zookeeper可以实现,但是对于分布式锁没有系统梳理,忽略了数据库作为分布式锁的重要应用,本文主要梳理分布式锁实现的主要思路:
按照加锁位置,分为在应用层,缓存层,数据库层加锁
按照加锁的类型,分为乐观锁和悲观锁
悲观锁
顾名思义,悲观锁在修改整个过程中保持对修改数据的加锁,一直到修改结束,防止其它线程或者进程对数据修改。
悲观锁适用于写多读少的情况下,技术实现上依赖数据库的锁机制实现,保证最大程度的独占性。
常用select for update,进行加锁,并且取消事务的自动提交,在修改之后其它事务请求才可以修改数据,其间,select from只读操作是可以进行的。
乐观锁
分为三个阶段:数据读取、写入校验、数据写入。
假设数据一般情况下不会造成冲突,只有在数据进行提交更新时,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回错误信息,让用户决定如何去做。fail-fast机制
乐观锁在适用于读多写少的情况下,常用的实现技术包括
利用数据库锁机制实现
利用数据库实现的核心思想是数据读取,校验,写入放入数据库事务当中进行,如果分开,就存在并发操作问题。
例子1:
假如有个10个线程,每个线程执行10次,每次对sum加1操作(初始值为0)。如果保证最后结果为100?
分析:
10个线程并行进行,如果不进行并发控制,那么结果一般都不是100,因为线程读取值->加1操作->更新sum值。
两个线程同时读取,值为3,然后加1,为4,写会数据库,很显然,结果不是我们期待的。发生这个情况的根本原因是读取数据,加1,写回数据库,整个操作不是原子性的。如果保证原子性,利用数据库锁机制实现,更新sql如下语句:
update table set sum=sum+1 where id={id}
数据库在执行update语句时会锁定这条记录,当然这个字段需要建立索引(查询字段必需有索引,主键索引,唯一索引,普通索引都可以,但是必需有,另外,即使有索引,有些情况下,数据量小,大部分数据相等的情况下,mysql会认为全表扫描效率更高,自动忽略索引,因而使用表锁)
InnoDB这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁!
例子2
买家操作一笔订单,执行确认收货,假如同一笔订单打开了两个窗口,开始时在一个窗口确认成功,后来在另一个窗口又点了一次,此时应如何解决?
分析:点击确认收货,后台的逻辑是什么?
//根据订单号读取订单信息
//判断订单状态-这个判断在并发操作情况下,根本不准确,数据库update语句是原子性的,因为行锁的存在
//修改为确认收获
整个过程都不是原子性的,并发情况下会有问题。如果点击两次,并发操作的话,可能请求1读取,请求2读取,并发进行,此时都是“未确认收获”,会进行两次更改操作,sql如下:
update order set status=1 where orderId=$orderId
为了避免多次更改操作,利用数据库锁机制,可以修改sql为:
update order set status=1 where orderId=$orderId and status=0
第二个sql之所以能解决问题是因为以下两点:
1. 增加前置条件判断,如果是已经确认,不操作
2.update增加行锁,不会有其它请求修改状态
3.出现多次修改的流程根本原因在于读取-判断-修改整个流程不是原子性的,解决方法把读取-判断-修改用一条sql语句执行,主要利用行锁实现了原子操作
例子3
如果没有类似上面的status这种前置条件,如何处理?常见的就是增加verson字段进行乐观锁控制。
select amount, version from order where orderId=$id;//返回1000.00和1.0
update order set amount=amount+100 where orderId=$id and version=1.0
//如果db中version为1.0,则成功,否则失败,防止在读取数据后,update之前有其它请求修改过数据,导致并发更新丢失
例子4
例子3在高并发场景下,业务层可能感知到大量失败,上例中其实可以amount字段进行乐观锁,跟version一样,如下:
update order set amount=amount+100 where orderId=$id and amount=1000.00
高并发情况下,version做乐观锁,会有大量失败。因为大量请求并发进行,version判断大部分会失败。所以在秒杀扣除库存的场景下,可以用库存数作为乐观锁。
update item
set quantity=quantity - #sub_quantity#
where item_id=#id#
and quantity - #sub_quantity# > 0
数据库机制实现分布式锁参考文章:https://github.com/aalansehaiyang/technology-talk/blob/master/system-architecture/%E9%94%81%E6%9C%BA%E5%88%B6.md
利用缓存实现分布式锁
利用缓存的某些原子特性实现分布式锁,关键在于原子性操作,比如redis的setnx操作,当然需要优化,可能面临key在主从切换时丢失的问题,即使增加超时设置。官方推荐的redission lock可以解决这个问题。
也可以利用memorycache 解决,原理类似。
具体实现可以参考博客的redis分布式锁实现文章