在分布式或多进程多线程的模式中,如果大家对同一个资源进行操作中避免不了资源竞争问题。
比如现在有一个商品,一个人只能买一次,正常情况下我们的后台代码逻辑是
正常的用户这样判断一般是没有问题的,但是我突然手很快点两下,两个请求一起过来请求接口,在请求1还没有把购买记录成功写进数据库之前,请求2过来判断用户是否购买过都会是还没有购买,这个时候也会去完成下单,明明这个商品只能购买一次,但是只要手速度快就能成功购买多次
因为node是单线程异步编程的方式所以不会有线程安全问题,在多线程编程语言中比如java你就需要用 synchronized对象锁来进行代码的包裹,同一时间只能有一个线程访问,保证线程安全
也是为了控制同一操作系统中多个进程访问一个共享资源,只是因为程序的独立性,各个进程是无法控制其他进程对资源的访问的,node可以利用cluster根据系统资源 CPU 的核心数可以开启多进程模式,因为每个进程都是单独运行的,也有独立的空间,会有遇到资源竞争问题,java的synchronized的作用域是一个JVM中,你开启多个java进程就会有多个JVM,所以也没办法解决
当我们一个项目大了过后我们就会把项目开始部署多台服务器,处于分布式环境下对同一共享资源进行操作还是会面临同样的问题,当多个客户端对同一资源进行先读后写操作就会引发并发问题,一般分布式锁的实现都是用一个所以服务器都能访问的容器来进行加锁解锁操作,比如mysql,redis,Zookeeper
redis SETNX
操作
但是为了防止死锁 我们需要给key 上一个过期时间
expire key seconds
但是这是两步操作如果没有设置上那么就造成死锁了
set key value [EX seconds] [PX milliseconds] [NX|XX]
这条命令可以一口气执行完上面两步操作,
释放锁的过程就是将原来的key删除就可以了,但是如果请求1中间某段代码耗时过长导致锁自动过时了,然后请求2过来又添加这个key 然后进行操作,这是请求1继续运行然后就删除key,但此时的key已经不是请求1的key了,而是请求2的key,如何避免这种情况就需要把value设置随机数,删除的时候判断一下是否还是这个value,就能判断这个key是否是你的,如果是你的就执行删除,但是判断和删除不是一个原子性的操作,此处仍需借助 Lua 脚本实现,因为redis运行lua脚本是原子性的
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
npm i redlock
npm i ioredis
const ioredis = require('ioredis')
const Redlock = require('redlock')
const client = new ioredis()
const redlock = new Redlock([client])
router.post('/test', async function (ctx, next){
let lock = null
try {
// 请求锁 // 第一个参数是锁的名称,第二个是锁的有效期
lock = await redlock.lock('lock', 10000)
//业务逻辑代码
······
// 返回成功信息
ctx.body = {
code: 200,
msg: '成功'
}
// 释放锁
lock.unlock()
} catch (e) {
console.log('请求锁失败', e)
lock = null
// 返回错误信息
return ctx.body = {
code: 500,
msg: '服务器繁忙,请稍候再试'
}
}
})
把redis客户端设置到redLock中的services,还可以传入options进行自定义数据,比如自定义重试延迟啊,重试数量等
源代码 展示 下面有每一步的说明
Redlock.prototype._lock = function _lock(resource, value, ttl, callback) {
const self = this;
// array of locked resources
resource = Array.isArray(resource) ? resource : [resource];
return new Promise(function(resolve, reject) {
let request;
// the number of times we have attempted this lock
let attempts = 0;
// create a new lock
if(value === null) {
value = self._random();
request = function(server, loop){
return server.eval(
[
self.lockScript,
resource.length,
...resource,
value,
ttl
],
loop
);
};
}
// extend an existing lock
else {
request = function(server, loop){
return server.eval(
[
self.extendScript,
resource.length,
...resource,
value,
ttl
],
loop
);
};
}
function attempt(){
attempts++;
// the time when this attempt started
const start = Date.now();
// the number of votes needed for consensus
const quorum = Math.floor(self.servers.length / 2) + 1;
// the number of servers which have agreed to this lock
let votes = 0;
// the number of async redis calls still waiting to finish
let waiting = self.servers.length;
function loop(err, response) {
if(err) self.emit('clientError', err);
if(response === resource.length || response === '' + resource.length) votes++;
if(waiting-- > 1) return;
// Add 2 milliseconds to the drift to account for Redis expires precision, which is 1 ms,
// plus the configured allowable drift factor
const drift = Math.round(self.driftFactor * ttl) + 2;
const lock = new Lock(self, resource, value, start + ttl - drift, attempts);
// SUCCESS: there is concensus and the lock is not expired
if(votes >= quorum && lock.expiration > Date.now())
return resolve(lock);
// remove this lock from servers that voted for it
return lock.unlock(function(){
// RETRY
if(self.retryCount === -1 || attempts <= self.retryCount)
return setTimeout(attempt, Math.max(0, self.retryDelay + Math.floor((Math.random() * 2 - 1) * self.retryJitter)));
// FAILED
return reject(new LockError('Exceeded ' + self.retryCount + ' attempts to lock the resource "' + resource + '".', attempts));
});
}
return self.servers.forEach(function(server){
return request(server, loop);
});
}
return attempt();
})
// optionally run callback
.nodeify(callback);
};
我们发现第一步进来先判断了 value是否存在如果不存在会设置一个随机数,封装方法等待被调用,loop回调方法,lua脚本执行完毕后执行的方法
// constants
const lockScript = `
-- Return 0 if an entry already exists.
for i, key in ipairs(KEYS) do // 循环key 数组
if redis.call("exists", key) == 1 then // 取出key 判断可以是否存在如果存在返回0
return 0
end
end
-- Create an entry for each provided key.
for i, key in ipairs(KEYS) do // 循环key 数组
redis.call("set", key, ARGV[1], "PX", ARGV[2]) //设置key的value和过期时间
end
-- Return the number of entries added.
return #KEYS
`;
然后往下有一个attempt()方法的封装和调用
function attempt(){
attempts++; // 尝试上锁数+1
// the time when this attempt started
const start = Date.now(); // 获取当前时间当 开始时间
/**
* quorum和votes说明
* 如果redis是传入的多个客户端 至少需要加锁多少个才算成功
* 如果是6 Math.floor(6 / 2) + 1 那么就是4 也就是说
* 至少需要加锁成功4个才算是加锁成功
* @type {number}
*/
// the number of votes needed for consensus
// 达成解锁共识至少的票数
const quorum = Math.floor(self.servers.length / 2) + 1;
// the number of servers which have agreed to this lock
// 实际票数
let votes = 0;
// the number of async redis calls still waiting to finish
// 等待加锁redis客户端个数
let waiting = self.servers.length;
// 定义回调(在脚本执行完毕后执行的 通知成功还是失败)
function loop(err, response) {
// 如果报错抛出异常
if(err) self.emit('clientError', err);
// 成功 votes票数加1
if(response === resource.length || response === '' + resource.length) votes++;
// 如果后面还有几个客户端还没有执行完 停止操作,如果是最后一个了就继续执行
if(waiting-- > 1) return;
// Add 2 milliseconds to the drift to account for Redis expires precision, which is 1 ms,
// plus the configured allowable drift factor
// 设置飘移
const drift = Math.round(self.driftFactor * ttl) + 2;
// 返回参数存储方便返回
const lock = new Lock(self, resource, value, start + ttl - drift, attempts);
// 如果达成共识并且锁没有过期
// SUCCESS: there is concensus and the lock is not expired
if(votes >= quorum && lock.expiration > Date.now())
return resolve(lock);
// 如果没有达成共识 重试
// remove this lock from servers that voted for it
return lock.unlock(function(){
// 开始重试
// RETRY
if(self.retryCount === -1 || attempts <= self.retryCount)
return setTimeout(attempt, Math.max(0, self.retryDelay + Math.floor((Math.random() * 2 - 1) * self.retryJitter)));
// 重试无果 失败
// FAILED
return reject(new LockError('Exceeded ' + self.retryCount + ' attempts to lock the resource "' + resource + '".', attempts));
});
}
// 循环调用每个redis客服端加锁
return self.servers.forEach(function(server){
return request(server, loop);
});
}
return attempt();// 调用 attempt方法
})
解锁的模式和加锁的模式类似
先看lua脚本
const unlockScript = `
local count = 0
for i, key in ipairs(KEYS) do
-- Only remove entries for *this* lock value.
if redis.call("get", key) == ARGV[1] then // 判断key的value 是否和传进来的value一样
redis.pcall("del", key) // 一样就删除
count = count + 1 // 删除数量+1
end
end
-- Return the number of entries removed.
return count
`;
Redlock.prototype.unlock = function unlock(lock, callback) {
const self = this;
// array of locked resources
const resource = Array.isArray(lock.resource)
? lock.resource
: [lock.resource];
// immediately invalidate the lock
lock.expiration = 0;
return new Promise(function(resolve, reject) {
// the number of votes needed for consensus
// 达成共识的票数
const quorum = Math.floor(self.servers.length / 2) + 1;
// the number of servers which have agreed to release this lock
// 实际票数
let votes = 0;
// the number of async redis calls still waiting to finish
// 等待解锁的数量
let waiting = self.servers.length;
// 循环解锁
// release the lock on each server
self.servers.forEach(function(server){
return server.eval(
[
self.unlockScript,
resource.length,
...resource,
lock.value
],
loop
)
});
// 定义回调
function loop(err, response) {
if(err) self.emit('clientError', err);
// - If the response is less than the resource length, than one or
// more resources failed to unlock:
// - It may have been re-acquired by another process;
// - It may hava already been manually released;
// - It may have expired;
if(response === resource.length || response === '' + resource.length)
votes++;
if(waiting-- > 1) return;
// SUCCESS: there is concensus and the lock is released
// 达成共识,并且锁已释放
if(votes >= quorum)
return resolve(); // 解锁成功
// FAILURE: the lock could not be released
return reject(new LockError('Unable to fully release the lock on resource "' + lock.resource + '".'));
}
})
// optionally run callback
.nodeify(callback);
};