Java面试题分布式篇一篇文章搞明白分布式锁

前言

以前一直用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分布式锁实现文章

 

    原文作者:java锁
    原文地址: https://blog.csdn.net/hanruikai/article/details/81458981
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