浅谈分布式锁

概述

为了防止分布式系统中的多个进程之间相互干扰,我们需要一种分布式协调技术来对这些进程进行调度。而这个分布式协调技术的核心就是来实现这个分布式锁。

为什么要使用分布式锁

file

成员变量 A 存在 JVM1、JVM2、JVM3 三个 JVM 内存中
成员变量 A 同时都会在 JVM 分配一块内存,三个请求发过来同时对这个变量操作,显然结果是不对的
不是同时发过来,三个请求分别操作三个不同 JVM 内存区域的数据,变量 A 之间不存在共享,也不具有可见性,处理的结果也是不对的
注:该成员变量 A 是一个有状态的对象
如果我们业务中确实存在这个场景的话,我们就需要一种方法解决这个问题,这就是分布式锁要解决的问题

分布式锁应该具有哪些特点

在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;
高可用的获取锁与释放锁;
高性能的获取锁与释放锁;
具备可重入特性;
具备锁失效机制,防止死锁;
具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。

实现分布式锁的N种方式

  1. 数据库实现排他锁
  2. Redis:和 Memcached 的方式类似,利用 Redis 的 setnx 命令。此命令同样是原子性操作,只有在 key 不存在的情况下,才能 set 成功。
  3. Zookeeper:利用 Zookeeper 的顺序临时节点,来实现分布式锁和等待队列。Zookeeper 设计的初衷,就是为了实现分布式锁服务的。
  4. Memcached:利用 Memcached 的 add 命令。此命令是原子性操作,只有在 key 不存在的情况下,才能 add 成功,也就意味着线程得到了锁。
  5. Chubby:Google 公司实现的粗粒度分布式锁服务,底层利用了 Paxos 一致性算法。
    接下来,介绍三种常用的方式

1.基于Mysql实现分布式锁

表结构

-- auto-generated definition
create table distributed_lock
(id          int auto_incrementprimary key,method_name varchar(100)                       not null comment '获取锁的方法名',remark      varchar(100)                       null comment '备注信息',status      int                                not null comment '分配状态: 1-未分配,2-已分配',update_time datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP,version     int                                not null comment '版本号',constraint uidx_method_nameunique (method_name)
)comment '分布式锁表';

先获取锁的信息

select id, method_name, status,version from distributed_lock where status=1 and method_name='methodName';```### 占有锁     

update t_resoure set status=2, version=2, update_time=now() where method_name=‘methodName’ and status=1 and version=2;```

如果没有更新影响到一行数据,则说明这个资源已经被别人占位了。
以上也是cap理论的悲观锁,在数据库上面的应用。

缺点:

1、这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。
2、这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。
3、这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。
4、这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。

解决方案:

  1. 数据库是单点?搞两个数据库,数据之前双向同步。一旦挂掉快速切换到备库上。

  2. 没有失效时间?只要做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍。

  3. 非阻塞的?搞一个while循环,直到insert成功再返回成功。

  4. 非重入的?在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。

2.基于Redis实现分布式锁

file

首先要弄清楚几个redis命令的概念

setnx()

setnx 的含义就是 SET if Not Exists,其主要有两个参数 setnx(key, value)。该方法是原子的,如果 key 不存在,则设置当前 key 成功,返回 1;如果当前 key 已经存在,则设置当前 key 失败,返回 0。

expire()

expire 设置过期时间,要注意的是 setnx 命令不能设置 key 的超时时间,只能通过 expire() 来对 key 设置。

getset()

这个命令主要有两个参数 getset(key,newValue)。该方法是原子的,对 key 设置 newValue 这个值,并且返回 key 原来的旧值。假设 key 原来是不存在的,那么多次执行这个命令,会出现下边的效果:
getset(key, “value1”) 返回 null 此时 key 的值会被设置为 value1
getset(key, “value2”) 返回 value1 此时 key 的值会被设置为 value2
依次类推!

使用步骤:

1.setnx(lockkey, 当前时间 过期超时时间),如果返回 1,则获取锁成功;如果返回 0 则没有获取到锁,转向 2。

2.get(lockkey) 获取值 oldExpireTime ,并将这个 value 值与当前的系统时间进行比较,如果小于当前系统时间,则认为这个锁已经超时,可以允许别的请求重新获取,转向 3。

3.计算 newExpireTime = 当前时间 过期超时时间,然后 getset(lockkey, newExpireTime) 会返回当前 lockkey 的值currentExpireTime。

4.判断 currentExpireTime 与 oldExpireTime 是否相等,如果相等,说明当前 getset 设置成功,获取到了锁。如果不相等,说明这个锁又被别的请求获取走了,那么当前请求可以直接返回失败,或者继续重试。

