一 概述
在分布式系统环境中,一个方法在同一时间只能被一个机器的一个线程执行。所以系统中应设计出高可用的获取锁与释放锁,高性能的获取锁与释放锁,具备可重入特性(可理解为重新进入,由多于一个任务并发使用,而不必担心数据错误),具备锁失效机制,防止死锁。
二 Redis实现分布式锁
锁的特性:
- 互斥性:任意时刻只能有一个客户端获取锁,不能存在两个客户端获取锁
- 安全性:锁只能由持有锁的客户端删除,不能由其他客户端删除
- 防死锁:获取锁的客户端由于某种原因而宕机,未能释放锁,其他客户端也无法获取该锁,从而导致死锁,所以需要有一种机制来避免该情况的发生
- 容错性:当一些结点出现宕机,如一些redis结点出现宕机的时候,客户端仍然能够获取和释放锁
实现所得方案一:
SETNX key value //如果key不存在,则创建可并赋值value,且key的值不能修改
时间复杂度:O(1);
返回值:设置成功返回1;设置失败,返回0。
SENTX key已经创建好后,key就无法重新赋值,且key值是长期有效的,SETNX的操作是原子性的,要么不执行,执行了一定会走完整个执行流程。所以这时候分布式锁已经设置好后,那么该线程会一直占用锁,无法释放。
expire key seconds //会规定key 的生存时间,当key过期后就会被自动删除。
实现锁的代码
long status = redisService.setnx(key,"1"); //具有原子性
if (status == 1) {
redisService.exprire(key,expire); //具有原子性
//独占资源的逻辑代码
function();
}
生成分布式锁方案代码所示,使用redisService.setnx(key,"1"); 然后通过status的值与1作比较,如果设置成功,则status为1,然后设置过期时间,执行独占资源的逻辑,当存在第二个线程调用redisService.setnx(key,"1")的时候,status就会被设置为0,说明存在别的线程占据了资源无法执行与资源相关的操作,下次操作只能是其他线程释放锁。
但是当某一线程执行命令redisService.setnx(key,"1");使得status为1,但是当执行if(status == 1)之后就结束了,setnx(key,"1")一直存在,使得status一直为1,所以其他线程无法对资源进行操作。这样存在原子性不足的问题。redisService.setnx(key,"1"); 和 reidsService.expire(key,expire);本身都是原子操作,但是结合起来就会存在原子性不足的问题。
三 解决原子性不足的问题
set key value [EX seconds] [PX milliseconds] [NX|XX]
- EX second:设置键的过期时间为second
- PX millisecond:设置键的过期时间为millisecond毫秒
- NX:只有键不存在的时候,才可以对键进行设置操作
- XX:只有键已经存在的时候,才对键进行设置操作
- SET操作完成时,返回OK,否则返回nil(理解为NULL)
解决原子性不足的代码
//原子性操作命令
String result = redisService.set(lockkey,requestID,SET_IF_NOT_EXIST,SET_WITH_EXPIRE_TIME,expireTime);
if("OK".equals(result)) {
//执行独占资源的代码
}
注意:如果存在大量的key同时过期时,由于清楚大量的key比较消耗时间,会造成系统的卡顿,所以我们应该在设置及key过期时间的时候给每个key的过期时间加上一个随机值,使得大量的key的过期时间变得分散。