在一些分布式环境下、多线程并发编程中,如果对同一资源进行读写操作,避免不了的一个就是资源竞争问题,通过引入分布式锁这一概念,可以解决数据一致性问题。

作者简介:五月君,Nodejs Developer,慕课网认证作者,热爱技术、喜欢分享的 90 后青年,欢迎关注 Nodejs技术栈 和 Github 开源项目 https://www.nodejs.red

认识线程、进程、分布式锁

线程锁:单线程编程模式下请求是顺序的,一个好处是不需要考虑线程安全、资源竞争问题,因此当你进行 Node.js 编程时,也不会去考虑线程安全问题。那么多线程编程模式下,例如 Java 你可能很熟悉一个词 synchronized,通常也是 Java 中解决并发编程最简单的一种方式,synchronized 可以保证在同一时刻仅有一个线程去执行某个方法或某块代码。

进程锁:一个服务部署于一台服务器,同时开启多个进程,Node.js 编程中为了利用操作系统资源,根据 CPU 的核心数可以开启多进程模式,这个时候如果对一个共享资源操作还是会遇到资源竞争问题,另外每一个进程都是相互独立的,拥有自己独立的内存空间。关于进程锁通过 Java 中的 synchronized 也很难去解决,synchronized 仅局限于在同一个 JVM 中有效。

分布式锁:一个服务无论是单线程还是多进程模式,当多机部署、处于分布式环境下对同一共享资源进行操作还是会面临同样的问题。此时就要去引入一个概念分布式锁。如下图所示,由于先读数据在通过业务逻辑修改之后进行 SET 操作,这并不是一个原子操作,当多个客户端对同一资源进行先读后写操作就会引发并发问题,这时就要引入分布式锁去解决,通常也是一个很广泛的解决方案。

基于 Redis 的分布式锁实现思路

实现分布式锁的方式有很多:数据库、Redis、Zookeeper。这里主要介绍的是通过 Redis 来实现一个分布式锁,至少要保证三个特性:安全性、死锁、容错。

安全性:所谓一个萝卜一个坑,第一点要做的是上锁,在任意时刻要保证仅有一个客户端持有该锁。

死锁:造成死锁可能是由于某种原因,本该释放的锁没有被释放,因此在上锁的时候可以同步的设置过期时间,如果由于客户端自己的原因没有被释放,也要保证锁能够自动释放。

容错:容错是在多节点的模式下需要考虑的,只要能保证 N/2+1 节点可用,客户端就可以成功获取、释放锁。

Redis 单实例分布式锁实现

在 Redis 的单节点实例下实现一个简单的分布式锁,这里会借助一些简单的 Lua 脚本来实现原子性,不了解可以参考之前的文章 Node.js 中实践 Redis Lua 脚本

上锁

上锁的第一步就是先通过 setnx 命令占坑,为了防止死锁,通常在占坑之后还会设置一个过期时间 expire,如下所示:

setnx key value
expire key seconds

以上命令不是一个原子性操作,所谓原子性操作是指命令在执行过程中并不会被其它的线程或者请求打断,以上如果 setnx 执行成功之后,出现网络闪断 expire 命令便不会得到执行,会导致死锁出现。

也许你会想到使用事物来解决,但是事物有个特点,要么成功要么失败,都是一口气执行完成的,在我们上面的例子中,expire 是需要先根据 setnx 的结果来判断是否需要进行设置,显然事物在这里是行不通的,社区也有很多库来解决这个问题,现在 Redis 官方 2.8 版本之后支持 set 命令传入 setnx、expire 扩展参数,这样就可以一条命令一口气执行,避免了上面的问题,如下所示:

  • value:建议设置为一个随机值,在释放锁的时候会进一步讲解
  • EX seconds:设置的过期时间
  • PX milliseconds:也是设置过期时间,单位不一样
  • NX|XX:NX 同 setnx 效果是一样的
set key value [EX seconds] [PX milliseconds] [NX|XX]

释放锁

