浅谈分布式锁

概述

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

为什么要使用分布式锁

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,一经查实,立即删除!

相关文章

机器学习模型评估指标总结

常用机器学习算法包括分类、回归、聚类等几大类型&#xff0c;以下针对不同模型总结其评估指标 一、分类模型 常见的分类模型包括&#xff1a;逻辑回归、决策树、朴素贝叶斯、SVM、神经网络等&#xff0c;模型评估指标包括以下几种&#xff1a; &#xff08;1&#xff09;二分类…

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

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

shell脚本一键同时推送代码至github和gitee

自己写的东西&#xff0c;要同时推送多个git地址&#xff0c;解决办法如下: 1.先要初始化你的git 进入自己的项目目录&#xff0c;然后执行 git init cd /app/code/go-study git init 2.执行以下脚本: #!/bin/bash #author Oliver #since 2020-09-03 15:24:31git remote rm …

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

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

[css] 你会经常用到伪元素吗?一般都用在哪方面?

[css] 你会经常用到伪元素吗&#xff1f;一般都用在哪方面&#xff1f; 清浮动个人简介 我是歌谣&#xff0c;欢迎和大家一起交流前后端知识。放弃很容易&#xff0c; 但坚持一定很酷。欢迎大家一起讨论 主目录 与歌谣一起通关前端面试题

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

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

base64转图片

关于c#中Base64编码转图片 。个人建议存放文件时&#xff0c;文件名改为文件MD5值。同一个文件的MD5值相同。只要在上传的时候判断一下文件的MD5值。可以避免文件的重复上传哦 //图片的BASE64编码,var img Context.Request["imgbase64"]; #region base64转图片stri…

[css] 用css画出一把刻度尺

[css] 用css画出一把刻度尺 <div classruler><div classcm><div classmm></div><div classmm></div><div classmm></div><div classmm></div><div classmm></div><div classmm></div>&l…

[css] 说说position:sticky有什么应用场景

[css] 说说position:sticky有什么应用场景 吸顶效果个人简介 我是歌谣&#xff0c;欢迎和大家一起交流前后端知识。放弃很容易&#xff0c; 但坚持一定很酷。欢迎大家一起讨论 主目录 与歌谣一起通关前端面试题

hdu 4738 Caocao's Bridges 求无向图的桥【Tarjan】

<题目链接> 题目大意&#xff1a; 曹操在长江上建立了一些点&#xff0c;点之间有一些边连着。如果这些点构成的无向图变成了连通图&#xff0c;那么曹操就无敌了。周瑜为了防止曹操变得无敌&#xff0c;就打算去摧毁连接曹操的点的桥。但是诸葛亮把所有炸弹都带走了&…

[css] 如何使用css给一个正方形添加一条对角斜线?

[css] 如何使用css给一个正方形添加一条对角斜线&#xff1f; background:linear-gradient(45deg,transparent 49.5%,deeppink 49.5%,deeppink 50.5%,transparent 50.5%);个人简介 我是歌谣&#xff0c;欢迎和大家一起交流前后端知识。放弃很容易&#xff0c; 但坚持一定很酷。…

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)) --插入数据…

[css] 举例说明CSS特性检测的方式有哪些?

[css] 举例说明CSS特性检测的方式有哪些&#xff1f; 原生的 supports 的性能肯定是最好的&#xff0c;而且无需引入外部 javascript &#xff0c;首推这个&#xff0c;但是无奈兼容问题&#xff0c;目前来看不是最好的选择。Modernizr 功能强大&#xff0c;兼容性好&#xff…

DIV布局、浮动

2018-08-17 float属性转载于:https://www.cnblogs.com/twinkle-star/p/9493415.html

工作328:uni-局部过滤器处理数据

toChangeLess(e){console.log(e)/* 过滤数据中的循环请求*/if(e0){return "不循环"}else if(e12345){return "周一到周五循环"}else if(e12345678){return "每日循环"}}运行结果

css ——行级元素与块级元素解析

一 . 先说说二者的本质区别吧&#xff1a; 行级元素是可以和其他元素处于一行&#xff0c;不用必须另起一行。块级元素是每个块级元素都是独自占一行&#xff0c;其后的元素也只能另起一行&#xff0c;并不能两个元素共用一行。 二 .下面看看各自的特点&#xff1a; 块级元素 1…

工作329:uni-数据为空不显示

<u-image v-if"tableData[item]!null" style"display: block;float: left;margin-left: 26rpx;margin-top: 26rpx;" width"120rpx" height"120rpx" :src"tableData[item]"></u-image>

vue.js框架:数组的各种变异方法

今天阅读vue官网的学习教程&#xff0c;看到一个观察数组的变异方法。变异方法&#xff1f;&#xff1f;&#xff1f;excuse me??什么东西&#xff1f;&#xff1f;guide就给了这么一堆东西&#xff1a; 原来这些方法如下&#xff1a; push() pop() shift() uns…

CodeForces - 894E Ralph and Mushrooms (强连通缩点+dp)

题意&#xff1a;一张有向图&#xff0c;每条边上都有wi个蘑菇&#xff0c;第i次经过这条边能够采到w-(i-1)*i/2个蘑菇&#xff0c;直到它为0。问最多能在这张图上采多少个蘑菇。 分析&#xff1a;在一个强连通分量内&#xff0c;边可以无限次地走直到该连通块内蘑菇被采完为止…

工作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)},