基于RedisTemplate和Redisson的redis分布式锁

欢迎转载,转载请注明网址:https://blog.csdn.net/qq_41910280

简介
  本文基于spring data redis和redisson两种方式完成了redis分布式锁, 以及注解实现。

版本说明
2019-03-07 见https://blog.csdn.net/qq_41910280/article/details/88837576
2021-12-07 见简介

1. 基于RedisTemplate的分布式锁

  
优点:加锁和解锁都基于lua脚本,保证操作的原子性,同时不依赖于特定redis客户端实现
缺点:1.不可重入;(修改也只需要修改脚本)
   2.没有自动续期(可以使用ScheduledExecutorService,不要使用Timer,会有系统时钟依赖)

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * redis分布式锁
 *
 * @author zhouyou
 * @date 2020/1/17 17:22
 * @email zhouyouchn@126.com
 */
@Component
public class RedisLockTool {
    private static final Logger logger = LoggerFactory.getLogger(RedisLockTool.class);
    private RedisSerializer<String> stringSerializer = StringRedisSerializer.UTF_8;
    private RedisSerializer<Long> longSerializer = new Jackson2JsonRedisSerializer<>(Long.class);
    private RedisSerializer<Object> argsSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
    private static final long DEFAUT_TIMEOUT_MS = 30_000L;
    private static final RedisScript<Long> SCRIPT_DEL = new DefaultRedisScript("if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return '0' end", Long.class);
    private static final RedisScript<String> SCRIPT_LOCK = new DefaultRedisScript("return redis.call('SET', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2])", String.class);

    private String host;
    private String pid;
    @Autowired
    StringRedisTemplate redisTemplate;

    @PostConstruct
    public void init() {
        String host;
        try {
            host = InetAddress.getLocalHost().getHostAddress();
        } catch (UnknownHostException e) {
            logger.error("getLocalHost异常", e);
            host = StringUtils.EMPTY;
        }
        this.host = host;

        RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
        String name = runtimeMXBean.getName();
        String pid = "0";
        if (StringUtils.isNotBlank(name) && name.contains("@")) {
            pid = name.substring(0, name.indexOf("@"));
        }
        this.pid = pid;
    }

    /**
     * 加锁
     *
     * @param key        锁的key
     * @param identifier 锁标识
     * @param lockExp    锁的超时时间 毫秒
     * @return 锁标识 null表示加锁失败
     */
    public String lock(String key, String identifier, long lockExp) {
        return lock(key, identifier, lockExp, DEFAUT_TIMEOUT_MS);
    }

    /**
     * 加锁
     *
     * @param key        锁的key
     * @param identifier 锁标识
     * @param lockExp    锁的超时时间 毫秒
     * @param timeout    超时时间
     * @return 锁标识 null表示加锁失败
     */
    public String lock(String key, String identifier, long lockExp, long timeout) {
        identifier = getIdentifier(identifier);
        // 根据并发调整超时时间
        if (timeout <= 0) {
            return tryLock0(key, identifier, lockExp) ? identifier : null;
        }
        long end = System.currentTimeMillis() + timeout;
        while (System.currentTimeMillis() < end) {
            if (tryLock0(key, identifier, lockExp)) {
                return identifier;
            }

            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                // 业务自行处理
                Thread.currentThread().interrupt();
            }
        }
        return null;
    }

    private boolean tryLock0(String key, String identifier, long lockExp) {
        if (setNx(key, identifier, lockExp)) {
            // 返回value值,用于释放锁时间确认
            logger.info("lock success, key:{}, identifier:{}", key, identifier);
            return true;
        } else {
            logger.info("lock fail, key:{}", key);
        }
        return false;
    }

    /**
     * 加锁 不阻塞
     * 用于分布式中只需要一个服务进行运算
     *
     * @param key        锁的key
     * @param identifier 锁标识
     * @param lockExp    锁的超时时间 毫秒
     * @return 锁标识 null表示加锁失败
     */
    public String tryLock(String key, String identifier, long lockExp) {
        identifier = getIdentifier(identifier);
        return tryLock0(key, identifier, lockExp) ? identifier : null;
    }

    private String getIdentifier(String identifier) {
        if (StringUtils.isBlank(identifier)) {
            return host + "-" + pid + "-" + Thread.currentThread().getId();
        }
        return identifier;
    }

    /**
     * 释放锁
     *
     * @param key        锁的key
     * @param identifier 释放锁的标识
     * @return
     */
    public boolean unlock(String key, String identifier) {
        Long execute = redisTemplate.execute(SCRIPT_DEL, argsSerializer, longSerializer, Collections.singletonList(key), getIdentifier(identifier));
        boolean exeFlag = Objects.equals(execute, 1L);
        logger.info("unlock {}, key: {}, identifier: {}", exeFlag ? "success" : "fail", key, identifier);
        return exeFlag;
    }

    /**
     * @param key
     * @param value
     * @param ms
     * @return
     */
    private boolean setNx(final String key, final String value, long ms) {
        try {
            String execute = redisTemplate.execute(SCRIPT_LOCK, argsSerializer, stringSerializer, Collections.singletonList(key), value, ms);
            return "OK".equals(execute);
        } catch (Exception e) {
            logger.error(String.format("lock失败, key: %s", key), e);
            return false;
        }
    }


    /**
     * @param key
     * @return 秒
     */
    public Long ttl(String key) {
        return redisTemplate.getExpire(key);
    }

    public String get(String key) {
        return redisTemplate.opsForValue().get(key);
    }

    /**
     * 重置锁时效
     *
     * @param key
     * @param ms
     * @return
     */
    public Boolean expire(String key, Long ms) {
        Boolean expire = redisTemplate.expire(key, ms, TimeUnit.MILLISECONDS);
        return expire;
    }

    public Boolean del(String key) {
        return redisTemplate.delete(key);
    }
}
       

