分布式锁简述

在单机时代,虽然不存在分布式锁,但也会面临资源互斥的情况,只不过在单机的情况下,如果有多个线程要同时访问某个共享资源的时候,我们可以采用线程间加锁的机制,即当某个线程获取到这个资源后,就需要对这个资源进行加锁,当使用完资源之后,再解锁,其它线程就可以接着使用了。例如,在JAVA中,甚至专门提供了一些处理锁机制的一些API(synchronize/Lock等)。

但是到了分布式系统的时代,这种线程之间的锁机制,就没作用了,系统可能会有多份并且部署在不同的机器上,这些资源已经不是在线程之间共享了,而是属于进程之间共享的资源。因此,为了解决这个问题,「分布式锁」就强势登场了。

分布式锁是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。

在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,这个时候,便需要使用到分布式锁。

分布式锁要满足哪些要求呢?

  • 排他性:在同一时间只会有一个客户端能获取到锁,其它客户端无法同时获取
  • 避免死锁:这把锁在一段有限的时间之后,一定会被释放(正常释放或异常释放)
  • 高可用:获取或释放锁的机制必须高可用且性能佳
  • ...

目前相对主流的有三种,从实现的复杂度上来看,从上往下难度依次增加:

  • 数据库(MySQL)
  • Redis
  • ZooKeeper

基于数据库实现

基于数据库来做分布式锁的话,通常有两种做法:

  • 基于数据库的乐观锁
  • 基于数据库的悲观锁

乐观锁

乐观锁的特点先进行业务操作,不到万不得已不去拿锁。即“乐观”的认为拿锁多半是会成功的,因此在进行完业务操作需要实际更新数据的最后一步再去拿一下锁就好。

乐观锁机制其实就是在数据库表中引入一个版本号(version)字段来实现的。当我们要从数据库中读取数据的时候,同时把这个version字段也读出来,如果要对读出来的数据进行更新后写回数据库,则需要将version加1,同时将新的数据与新的version更新到数据表中,且必须在更新的时候同时检查目前数据库里version值是不是之前的那个version,如果是,则正常更新。如果不是,则更新失败,说明在这个过程中有其它的进程去更新过数据了。

看图叙事。模拟实战场景。

如上图,故事男主人公(以下简称男主)打算去ATM机取3000元,故事女主人公(以下简称女主)则要在某宝买买买,买个包需要3000元,账户的余额是5000元。如果没有采用锁的话,在两人同时取款和买买买,可能会出现合计消费了6000,导致账户余额异常。所以需要用到锁的机制,当男主女主甚至更多小主同时消费时,除了读取到6000的账户余额外,还需要读取到当前的版本号version=1,等先行消费成功的主人公(无论谁先消费)去出发修改账户余额的同时,会触发version=version+1,即version=2。那么其他人使用未更新的version(1)去更新账户余额时就会发现版本号不对,就会导致本次更新失败,就得重新去读取最新账户余额以及版本号。

乐观锁遵循的两点法则:

  • 锁服务要有递增的版本号version
  • 每次更新数据的时候都必须先判断版本号对不对,然后再写入新的版本号

悲观锁

悲观锁的特点是先获取锁,再进行业务操作,即“悲观”的认为获取锁是非常有可能失败的,因此要先确保获取锁成功再进行业务操作。

通常所说的“一锁二查三更新”即指的是使用悲观锁。通常来讲在数据库上的悲观锁需要数据库本身提供支持,即通过常用的select ... for update操作来实现悲观锁。当数据库执行select for update时会获取被select中的数据行的行锁,因此其他并发执行的select for update如果试图选中同一行则会发生排斥(需要等待行锁被释放),因此达到锁的效果。select for update获取的行锁会在当前事务结束时自动释放,因此必须在事务中使用。

示例:

/**
* 消费以后更新银行余额
* @param bankId 银行卡号
* @param cost 消费金额
* @return
*/
public boolean consume(Long bankId, Integer cost){
//先锁定银行账户
BankAccount product = query("SELECT * FROM bank_account WHERE bank_id=#{bankId} FOR UPDATE", bankId);
if (product.getNumber() > 0) {
int updateCnt = update("UPDATE tb_product_stock SET number=#{cost} WHERE product_id=#{productId}", cost, bankId);
if(updateCnt > 0){ //更新库存成功
return true;
}
}
return false;
}

