栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

基于RedisTemplate和Redisson的redis分布式锁(2021)

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

基于RedisTemplate和Redisson的redis分布式锁(2021)

基于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 见简介

文章目录
      • 基于RedisTemplate和Redisson的redis分布式锁
    • 1. 基于RedisTemplate的分布式锁
    • 2. 基于Redisson的实现
    • 3. 注解
    • 参考文献

1. 基于RedisTemplate的分布式锁

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

import com.alibaba.fastjson.support.spring.FastJsonRedisSerializer;
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 java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.Objects;
import java.util.concurrent.TimeUnit;


@Component
public class RedisLockTool {
    private static final Logger logger = LoggerFactory.getLogger(RedisLockTool.class);
    private final RedisSerializer redisSerializerString = StringRedisSerializer.UTF_8;
    private final RedisSerializer redisSerializerLong = new FastJsonRedisSerializer(Long.class);
    private final Jackson2JsonRedisSerializer objectJackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
    private final long defautTimeoutMs = 120_000L;
    private static final Redisscript 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 script_LOCK = new DefaultRedisscript("return redis.call('SET', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2])", String.class);

    @Autowired
    StringRedisTemplate redisTemplate;

    
    public String lock(String key, String identifier, long lockExp) {
        return lock(key, identifier, lockExp, defautTimeoutMs);
    }

    
    public String lock(String key, String identifier, long lockExp, long timeout) {
        identifier = getIdentifier(identifier);
        key = getKey(key);
        // 根据并发调整超时时间
        long end = System.currentTimeMillis() + timeout;
        while (System.currentTimeMillis() < end) {
            if (setNx(key, identifier, lockExp)) {
                // 返回value值,用于释放锁时间确认
                logger.info("lock success, key:{}, identifier:{}", key, identifier);
                return identifier;
            } else {
                logger.info("lock fail, key:{}", key);
            }

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

    
    public String lockNoWait(String key, String identifier, long lockExp) {
        identifier = getIdentifier(identifier);
        key = getKey(key);
        if (setNx(key, identifier, lockExp)) {
            // 返回value值,用于释放锁时间确认
            logger.info("lock success, key:{}, identifier:{}", key, identifier);
            return identifier;
        } else {
            logger.info("lock fail, key:{}", key);
        }
        return null;
    }

    
    public String resetLockExp(String key, String identifier, long lockExp) {
        identifier = getIdentifier(identifier);
        String lockKey = getKey(key);

        if (identifier.equals(getValue(lockKey))) {
            expireByMill(lockKey, lockExp);
            // 返回value值,用于释放锁时间确认
            logger.info("reset lock success, key:{}, id:{}", lockKey, identifier);
            return identifier;
        } else {
            logger.info("reset lock fail, key:{}", lockKey);
            return null;
        }

    }

    private String getIdentifier(String identifier) {
        if (StringUtils.isBlank(identifier)) {
            identifier = TimeUtils.now(TimeUtils.ISO_DATETIME_WITHOUT_HYPHEN) + "-" + getLocalHostName();
        }
        return identifier;
    }

    
    public boolean unlock(String key, String identifier) {
        key = getKey(key);
        Long execute = redisTemplate.execute(script_DEL, objectJackson2JsonRedisSerializer, redisSerializerLong, Collections.singletonList(key), identifier);
//        return execute != null && execute >= 1L;
        return Objects.equals(execute, 1L);
    }

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

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

    public String getKey(String keyName) {
        return "LOCK:" + keyName;
    }

    private String getLocalHostName() {
        try {
            return InetAddress.getLocalHost().getHostName();
        } catch (UnknownHostException e) {
            logger.error("InetAddress.getLocalHost出现异常", e);
            return StringUtils.EMPTY;
        }
    }

    private void remove(String key) {
        redisTemplate.delete(key);
    }

    
    private boolean setNx(final String key, final String value, long ms) {
        try {
            String execute = redisTemplate.execute(script_LOCK, objectJackson2JsonRedisSerializer, redisSerializerString, Collections.singletonList(key), value, ms);
            return "OK".equals(execute);
        } catch (Exception e) {
            logger.error("lock失败------------", e);
            logger.error("redis setNxEx error, key : {}", key);
            return false;
        }
    }

    private void expireByMill(String key, long exp) {
        redisTemplate.expire(key, exp, TimeUnit.MILLISECONDS);
    }

    
    public Long ttl(String key) {
        return redisTemplate.getExpire(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;


@Component
public class RedissonLockTool {
    private static final Logger logger = LoggerFactory.getLogger(RedissonLockTool.class);
    @Autowired
    RedissonClient redisson;
    private final TimeUnit unit = TimeUnit.MILLISECONDS;

    
    public void lock(String lockName) {
        RLock rLock = redisson.getLock(lockName);
        rLock.lock();
    }

    
    public void lock(String lockName, long leaseTime) {
        RLock rLock = redisson.getLock(lockName);
        rLock.lock(leaseTime, unit);
    }

    
    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;
    }

    
    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;
    }

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

    
    public boolean isLocked(String lockName) {
        RLock rLock = redisson.getLock(lockName);
        return rLock.isLocked();
    }


    
    public boolean holdsLock(String lockName) {
        RLock rLock = redisson.getLock(lockName);
        return rLock.isHeldByCurrentThread();
    }

}

3. 注解

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

1.注解类

import java.lang.annotation.*;


@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;


@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 欢迎交流,共同进步。
欢迎转载,转载请注明本网址。
转载请注明:文章转载自 www.mshxw.com
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号