2. 基于Redisson的实现

  
优点:支持重入,watchdog自动续期
缺点:1.一个线程加的锁只能同一线程解锁,对线程间协作支持不好
2.需要redisson支持
3.对于多redis实例的情况,当刚获取锁后master宕机,而数据尚未同步至slave,其他客户端可以从该slave点(晋级为master)获得锁。想解决这种问题,需要使用RedLock算法(详见参考文档章节),获得至少N/2+1个Redis实例的锁才算加锁成功,否则立即释放锁,并在一个随机延时之后重试(避免活锁)

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

/**
 * Redisson分布式锁
 * lockName是hash-hash值
 * RedissonClient会有一个uuid标识, 再加线程id, 组成hash-key值(即 非同一个线程无法解锁)
 * hash-value值为重入次数
 *
 * @author zhouyou
 * @date 2020/1/17 17:22
 * @email zhouyouchn@126.com
 */
@Component
public class RedissonLockTool {
    private static final Logger logger = LoggerFactory.getLogger(RedissonLockTool.class);
    @Autowired
    RedissonClient redisson;
    private final TimeUnit unit = TimeUnit.MILLISECONDS;

    /**
     * 加锁 watchdog自动续期
     * 如果没有在finally释放 可能会导致死锁
     *
     * @param lockName
     */
    public void lock(String lockName) {
        RLock rLock = redisson.getLock(lockName);
        rLock.lock();
    }

    /**
     * 加锁
     *
     * @param lockName
     * @param leaseTime 锁最大有效时间
     */
    public void lock(String lockName, long leaseTime) {
        RLock rLock = redisson.getLock(lockName);
        rLock.lock(leaseTime, unit);
    }

    /**
     * 加锁 成功之前最多等待waitTime时间
     *
     * @param lockName
     * @param waitTime
     * @return
     */
    public boolean tryLock(String lockName, long waitTime) {
        RLock rLock = redisson.getLock(lockName);
        boolean getLock = false;
        try {
            getLock = rLock.tryLock(waitTime, unit);
        } catch (InterruptedException e) {
            logger.error("tryLock fail,lockName=" + lockName, e);
        }
        return getLock;
    }

    /**
     * 加锁操作
     *
     * @param lockName
     * @param leaseTime 锁最大有效时间
     * @param waitTime  加锁等待时间
     * @return
     */
    public boolean tryLock(String lockName, long leaseTime, long waitTime) {
        RLock rLock = redisson.getLock(lockName);
        boolean getLock = false;
        try {
            getLock = rLock.tryLock(waitTime, leaseTime, unit);
        } catch (InterruptedException e) {
            logger.error("tryLock fail,lockName=" + lockName, e);
        }
        return getLock;
    }

    /**
     * 解锁
     *
     * @param lockName
     */
    public boolean unlock(String lockName) {
        try {
            redisson.getLock(lockName).unlock();
        } catch (Exception e) {
            logger.error("unlock fail", e);
            return false;
        }
        return true;
    }

    /**
     * 该锁是否已经被任何线程锁定
     *
     * @param lockName 锁名称
     */
    public boolean isLocked(String lockName) {
        RLock rLock = redisson.getLock(lockName);
        return rLock.isLocked();
    }


    /**
     * 该锁是否已经被当前线程锁定
     *
     * @param lockName 锁名称
     */
    public boolean holdsLock(String lockName) {
        RLock rLock = redisson.getLock(lockName);
        return rLock.isHeldByCurrentThread();
    }

}

3. 注解

  优点:开发简单
  缺点:粒度较大,不够灵活,内部调用无效

1.注解类

import java.lang.annotation.*;

/**
 * DistributedLock
 *
 * @author zhouyou
 * @date 2019/10/28 18:18
 * @email zhouyouchn@126.com
 */
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DistributedLock {

    /**
     * 锁的名称
     */
    String value() default "redisson";

    /**
     * 锁的有效时间
     */
    int leaseTime() default 10000;
}

2.处理注解的类

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.test.utils.RedissonLockTool;

/**
 * DistributedLock注解处理
 *
 * @author zhouyou
 * @date 2019/10/28 18:18
 * @email zhouyouchn@126.com
 */
