Redis分布式锁机制

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/caox_nazi/article/details/85624232

Redis分布式锁机制

【基本机制】:

基于redis实现的Java分布式锁主要依赖redis的SETNX()命令和DEL()命令,SETNX相当于上锁(lock),DEL相当于释放锁(unlock)。我们只要实现Lock接口重写lock()和unlock()即可。但是这还不够,安全可靠的分布式锁应该满足满足下面三个条件:

  • l 互斥,不管任何时候,只有一个客户端能持有同一个锁。
  • l 不会死锁,最终一定会得到锁,即使持有锁的客户端对应的master节点宕掉。
  • l 容错,只要大多数Redis节点正常工作,客户端应该都能获取和释放锁。

【问题】:

什么情况下回不满足上面三个条件呢。多个线程(客户端)同时竞争锁可能会导致多个客户端同时拥有锁。比如,

(1)线程1在master节点拿到了锁(存入key)

(2)master节点在把线程1创建的key写入slave之前宕机了,此时集群中的节点已经没有锁(key)了,包括master节点的slaver节点

(3)slaver节点升级为master节点

(4)线程2向新的master节点发起锁(存入key)请求,很明显,能请求成功。

可见,线程1和线程2同时获得了锁。如果在更高并发的情况,可能会有更多线程(客户端)获取锁

  • 【死锁】: 

拥有锁的线程(客户端)长时间的执行或者因为某种原因造成阻塞,就会导致锁无法释放(unlock没有调用),其它线程就不能获取锁而而产生无限期死锁的情况。其它线程在执行lock失败后即使粗暴的执行unlock删除key之后也不能正常释放锁,因为锁就只能由获得锁的线程释放,锁不能正常释放其它线程仍然获取不到锁。

设置锁的有效时间(redis的expire命令),不管是什么原因导致的死锁,有效时间过后,锁将会被自动释放 

  • 【解决方法】: 
  •  【容错】:(防止阻塞)

 只要有Redis节点正常工作,客户端应该都能获取和释放锁,我们必须用相同的key不断循环向Master节点请求锁,当请求时间超过设定的超时时间则放弃请求锁,这个可以防止一个客户端在某个宕掉的master节点上阻塞过长时间,如果一个master节点不可用了,应该尽快尝试下一个master节点。释放锁比较简单,因为只需要在所有节点都释放锁就行,不管之前有没有在该节点获取锁成功

 【RedLock算法流程】:

