Redis 实现分布式锁

464 阅读3分钟

1.简介

在单机场景中,解决资源互斥访问的话,可以使用synchronized或者ReetrantLock去进行实现;但是,随着分布式的架构出现,那么,就需用通过分布式锁来控制分布式系统之间共享资源的访问了;
分布式锁大概可以通过三种方式:

  • 数据库实现分布式锁
  • zk实现分布式锁
  • redis实现分布式锁 我这篇文章主要是讲解一下redis实现分布式锁

2.实现

在redis中,有一个语句 set key value [EX seconds|PX milliseconds] [NX|XX]

  • EX seconds: 将键的过期时间设置为seconds秒
  • PX milliseconds: 将键的过期时间设置为milliseconds毫秒
  • NX 只有键不存在时,才能对键进行设置
  • XX 只有键已经存在时,才对键进行设置

所以,当key不存在时,才能对键进行设置,当key存在时,不对key进行设置;这个功能很符合我们分布式锁的定义;

set "key" 100 ex 100 nx
OK
set "key" 100 ex 100 nx
(nil)

在这部分的实现,有很多大佬已经实现过了;大佬文章我自己在学习的过程中,也是查看该大佬的文章来进行实现的;

2.1 Redis实现分布式锁问题

我们都知道,我们使用原子性的Redis语句set key value NX,让多个线程想要获取锁的时候,只有一个线程获取到;但是,如果,该获取到锁的线程发生异常,或者业务处理完毕没有即使释放锁,会导致其他线程会一直尝试获取锁阻塞;但是,这个时候,又引入了一个新的问题,超时时间设定多久合适呢?超时时间太长的话,别的线程会进行阻塞等待;超时时间太短的话,可能当前线程还没有执行完毕,这个资源就被别的线程获取访问了。

2.2 解决方案

由于,这个业务处理的时间,受各个因素所导致,比如说:网络、并发量、当前系统IO CPU的占用情况等因素影响,需要进行动态的设置;所以,我采用一种Future / Callable的方式来进行动态增加key值的存活时间;
流程如下:

  • 1.前端页面传过来id时,通过,AOP获取到该参数
  • 2.根据获取的参数,去获取对应的分布式锁,如果,没有获取到就直接报错(可以做相应更改)
  • 3.把对应Controller层的方法,提交到Callable中进行执行,获取对应的Futrue;
  • 4.获取到Futurn之后,每一定时间调用一次isDone(),如果,执行完毕了,就释放锁;如果,还没执行完毕,就增加当前id的存活时间; 相关代码如下:
/**
 * @description: AOP拦截器
 * @author: lgb
 * @create: 2019/05/17 20:54
 */
@Aspect
@Component
public class LockMethodAspect {
    @Autowired
    private RedisLockHelper redisLockHelper;
    @Autowired
    private JedisUtil jedisUtil;
    private Logger logger = LoggerFactory.getLogger(LockMethodAspect.class);

    @Around("@annotation(com.redis.lock.annotation.RedisLock)")
    public Object around(ProceedingJoinPoint joinPoint) {
        Jedis jedis = jedisUtil.getJedis();
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();

        RedisLock redisLock = method.getAnnotation(RedisLock.class);
        String value = UUID.randomUUID().toString();

        Object[] args = joinPoint.getArgs();
        String key = redisLock.key();
        if (args != null && args.length > 0) {
            Object arg = args[0];
            if (arg != null) {
                key = (String) arg;

            }
        }


        try {
            final boolean islock = redisLockHelper.lock(jedis, key, value, redisLock.expire(), redisLock.timeUnit());
            logger.info("isLock : {}", islock);
            if (!islock) {
                logger.error("获取锁失败");
                throw new RuntimeException("获取锁失败");
            }
            try {
                logger.info("获取到锁:" + key);
                String result = "";
                ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1, 1, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1));
                Callable<String> callable = new Callable<String>() {
                    @Override
                    public String call() throws Exception {
                        try {
                            return (String) joinPoint.proceed();
                        } catch (Throwable throwable) {
                            logger.warn("业务抛出了异常");
                            throwable.printStackTrace();
                        }
                        return null;
                    }
                };
                Future<String> future = threadPoolExecutor.submit(callable);
                while (true) {
                    Thread.sleep(1000);
                    if (future.isDone()) {
                        result = future.get();
                        break;
                    } else {
                        jedis.expire(key, 1);
                    }
                }
                return result;
            }
            catch (Throwable throwable) {
                throw new RuntimeException("系统异常");
            }
        } finally {
            logger.info("释放锁");
            redisLockHelper.unlock(jedis, key, value);
            jedis.close();
        }
    }
}

2.3 相关测试

Postman 同时输入以下网址进行测试:

http://localhost:8080/index?id=11
返回index
http://localhost:8080/index?id=11
    "timestamp": "2021-05-17T12:56:40.038+0000",
    "status": 500,
    "error": "Internal Server Error",
    "message": "获取锁失败",
    "path": "/index"
http://localhost:8080/index?id=12
index

两个线程id = 11去访问,会失败;一个线程id = 12去访问,就成功了,在一定时间内,只能有一个id去访问后端数据; 符合预期
gitte相关代码