5.在获取到锁之后,当前线程可以开始自己的业务处理,当处理完毕后,比较自己的处理时间和对于锁设置的超时时间,如果小于锁设置的超时时间,则直接执行 delete 释放锁;如果大于锁设置的超时时间,则不需要再锁进行处理。

话不多说,上代码:

public final class RedisLockUtil {
​private static final int defaultExpire = 60;
​private RedisLockUtil() {//}
​/*** 加锁* @param key redis key* @param expire 过期时间,单位秒* @return true:加锁成功,false,加锁失败*/public static boolean lock(String key, int expire) {
​RedisService redisService = SpringUtils.getBean(RedisService.class);long status = redisService.setnx(key, "1");
​if(status == 1) {redisService.expire(key, expire);return true;}
​return false;}
​public static boolean lock(String key) {return lock2(key, defaultExpire);}
​/*** 加锁* @param key redis key* @param expire 过期时间,单位秒* @return true:加锁成功,false,加锁失败*/public static boolean lock2(String key, int expire) {
​RedisService redisService = SpringUtils.getBean(RedisService.class);
​long value = System.currentTimeMillis()   expire;long status = redisService.setnx(key, String.valueOf(value));
​if(status == 1) {return true;}long oldExpireTime = Long.parseLong(redisService.get(key, "0"));if(oldExpireTime < System.currentTimeMillis()) {//超时long newExpireTime = System.currentTimeMillis()   expire;long currentExpireTime = Long.parseLong(redisService.getSet(key, String.valueOf(newExpireTime)));if(currentExpireTime == oldExpireTime) {return true;}}return false;}
​public static void unLock1(String key) {RedisService redisService = SpringUtils.getBean(RedisService.class);redisService.del(key);}
​public static void unLock2(String key) {    RedisService redisService = SpringUtils.getBean(RedisService.class);    long oldExpireTime = Long.parseLong(redisService.get(key, "0"));   if(oldExpireTime > System.currentTimeMillis()) {        redisService.del(key);    }}
}

3.基于Zookeeper实现分布式锁

首先,我们先来看看zookeeper的相关知识。

zk 一般由多个节点构成(单数),采用 zab 一致性协议。因此可以将 zk 看成一个单点结构,对其修改数据其内部自动将所有节点数据进行修改而后才提供查询服务。

zk 的数据以目录树的形式,每个目录称为 znode, znode 中可存储数据(一般不超过 1M),还可以在其中增加子节点。
子节点有三种类型。序列化节点,每在该节点下增加一个节点自动给该节点的名称上自增。临时节点,一旦创建这个 znode 的客户端与服务器失去联系,这个 znode 也将自动删除。最后就是普通节点。
Watch 机制,client 可以监控每个节点的变化,当产生变化会给 client 产生一个事件。

zookeeper基本锁:

**原理:**利用临时节点与 watch 机制。每个锁占用一个普通节点 /lock,当需要获取锁时在 /lock 目录下创建一个临时节点,创建成功则表示获取锁成功,失败则 watch/lock 节点,有删除操作后再去争锁。临时节点好处在于当进程挂掉后能自动上锁的节点自动删除即取消锁。

**缺点:**所有取锁失败的进程都监听父节点,很容易发生羊群效应,即当释放锁后所有等待进程一起来创建节点,并发量很大。

zookeeper分布式锁实现:

**原理:**上锁改为创建临时有序节点,每个上锁的节点均能创建节点成功,只是其序号不同。只有序号最小的可以拥有锁,如果这个节点序号不是最小的则 watch 序号比本身小的前一个节点 (公平锁)。

步骤:

  1. 在 /lock 节点下创建一个有序临时节点 (EPHEMERAL_SEQUENTIAL)。

  2. 判断创建的节点序号是否最小,如果是最小则获取锁成功。不是则取锁失败,然后 watch 序号比本身小的前一个节点。

  3. 当取锁失败,设置 watch 后则等待 watch 事件到来后,再次判断是否序号最小。

  4. 取锁成功则执行代码,最后释放锁(删除该节点)。

代码如下:

public final class RedisLockUtil {
​private static final int defaultExpire = 60;
​private RedisLockUtil() {//}
​/*** 加锁* @param key redis key* @param expire 过期时间,单位秒* @return true:加锁成功,false,加锁失败*/public static boolean lock(String key, int expire) {
​RedisService redisService = SpringUtils.getBean(RedisService.class);long status = redisService.setnx(key, "1");
​if(status == 1) {redisService.expire(key, expire);return true;}
​return false;}
​public static boolean lock(String key) {return lock2(key, defaultExpire);}
​/*** 加锁* @param key redis key* @param expire 过期时间,单位秒* @return true:加锁成功,false,加锁失败*/public static boolean lock2(String key, int expire) {
​RedisService redisService = SpringUtils.getBean(RedisService.class);
​long value = System.currentTimeMillis()   expire;long status = redisService.setnx(key, String.valueOf(value));
​if(status == 1) {return true;}long oldExpireTime = Long.parseLong(redisService.get(key, "0"));if(oldExpireTime < System.currentTimeMillis()) {//超时long newExpireTime = System.currentTimeMillis()   expire;long currentExpireTime = Long.parseLong(redisService.getSet(key, String.valueOf(newExpireTime)));if(currentExpireTime == oldExpireTime) {return true;}}return false;}
​public static void unLock1(String key) {RedisService redisService = SpringUtils.getBean(RedisService.class);redisService.del(key);}
​public static void unLock2(String key) {    RedisService redisService = SpringUtils.getBean(RedisService.class);    long oldExpireTime = Long.parseLong(redisService.get(key, "0"));   if(oldExpireTime > System.currentTimeMillis()) {        redisService.del(key);    }}
}

优缺点:

优点:

有效的解决单点问题,不可重入问题,非阻塞问题以及锁无法释放的问题。实现起来较为简单。

缺点:

性能上可能并没有缓存服务那么高,因为每次在创建锁和释放锁的过程中,都要动态创建、销毁临时节点来实现锁功能。ZK 中创建和删除节点只能通过 Leader 服务器来执行,然后将数据同步到所有的 Follower 机器上。还需要对 ZK的原理有所了解。

三种方案的比较

上面几种方式,哪种方式都无法做到完美。就像CAP一样,在复杂性、可靠性、性能等方面无法同时满足,所以,根据不同的应用场景选择最适合自己的才是王道。

从理解的难易程度角度(从低到高)
数据库 > 缓存 > Zookeeper

从实现的复杂性角度(从低到高)
Zookeeper >= 缓存 > 数据库

从性能角度(从高到低)
缓存 > Zookeeper >= 数据库

从可靠性角度(从高到低)
Zookeeper > 缓存 > 数据库

获取更多互联网知识请关注公众号:

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/413528.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[css] 说说sroll-snap-type属性的运用场景有哪些?相关联的属性还有哪些?

[css] 说说sroll-snap-type属性的运用场景有哪些&#xff1f;相关联的属性还有哪些&#xff1f; 使用 sroll-snap-type 优化滚动 根据 CSS Scroll Snap Module Level 1 规范&#xff0c;CSS 新增了一批能够控制滚动的属性&#xff0c;让滚动能够在仅仅通过 CSS 的控制下&#…

简单的一个月之设计实现内容1

需求:简单的新闻管理系统,实现简单的增删改查功能 1.数据库表的建立 ID非空,数据类型看着给 2.写实体(entity)News.java 要与数据库中的字段相对应,(id,optimistic我没写,问题不大)1 package com.pay.boss.entity; //封装好的entity包,直接引用2 3 import java.util.Date; …

分享一个有趣的网站“让我帮你百度一下“

如何解决同事的弱智问题&#xff0c;分享一个有趣的网站 日常工作中&#xff0c;总有些人会问你一些弱智的问题 你只需要三步就可以完美解决: 1.打开这个链接: 让我帮你百度一下 2.输入他的问题、点回车 3.复制结果链接甩到他的脸上_ hahahhahahaha 这样就解决了一切烦恼&a…

SQL Server2014 SP2新增的数据库克隆功能

SQL Server2014 SP2新增的数据库克隆功能 原文:SQL Server2014 SP2新增的数据库克隆功能SQL Server2014 SP2新增的数据库克隆功能 创建测试库 --创建测试数据库 create database testtestuse testtest go --创建表 create table testtest(id int ,name varchar(20)) --插入数据…

工作328:uni-两个页面对象传递

getDetailList(record){console.log(record)uni.navigateTo({url:../formdaliyList/formdaliyList?recordencodeURIComponent(JSON.stringify(record))})},onLoad(e){/* JSON.parse() */let obj JSON.parse(decodeURIComponent(e.record));console.log(obj)},

vue-js 特殊变量$event常识

背景 如果我们要阻止默认事件&#xff0c;在 chrome 等浏览器中&#xff0c;我们可能要写一个&#xff1a; event.preventDefault(); 而在 IE 中&#xff0c;我们则需要写&#xff1a; event.returnValue false; jquery &#xff0c;跨浏览器的实现&#xff0c;我们统一只…

七、线性表的链式存储结构

1、问题引入 开发数组类模板的原因在于&#xff1a;在创建基于顺序存储结构的线性表时&#xff0c;发现这样的线性表可能被误用&#xff0c;因为重载了数组访问操作符&#xff0c;使用时跟数组类似&#xff0c;但是线性表和数组有很大的区别&#xff0c;所以激发了新的需求&…

工作333:uni-增加添加成功提示

this.$u.api.getPartyAdd(this.form).then(res > {console.log(res)if(res.data.code200){/* uni.showLoading({title: 添加中}); */this.$refs.uToast.show({title: res.data.message,type: success,})setTimeout(function () {uni.navigateTo({url: "../notice_notic…

HDU 6064 RXD and numbers

传送门 有向图生成树计数 &#xff08;度数 ->入度->外向树&#xff09; BEST定理 &#xff08;不定起点的欧拉回路个数某点为根的外向树个数&#xff08;存在欧拉回路->每个点为根的外向树个数相等&#xff09;*&#xff08;每个点的度数&#xff08;存在欧拉回路-&g…

vue.js框架的生命周期:常用钩子函数

//组件实例化之前执行的函数 beforeCreate:function () { alert("组件实例化之前执行的函数") }, //组件实例化完毕执行的函数 created:function () { alert("组件实例化完毕执行的函数") }, //组件挂载前&#xff0c;页面仍未展示&#xff0c;但虚拟Dom已…

工作334:uni-控制整个label区域可选

<navigator url"../LevineHua-editor/LevineHua-editor" class"single"><u-form-item label"内容" label-width"100%" prop"content" class"label-left"><u-input placeholder"请输入公告内容…

零基础Python知识点回顾(一)

如果你是小白&#xff0c;建议只要安装官网的python-3.7.0-amd64.exe 然后在电脑cmd命令提示符 输入检查是否已经安装pip,一般安装了python都会有的。 >pip (有的话会提示一大串说明&#xff0c;没有的话会提示没有) 假设有了pip&#xff0c;下面来用pip 来安装…

工作335:uni-增加表单验证

<u-form-item prop"account"><u-input v-model"form.account" placeholder"请输入账号"></u-input></u-form-item><u-form-item prop"password"><u-input class"inp" v-model"form.p…

使用IDEA 创建SpringBoot项目

首先File ->New -> project ->Spring Initializr ->next 如果没有Spring Initializr 选择右下角config ->setting 搜索Springboot 勾选 然后apply ok 第二步&#xff1a; 在项目中写上自己需要的名称 type 选择 maven project ->next 第三步&#xff1a;…

工作335:uni-ele-el-date-picker限制选择时间

<el-form-item :label"index 1 .发布时间"><el-date-picker v-model"task.start_time" :picker-options"pickerOptions" :disabled"viewMode"/></el-form-item>pickerOptions: {disabledDate(time) {return time…

PhantomJS 一个隐形的浏览器

下载地址: http://phantomjs.org/download.html 使用方法: 下载压缩包解压出来找到phantomjs.exe 放到python的根目录下 转载于:https://www.cnblogs.com/kaibindirver/p/9509684.html

线段树、树状数组

A 树状数组:1 #include<iostream>2 #include<cstdio>3 #include<algorithm>4 #include <string.h>5 using namespace std;6 // 1h / 10min7 const int maxn 32001;8 int c[maxn],ans[maxn]; // c[i] : 以i为横坐标的星星左侧和下侧星星的个数&#xf…

json-server的使用

这里跳过node安装以及npm安装 先全局安装&#xff1a;npm install json-server -g在桌面创建一个空的文件夹JSONSERVERcd 进入到这个文件夹执行npm init --yes&#xff0c;这时候文件夹会多出一个package.json的文件项目再安装一次&#xff1a;npm install json-server --sav…

Eclipse 使用 CodeMix 插件 开发 Vue.js

前言&#xff1a;对比vscode&#xff0c; codemix集成在eclipse插件里面&#xff0c;符合使用习惯 1、下载并安装node.js 官网地址&#xff1a; https://nodejs.org/en/ &#xff0c; 当前版本是 node-v8.11.4-x64.msi&#xff0c; 安装过程直接默认选项即可。 安装完成后&…

开发模式下浏览器不支持ES6

在工程项目下安装babel: cnpm install babel-preset-stage-2 --sava-dev 然后在工程项目下找到.babelrc文件打开并在这个位置添加这个参数&#xff0c;如下图 最后&#xff0c;cnpm run dev重启一下项目即可