【Resis分布式锁sping配置依赖】:

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.caox</groupId>
  <artifactId>spring-demo</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>war</packaging>

  <name>spring-demo Maven Webapp</name>
  <!-- FIXME change it to the project's website -->
  <url>http://www.example.com</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.7</maven.compiler.source>
    <maven.compiler.target>1.7</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <!-- spring版本号 -->
    <spring.version>4.1.1.RELEASE</spring.version>
    <!-- mybatis版本号 -->
    <mybatis.version>3.2.6</mybatis.version>
    <!-- log4j日志文件管理包版本 -->
    <slf4j.version>1.7.7</slf4j.version>
    <log4j.version>1.2.17</log4j.version>
    <!-- jackson包版本 -->
    <jackson.version>2.5.0</jackson.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>

    <!--spring单元测试依赖 -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
      <version>${spring.version}</version>
      <scope>test</scope>
    </dependency>

    <!-- springMVC核心包 -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>${spring.version}</version>
    </dependency>

    <!-- spring核心包 -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-core</artifactId>
      <version>${spring.version}</version>
    </dependency>

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-beans</artifactId>
      <version>${spring.version}</version>
    </dependency>

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>${spring.version}</version>
    </dependency>

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context-support</artifactId>
      <version>${spring.version}</version>
    </dependency>

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-aop</artifactId>
      <version>${spring.version}</version>
    </dependency>

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-aspects</artifactId>
      <version>4.0.9.RELEASE</version>
      <!--<exclusions>-->
        <!--<exclusion>-->
          <!--<groupId>org.aspectj</groupId>-->
          <!--<artifactId>aspectjweaver</artifactId>-->
        <!--</exclusion>-->
      <!--</exclusions>-->
    </dependency>

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-tx</artifactId>
      <version>${spring.version}</version>
    </dependency>

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-web</artifactId>
      <version>${spring.version}</version>
    </dependency>

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-jdbc</artifactId>
      <version>${spring.version}</version>
    </dependency>

    <!-- AOP begin -->
    <dependency>
      <groupId>org.aspectj</groupId>
      <artifactId>aspectjrt</artifactId>
      <version>1.7.4</version>
    </dependency>
    <dependency>
      <groupId>org.aspectj</groupId>
      <artifactId>aspectjweaver</artifactId>
      <version>1.7.4</version>
    </dependency>
    <dependency>
      <groupId>cglib</groupId>
      <artifactId>cglib</artifactId>
      <version>3.1</version>
    </dependency>
    <!-- AOP end -->

    <!-- Mysql数据库驱动包 -->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.34</version>
    </dependency>
    <!-- log start -->
    <dependency>
      <groupId>log4j</groupId>
      <artifactId>log4j</artifactId>
      <version>${log4j.version}</version>
    </dependency>

    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>${slf4j.version}</version>
    </dependency>

    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-log4j12</artifactId>
      <version>${slf4j.version}</version>
    </dependency>
    <!-- log end -->
    <!--servlet-->
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>3.0.1</version>
      <scope>provided</scope>
    </dependency>

    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-dbcp2</artifactId>
      <version>2.0</version>
    </dependency>

    <!--redis-->
    <dependency>
      <groupId>org.springframework.data</groupId>
      <artifactId>spring-data-redis</artifactId>
      <version>1.4.2.RELEASE</version>
      <!--<exclusions>-->
        <!--<exclusion>-->
          <!--<groupId>org.springframework</groupId>-->
          <!--<artifactId>spring-aop</artifactId>-->
        <!--</exclusion>-->
        <!--<exclusion>-->
          <!--<groupId>org.springframework</groupId>-->
          <!--<artifactId>spring-context-support</artifactId>-->
        <!--</exclusion>-->
        <!--<exclusion>-->
          <!--<groupId>org.springframework</groupId>-->
          <!--<artifactId>spring-tx</artifactId>-->
        <!--</exclusion>-->
        <!--<exclusion>-->
          <!--<groupId>org.springframework</groupId>-->
          <!--<artifactId>spring-context</artifactId>-->
        <!--</exclusion>-->
        <!--<exclusion>-->
          <!--<groupId>org.springframework</groupId>-->
          <!--<artifactId>spring-core</artifactId>-->
        <!--</exclusion>-->
        <!--<exclusion>-->
          <!--<groupId>org.slf4j</groupId>-->
          <!--<artifactId>slf4j-api</artifactId>-->
        <!--</exclusion>-->
      <!--</exclusions>-->
    </dependency>

    <dependency>
      <groupId>redis.clients</groupId>
      <artifactId>jedis</artifactId>
      <version>2.6.2</version>
      <!--<exclusions>-->
        <!--<exclusion>-->
          <!--<groupId>org.apache.commons</groupId>-->
          <!--<artifactId>commons-pool2</artifactId>-->
        <!--</exclusion>-->
      <!--</exclusions>-->
    </dependency>

    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-pool2</artifactId>
      <version>2.4.2</version>
    </dependency>
    <!--redis-->

    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.14.4</version>
    </dependency>
  </dependencies>

  <build>
    <finalName>spring-demo</finalName>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <source>1.7</source>
          <target>1.7</target>
        </configuration>
      </plugin>
    </plugins>

    <!-- 解决Maven项目编译后classes文件中没有.xml问题 -->
    <resources>
      <resource>
        <directory>src/main/java</directory>
        <includes>
          <include>*.properties</include>
          <include>*.xml</include>
        </includes>
        <filtering>true</filtering>
      </resource>
      <resource>
        <directory>src/main/java/resources</directory>
        <includes>
          <include>*.properties</include>
          <include>*.xml</include>
        </includes>
        <filtering>true</filtering>
      </resource>
    </resources>

    <!-- 解决Maven项目编译后classes文件中没有.xml问题 -->
  </build>
</project>

【RedisLock Java 实现】:

package com.caox.redis;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.data.redis.core.StringRedisTemplate;

import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
 * @author : nazi
 * @version : 1.0
 * @date : 2018/12/29 14:22
 */
public class RedisLock implements Lock{
    protected StringRedisTemplate redisStringTemplate;

    // 存储到redis中的锁标志
    private static final String LOCKED = "LOCKED";

    // 请求锁的超时时间(ms)
    private static final long TIME_OUT = 30000;

    // 锁的有效时间(s)
    public static final int EXPIRE = 60;

    // 锁标志对应的key;
    private String key;

    // state flag
    private volatile boolean isLocked = false;

    public RedisLock(String key) {
        this.key = key;
        @SuppressWarnings("resource")
        ApplicationContext ctx =  new ClassPathXmlApplicationContext("classpath*:application-context.xml");
        redisStringTemplate = (StringRedisTemplate)ctx.getBean("redisStringTemplate");
    }