乐观锁与悲观锁的区别

乐观锁的思路一般是表中增加版本字段,更新时where语句中增加版本的判断,算是一种CAS(Compare And Swep)操作,银行消费场景中version起到了版本控制的作用( AND version=#{version})。

悲观锁之所以是悲观,在于他认为本次操作会发生并发冲突,所以一开始就对银行账户加上锁(SELECT ... FOR UPDATE),然后就可以安心的做判断和更新,因为这时候不会有别人更新账户余额。

基于Redis实现

基于Redis实现的锁机制,主要是依赖redis自身的原子操作,例如:

SET user_key user_value NX PX 100

redis从2.6.12版本开始,SET命令才支持这些参数:

NX:只在在键不存在时,才对键进行设置操作,SET key value NX 效果等同于 SETNX key value

PX millisecond:设置键的过期时间为millisecond毫秒,当超过这个时间后,设置的键会自动失效

上述代码示例是指,当redis中不存在user_key这个键的时候,才会去设置一个user_key键,并且给这个键的值设置为 user_value,且这个键的存活时间为100ms

为什么这个命令可以帮我们实现锁机制呢?

因为这个命令是只有在某个key不存在的时候,才会执行成功。那么当多个进程同时并发的去设置同一个key的时候,就永远只会有一个进程成功。当某个进程设置成功之后,就可以去执行业务逻辑了,等业务逻辑执行完毕之后,再去进行解锁。

解锁很简单,只需要删除这个key就可以了,不过删除之前需要判断,这个key对应的value是当初自己设置的那个。

另外,针对redis集群模式的分布式锁,可以采用redis的Redlock(可能会被墙)机制。

基于ZooKeeper实现

其实基于ZooKeeper,就是使用它的临时有序节点来实现的分布式锁。

原理

当某客户端要进行逻辑的加锁时,就在zookeeper上的某个指定节点的目录下,去生成一个唯一的临时有序节点, 然后判断自己是否是这些有序节点中序号最小的一个,如果是,则算是获取了锁。如果不是,则说明没有获取到锁,那么就需要在序列中找到比自己小的那个节点,并对其调用exist()方法,对其注册事件监听,当监听到这个节点被删除了,那就再去判断一次自己当初创建的节点是否变成了序列中最小的。如果是,则获取锁,如果不是,则重复上述步骤。

当释放锁的时候,只需将这个临时节点删除即可。

如上图,locker是一个持久节点,node_1/node_2/.../node_n 就是上面说的临时节点,由客户端client去创建的。

client_1/client_2/.../clien_n 都是想去获取锁的客户端。以client_1为例,它想去获取分布式锁,则需要跑到locker下面去创建临时节点(假如是node_1)创建完毕后,看一下自己的节点序号是否是locker下面最小的,如果是,则获取了锁。如果不是,则去找到比自己小的那个节点(假如是node_2),找到后,就监听node_2,直到node_2被删除,那么就开始再次判断自己的node_1是不是序列中最小的,如果是,则获取锁,如果还不是,则继续找一下一个节点。

转自:https://www.toutiao.com/a6611803427183788552/?tt_from=weixin&utm_campaign=client_share&wxshare_count=1&timestamp=1539481883&app=news_article&utm_source=weixin&iid=43469421665&utm_medium=toutiao_ios&group_id=6611803427183788552

Java分布式锁的更多相关文章

  1. Java分布式锁之数据库实现

    之前的文章<Java分布式锁实现>中列举了分布式锁的3种实现方式,分别是基于数据库实现,基于缓存实现和基于zookeeper实现.三种实现方式各有可取之处,本篇文章就详细讲解一下Java分 ...

  2. Java分布式锁之数据库方式实现

    之前的文章<Java分布式锁实现>中列举了分布式锁的3种实现方式,分别是基于数据库实现,基于缓存实现和基于zookeeper实现.三种实现方式各有可取之处,本篇文章就详细讲解一下Java分 ...

  3. Java分布式锁实现详解

    在进行大型网站技术架构设计以及业务实现的过程中,多少都会遇到需要使用分布式锁的情况.那么问题也就接踵而至,哪种分布式锁更适合我们的项目? 下面就这个问题,我做了一些分析: 分布式锁现状: 目前几乎很多 ...

  4. Java分布式锁,搞懂分布式锁实现看这篇文章就对了

    随着微处理机技术的发展,人们只需花几百美元就能买到一个CPU芯片,这个芯片每秒钟执行的指令比80年代最大的大型机的处理机每秒钟所执行的指令还多.如果你愿意付出两倍的价钱,将得到同样的CPU,但它却以更 ...

  5. Java分布式锁看这篇就够了

    ### 什么是锁? 在单进程的系统中,当存在多个线程可以同时改变某个变量(可变共享变量)时,就需要对变量或代码块做同步,使其在修改这种变量时能够线性执行消除并发修改变量. 而同步的本质是通过锁来实现的 ...

  6. Java分布式锁的三种实现方案(redis)

    方案一:数据库乐观锁 乐观锁通常实现基于数据版本(version)的记录机制实现的,比如有一张红包表(t_bonus),有一个字段(left_count)记录礼物的剩余个数,用户每领取一个奖品,对应的 ...

  7. java 分布式锁 -图解- 秒懂

    目录 写在前面 1.1. 分布式锁 简介 1.1.1. 图解:公平锁和可重入锁 模型 1.1.2. 图解: zookeeper分布式锁的原理 1.1.3. 分布式锁的基本流程 1.1.4. 加锁的实现 ...

  8. java 分布式锁

    转自:http://www.hollischuang.com/archives/1716 目前几乎很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题.分布式的CA ...

  9. Java分布式锁三种实现方案

    方案一:数据库乐观锁 乐观锁通常实现基于数据版本(version)的记录机制实现的,比如有一张红包表(t_bonus),有一个字段(left_count)记录礼物的剩余个数,用户每领取一个奖品,对应的 ...

随机推荐

  1. 【虚拟机】解决网络适配器没有 VirtualBox Host-Only Ethernet Adapter 问题

    下面以windows系统来演示重新安装 VirtualBox Host-Only Ethernet Adapter的方法 1.“win+r”输入“devmgmt.msc”,出现如下界面: 2.点击菜单 ...

  2. Git commit/pull/push的操作步骤

    1.操作步骤需要严格执行如下顺序:commit->pull->push 2.commit:将代码提交到本地仓库. 3.pull:将远程仓库代码同步到本地仓库.如遇冲突,解决冲突,重复com ...

  3. windows常见软件库

    1.护卫神软件库 http://soft.huweishen.com/special/1.html 2.护卫神windows资料库 http://v.huweishen.com/ 3.国超软件下载 h ...

  4. ionic 解决APP安装到android上 状态栏显示黑色

    首先移除原本的 cordova-plugin-statusbar 运行命令: cordova plugin rm cordova-plugin-statusbar 最后需要重新安装: cordova ...

  5. ProtoBuf3.3 安装记录

    翻了很多教程,下载了 PB 的源码在自己的 mac 上编译及安装,记录下新的 1. 首先是下载源码了 https://github.com/google/protobuf/releases 虽然是 g ...

  6. 佛祖保佑永无BUG代码注释

    // // _oo0oo_ // o8888888o // 88" . "88 // (| -_- |) // 0\ = /0 // ___/`---'\___ // .' \\| ...

  7. Python- - -基础目录

    一.Python.pycharm的介绍与安装. 二.变量.整数.字符串.列表.字典.集合. 三.运算符.格式化输出.流程控制语句. 四.break和continue. 五.range.enumerat ...

  8. kafka consumer 指定 offset,进行消息回溯

    kafka consumer 如何根据 offset,进行消息回溯?下面的文档给出了 demo: https://cwiki.apache.org/confluence/display/KAFKA/0 ...

  9. Spring搭建练习遇到的坑

    1.Error:(7, 23) java: cannot find symbolsymbol: class ProceedingJoinPointlocation: class com.how2jav ...

  10. private、public、protected和默认

    类中的域最好标记为private: 方法最好标记为public: private:仅对本类可见 public:对所有类可见 protected:对本包和对所有子类可见 默认(什么都不写):对本包可见 ...