@Aspect
@Component
public class DistributedLockHandler {
    private static final Logger logger = LoggerFactory.getLogger(DistributedLockHandler.class);

    @Autowired
    RedissonLockTool lockTool;


    @Around("@annotation(distributedLock)")
    public void around(ProceedingJoinPoint joinPoint, DistributedLock distributedLock) {
        logger.info("[开始]@DistributedLock");
        //获取锁名称
        String lockName = distributedLock.value();
        //获取超时时间,默认10秒
        int leaseTime = distributedLock.leaseTime();
        try {
            lockTool.lock(lockName, leaseTime);
            Thread thread = Thread.currentThread();
            logger.info("lock:{} by thread{}:{}", lockName, thread.getId(), thread.getName());
            joinPoint.proceed();
        } catch (Throwable throwable) {
            logger.error("lock fail", throwable);
            // todo 发送报警信息
        } finally {
            // 解锁
            if (lockTool.holdsLock(lockName)) {
                lockTool.unlock(lockName);
            }
        }
        logger.info("[结束]@DistributedLock");
    }
}

测试代码:

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.test.annotation.DistributedLock;
import org.test.utils.RedisLockTool;
import org.test.utils.RedissonLockTool;

import static java.util.concurrent.TimeUnit.SECONDS;


@Api(value = "测试", tags = "测试")
@RestController
@RequestMapping("cloud/test/")
public class TestController {
    public static final Logger logger = LoggerFactory.getLogger(TestController.class);
    @Autowired
    RedissonLockTool redissonLockTool;
    @Autowired
    RedisLockTool redisLockTool;

    @ApiOperation("锁")
    @GetMapping("lock")
    public Object lock() {
        redisLockTool.lock("name", "1234", 30000L);
        System.out.println(Thread.currentThread().getName() + ": lock success");
        return "ok";
    }

    @ApiOperation("解锁")
    @GetMapping("unlock")
    public Object unlock() {
        boolean unlock = redisLockTool.unlock("name", "1234");
        System.out.println(unlock);
        System.out.println(Thread.currentThread().getName() + ": unlock success");
        return "ok";
    }

    @ApiOperation("redissonLock")
    @GetMapping("redissonLock")
    public Object redissonLock() {
        redissonLockTool.lock("name");
        System.out.println(Thread.currentThread().getName() + ": lock success");
        return "ok";
    }

    @ApiOperation("redissonUnlock")
    @GetMapping("redissonUnlock")
    public Object redissonUnlock() {
        redissonLockTool.unlock("name");
        System.out.println(Thread.currentThread().getName() + ": unlock success");
        return "ok";
    }

    @ApiOperation("annotationLock")
    @GetMapping("annotationLock")
    @DistributedLock(value = "annotationLock", leaseTime = 20000)
    public Object annotationLock() {
        System.out.println("do something...");
        try {
            SECONDS.sleep(15);
        } catch (InterruptedException e) {
        }
        return "ok";
    }


}

参考文献

1.https://codeload.github.com/yudiandemingzi/spring-boot-distributed-redisson
2.redis官方文档
RedLock算法
  我们考虑这样一种场景,假设我们的redis没用使用备份。一个客户端获取到了3个实例的锁。此时,其中一个已经被客户端取到锁的redis实例被重启,在这个时间点,就可能出现3个节点没有设置锁,此时如果有另外一个客户端来设置锁,锁就可能被再次获取到,这样锁的互相排斥的特性就被破坏掉了。
  如果我们启用了AOF持久化,情况会好很多。我们可用使用SHUTDOWN命令关闭然后再次重启。因为Redis到期是语义上实现的,所以当服务器关闭时,实际上还是经过了时间,所有(保持锁)需要的条件都没有受到影响. 没有受到影响的前提是redis优雅的关闭。停电了怎么办?如果redis是每秒执行一次fsync,那么很有可能在redis重启之后,key已经丢弃。理论上,如果我们想在Redis重启地任何情况下都保证锁的安全,我们必须开启fsync=always的配置。这反过来将完全破坏与传统上用于以安全的方式实现分布式锁的同一级别的CP系统的性能.

然而情况总比一开始想象的好一些。当一个redis节点重启后,只要它不参与到任意当前活动的锁,没有被当做“当前存活”节点被客户端重新获取到,算法的安全性仍然是有保障的。
  为了达到这种效果,我们只需要将新的redis实例,在一个TTL时间内,对客户端不可用即可,在这个时间内,所有客户端锁将被失效或者自动释放.
  使用延迟重启可以在不采用持久化策略的情况下达到同样的安全,然而这样做有时会让系统转化为彻底不可用。比如大部分的redis实例都崩溃了,系统在TTL时间内任何锁都将无法加锁成功。

3.redisson官方文档

神奇的小尾巴:
本人邮箱:zhouyouchn@126.com zhoooooouyou@gmail.com
zhouyou@whut.edu.cn 欢迎交流,共同进步。
欢迎转载,转载请注明本网址。
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