    @Override
    public void lock() {
        //系统当前时间,毫秒
        long nowTime = System.nanoTime();
        //请求锁超时时间,毫秒
        long timeout = TIME_OUT*1000000;
        final Random r = new Random();
        try {
            //不断循环向Master节点请求锁,当请求时间(System.nanoTime() - nano)超过设定的超时时间则放弃请求锁
            //这个可以防止一个客户端在某个宕掉的master节点上阻塞过长时间
            //如果一个master节点不可用了,应该尽快尝试下一个master节点
            while ((System.nanoTime() - nowTime) < timeout) {
                //将锁作为key存储到redis缓存中,存储成功则获得锁
                if (redisStringTemplate.getConnectionFactory().getConnection().setNX(key.getBytes(),
                        LOCKED.getBytes())) {
                    //设置锁的有效期,也是锁的自动释放时间,也是一个客户端在其他客户端能抢占锁之前可以执行任务的时间
                    //可以防止因异常情况无法释放锁而造成死锁情况的发生
                    redisStringTemplate.expire(key, EXPIRE, TimeUnit.SECONDS);
                    isLocked = true;
                    //上锁成功结束请求
                    break;
                }
                //获取锁失败时,应该在随机延时后进行重试,避免不同客户端同时重试导致谁都无法拿到锁的情况出现
                //睡眠3毫秒后继续请求锁
                Thread.sleep(3, r.nextInt(500));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void unlock() {
        //释放锁
        //不管请求锁是否成功,只要已经上锁,客户端都会进行释放锁的操作
        if (isLocked) {
            redisStringTemplate.delete(key);
        }
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        // TODO Auto-generated method stub

    }

    @Override
    public boolean tryLock() {
        // TODO Auto-generated method stub
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        // TODO Auto-generated method stub
        return false;
    }

    @Override
    public Condition newCondition() {
        // TODO Auto-generated method stub
        return null;
    }
}

【参考文献】: 分布式缓存技术redis系列(五)——redis实战(redis与spring整合,分布式锁实现)

【springboot + redis分布式 + aop】:

package com.caox.aop;

import com.caox.annotions.RedisSync;
import com.caox.service.IRedisService;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.util.Objects;


/**
 * @author : nazi
 * @version : 1.0
 * @date : 2019/1/3 10:53
 */
@Slf4j
@Aspect
@Component
public class RedisSyncAop {
    private static final Logger logger = LoggerFactory.getLogger(RedisSyncAop.class);
    @Resource
    private IRedisService iRedisService;

    @Pointcut("@annotation(com.caox.annotions.RedisSync)")
    private void anyMethod(){
    }

    @Around("anyMethod()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        Object result = null;
        //获得锁
        Method method = ((MethodSignature)pjp.getSignature()).getMethod();
        String key = method.toString();
        RedisSync redisSync = method.getAnnotation(RedisSync.class);
        long waitTime = redisSync.waitTime();
        long currTime = System.currentTimeMillis();
        Boolean state = iRedisService.setNx(key, currTime);
        long saveTime = 0L;
        while (!state) {
            // 之前存在key 并发开始
            Long tempSaveTime = iRedisService.get(key, Long.class);
            // 若锁被释放
            if (tempSaveTime == null) {
                // 重新加锁
                state = iRedisService.setNx(key, currTime);
                continue;
            }
            // 锁被重新获取
            if (!tempSaveTime.equals(saveTime)) {
                currTime = System.currentTimeMillis();
                saveTime = tempSaveTime;
            }
            // 判断是否超时
            if (saveTime + redisSync.timeout() < currTime) {
                // 超时,直接获得锁  获取上一个锁的时间value
                Object tempTime = iRedisService.getSet(key, currTime);
                if(tempTime == null){
                    state = iRedisService.setNx(key, currTime);
                    continue;
                }
                // 判断锁是否被释放 或 未被抢先获取  saveTime = tempSaveTime; tempTime(获取上一个锁时间value)
                if (Objects.equals(saveTime, tempTime)) {
                    logger.warn("方法:{},执行超时,已被强制解锁!", key);
                    break;
                }
            }
            // 等待
            if(waitTime > 0) {
                try {
                    Thread.sleep(waitTime);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            state = iRedisService.setNx(key, currTime);
        }
        // 执行方法
        result = pjp.proceed();
        Long currSaveTime = iRedisService.get(key, Long.class);
        // 判断锁未被判定为超时
        if (currSaveTime != null && Objects.equals(currSaveTime, currTime)) {
            // 释放锁
            iRedisService.del(key);
        }
        return result;
    }
}

【参考文献】:SpringBoot+Redis 从HelloWorld到分布式锁

 

猜你喜欢

转载自blog.csdn.net/caox_nazi/article/details/85624232