释放锁的过程就是将原本占有的坑给删除掉,但是也并不能仅仅使用 del key 删除掉就万事大吉了,这样很容易删除掉别人的锁,为什么呢?举一个例子客户端 A 获取到一把 key = name1 的锁(2 秒中),紧接着处理自己的业务逻辑,但是在业务逻辑处理这块阻塞了耗时超过了锁的时间,锁是会自动被释放的,这期间该资源又被客户端 B 获取了 key = name1 的锁,那么客户端 A 在自己的业务处理结束之后直接使用 del key 命令删除会把客户端 B 的锁给释放掉了,所以释放锁的时候要做到仅释放自己占有的锁。

加锁的过程中建议把 value 设置为一个随机值,主要是为了更安全的释放锁,在 del key 之前先判断这个 key 存在且 value 等于自己指定的值才执行删除操作。判断和删除不是一个原子性的操作,此处仍需借助 Lua 脚本实现。

if redis.call("get",KEYS[1]) == ARGV[1] thenreturn redis.call("del",KEYS[1])
elsereturn 0
end

Redis 单实例分布式锁 Node.js 实践

使用 Node.js 的 Redis 客户端为 ioredis,npm install ioredis -S 先安装该包。

初始化自定义 RedisLock

class RedisLock {/*** 初始化 RedisLock* @param {*} client * @param {*} options */constructor (client, options={}) {if (!client) {throw new Error('client 不存在');}if (client.status !== 'connecting') {throw new Error('client 未正常链接');}this.lockLeaseTime = options.lockLeaseTime || 2; // 默认锁过期时间 2 秒this.lockTimeout = options.lockTimeout || 5; // 默认锁超时时间 5 秒this.expiryMode = options.expiryMode || 'EX';this.setMode = options.setMode || 'NX';this.client = client;}
}

上锁

通过 set 命令传入 setnx、expire 扩展参数开始上锁占坑,上锁成功返回,上锁失败进行重试,在 lockTimeout 指定时间内仍未获取到锁,则获取锁失败。

class RedisLock {/*** 上锁* @param {*} key * @param {*} val * @param {*} expire */async lock(key, val, expire) {const start = Date.now();const self = this;return (async function intranetLock() {try {const result = await self.client.set(key, val, self.expiryMode, expire || self.lockLeaseTime, self.setMode);// 上锁成功if (result === 'OK') {console.log(`${key} ${val} 上锁成功`);return true;}// 锁超时if (Math.floor((Date.now() - start) / 1000) > self.lockTimeout) {console.log(`${key} ${val} 上锁重试超时结束`);return false;}// 循环等待重试console.log(`${key} ${val} 等待重试`);await sleep(3000);console.log(`${key} ${val} 开始重试`);return intranetLock();} catch(err) {throw new Error(err);}})();}
}

释放锁

释放锁通过 redis.eval(script) 执行我们定义的 redis lua 脚本。

class RedisLock {/*** 释放锁* @param {*} key * @param {*} val */async unLock(key, val) {const self = this;const script = "if redis.call('get',KEYS[1]) == ARGV[1] then" +"   return redis.call('del',KEYS[1]) " +"else" +"   return 0 " +"end";try {const result = await self.client.eval(script, 1, key, val);if (result === 1) {return true;}return false;} catch(err) {throw new Error(err);}}
}

测试

这里使用了 uuid 来生成唯一 ID,这个随机数 id 只要保证唯一不管用哪种方式都可。

const Redis = require("ioredis");
const redis = new Redis(6379, "127.0.0.1");
const uuidv1 = require('uuid/v1');
const redisLock = new RedisLock(redis);function sleep(time) {return new Promise((resolve) => {setTimeout(function() {resolve();}, time || 1000);});
}async function test(key) {try {const id = uuidv1();await redisLock.lock(key, id, 20);await sleep(3000);const unLock = await redisLock.unLock(key, id);console.log('unLock: ', key, id, unLock);} catch (err) {console.log('上锁失败', err);}
}test('name1');
test('name1');

同时调用了两次 test 方法进行上锁,只有第一个是成功的,第二个 name1 26e02970-0532-11ea-b978-2160dffafa30 上锁的时候发现 key = name1 已被占坑,开始重试,由于以上测试中设置了 3 秒钟之后自动释放锁,name1 26e02970-0532-11ea-b978-2160dffafa30 在经过两次重试之后上锁成功。

name1 26e00260-0532-11ea-b978-2160dffafa30 上锁成功
name1 26e02970-0532-11ea-b978-2160dffafa30 等待重试
name1 26e02970-0532-11ea-b978-2160dffafa30 开始重试
name1 26e02970-0532-11ea-b978-2160dffafa30 等待重试
unLock:  name1 26e00260-0532-11ea-b978-2160dffafa30 true
name1 26e02970-0532-11ea-b978-2160dffafa30 开始重试
name1 26e02970-0532-11ea-b978-2160dffafa30 上锁成功
unLock:  name1 26e02970-0532-11ea-b978-2160dffafa30 true

源码地址

https://github.com/Q-Angelo/project-training/tree/master/redis/lock/redislock.js

Redlock 算法

以上是使用 Node.js 对 Redis 分布式锁的一个简单实现,在单实例中是可用的,当我们对 Redis 节点做一个扩展,在 Sentinel、Redis Cluster 下会怎么样呢?

以下是一个 Redis Sentinel 的故障自动转移示例图,假设我们客户端 A 在主节点 192.168.6.128 获取到锁之后,主节点还未来得及同步信息到从节点就挂掉了,这时候 Sentinel 会选举另外一个从节点做为主节点,那么客户端 B 此时也来申请相同的锁,就会出现同样一把锁被多个客户端持有,对数据的最终一致性有很高的要求还是不行的。

Redlock 介绍

鉴于这些问题,Redis 官网 redis.io/topics/distlock 提供了一个使用 Redis 实现分布式锁的规范算法 Redlock,中文翻译版参考 http://redis.cn/topics/distlock.html

Redlock 在上述文档也有描述,这里简单做个总结:Redlock 在 Redis 单实例或多实例中提供了强有力的保障,本身具备容错能力,它会从 N 个实例使用相同的 key、随机值尝试 set key value [EX seconds] [PX milliseconds] [NX|XX] 命令去获取锁,在有效时间内至少 N/2+1 个 Redis 实例取到锁,此时就认为取锁成功,否则取锁失败,失败情况下客户端应该在所有的 Redis 实例上进行解锁。

Node.js 中应用 Redlock

github.com/mike-marcacci/node-redlock 是 Node.js 版的 Redlock 实现,使用起来也很简单,开始之前先安装 ioredis、redlock 包。

npm i ioredis -S
npm i redlock -S

编码

const Redis = require("ioredis");
const client1 = new Redis(6379, "127.0.0.1");
const Redlock = require('redlock');
const redlock = new Redlock([client1], {retryDelay: 200, // time in msretryCount: 5,
});// 多个 Redis 实例
// const redlock = new Redlock(
//     [new Redis(6379, "127.0.0.1"), new Redis(6379, "127.0.0.2"), new Redis(6379, "127.0.0.3")],
// )async function test(key, ttl, client) {try {const lock = await redlock.lock(key, ttl);console.log(client, lock.value);// do something ...// return lock.unlock();} catch(err) {console.error(client, err);}
}test('name1', 10000, 'client1');
test('name1', 10000, 'client2');

测试

对同一个 key name1 两次上锁,由于 client1 先取到了锁,client2 无法获取锁,重试 5 次之后报错 LockError: Exceeded 5 attempts to lock the resource "name1".

js 拉勾网效果_Node.js 中实践基于 Redis 的分布式锁实现相关推荐

  1. 基于Redis的分布式锁和Redlock算法

    来自:后端技术指南针 1 前言 今天开始来和大家一起学习一下Redis实际应用篇,会写几个Redis的常见应用. 在我看来Redis最为典型的应用就是作为分布式缓存系统,其他的一些应用本质上并不是杀手 ...

  2. 基于 Redis 的分布式锁到底安全吗?

    [完整版] 网上有关Redis分布式锁的文章可谓多如牛毛了,不信的话你可以拿关键词"Redis 分布式锁"随便到哪个搜索引擎上去搜索一下就知道了.这些文章的思路大体相近,给出的实现 ...

  3. php使用redis分布式锁,php基于redis的分布式锁实例详解

    在使用分布式锁进行互斥资源访问时候,我们很多方案是采用redis的实现. 固然,redis的单节点锁在极端情况也是有问题的,假设你的业务允许偶尔的失效,使用单节点的redis锁方案就足够了,简单而且效 ...

  4. 基于Redis的分布式锁到底安全吗(上)?

    网上有关Redis分布式锁的文章可谓多如牛毛了,不信的话你可以拿关键词"Redis 分布式锁"随便到哪个搜索引擎上去搜索一下就知道了.这些文章的思路大体相近,给出的实现算法也看似合 ...

  5. 基于Redis的分布式锁真的安全吗?

    说明: 我前段时间写了一篇用consul实现分布式锁,感觉理解的也不是很好,直到我看到了这2篇写分布式锁的讨论,真的是很佩服作者严谨的态度, 把这种分布式锁研究的这么透彻,作者这种技术态度真的值得我好 ...

  6. 基于Redis的分布式锁到底安全吗?

    网上有关Redis分布式锁的文章可谓多如牛毛了,不信的话你可以拿关键词"Redis 分布式锁"随便到哪个搜索引擎上去搜索一下就知道了.这些文章的思路大体相近,给出的实现算法也看似合 ...

  7. 基于 Redis 的分布式锁到底安全吗(下)?

    自从我写完这个话题的上半部分之后,就感觉头脑中出现了许多细小的声音,久久挥之不去.它们就像是在为了一些鸡毛蒜皮的小事而相互争吵个不停.的确,有关分布式的话题就是这样,琐碎异常,而且每个人说的话听起来似 ...

  8. 基于 Redis 实现分布式锁思考

    以下文章来源方志朋的博客,回复"666"获面试宝典 来源:blog.csdn.net/xuan_lu/article/details/111600302 分布式锁 基于redis实 ...

  9. nx set 怎么实现的原子性_基于Redis的分布式锁实现

    前言 本篇文章主要介绍基于Redis的分布式锁实现到底是怎么一回事,其中参考了许多大佬写的文章,算是对分布式锁做一个总结 分布式锁概览 在多线程的环境下,为了保证一个代码块在同一时间只能由一个线程访问 ...

最新文章

  1. linux 子系统 巡检,Linux 系统巡检
  2. 1、计算机图形学——2D变换与齐次坐标
  3. BIOS-SMI Introduction
  4. BZOJ-3065 带插入区间K小值
  5. 写程序和做网店的压力都很大
  6. 数据结构课程设计 迷宫问题
  7. netty的使用场景,线程模型以及如何在springboot中使用netty?
  8. 单位启用人脸识别考勤
  9. Nginx总复习---1
  10. Innodb独立的undo tablespace
  11. 小京东商城用什么版本的php,小京东2018多用户通用商城网源码商城带手机版ecshop源码程序...
  12. JSP开发工具安装教程
  13. netbeans卸载
  14. 电脑linux 开启热点hostapt,Ubuntu16.04开启热点
  15. 社交电商的发展起伏有什么因素在其中影响?
  16. Hadoop中怎么解决Starting secondary namenodes [0.0.0.0]
  17. java树莓派温度传感器_怎样使用树莓派读取传感器上的温度和湿度
  18. 2.光栅图形显示技术
  19. ros 双wan配置_5G智慧路灯杆网关的双WAN口应用介绍
  20. tempdb大量闩锁等待问题分析

热门文章

  1. Android——电脑蓝屏重启后,studio无法认出Android环境 setup JDK(缓存!缓存!缓存)...
  2. 获取浏览器窗口宽高问题总结
  3. [杭电ACM]3336Count the string
  4. IT巨擎为何迷失互联网大潮
  5. shell 去除 字符串中的双引号
  6. python flask 设置个性化的响应体 响应头 状态码
  7. IntelliJ idea 缓存和索引 清理方法
  8. linux ubuntu 获取仓库源码并构建
  9. VC 利用DLL共享区间在进程间共享数据及进程间广播消息
  10. Linux2.6内核--内存管理(1)--分页机制