一 概述

在分布式系统环境中,一个方法在同一时间只能被一个机器的一个线程执行。所以系统中应设计出高可用的获取锁与释放锁,高性能的获取锁与释放锁,具备可重入特性(可理解为重新进入,由多于一个任务并发使用,而不必担心数据错误),具备锁失效机制,防止死锁。

二 Redis实现分布式锁

锁的特性

  1. 互斥性:任意时刻只能有一个客户端获取锁,不能存在两个客户端获取锁
  2. 安全性:锁只能由持有锁的客户端删除,不能由其他客户端删除
  3. 防死锁:获取锁的客户端由于某种原因而宕机,未能释放锁,其他客户端也无法获取该锁,从而导致死锁,所以需要有一种机制来避免该情况的发生
  4. 容错性:当一些结点出现宕机,如一些redis结点出现宕机的时候,客户端仍然能够获取和释放锁

实现所得方案一:

SETNX key value //如果key不存在,则创建可并赋值value,且key的值不能修改

时间复杂度:O(1);

返回值:设置成功返回1;设置失败,返回0。

redisson 获取已使用的lock redis获取锁释放锁_SETNX

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]

  1. EX second:设置键的过期时间为second
  2. PX millisecond:设置键的过期时间为millisecond毫秒
  3. NX:只有键不存在的时候,才可以对键进行设置操作
  4. XX:只有键已经存在的时候,才对键进行设置操作
  5. 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的过期时间变得分散。