Redis实现分布式锁


前言

在多线程环境中,如果多个线程同时访问共享资源(例如多机进行缓存预热,购买商品库存等),会发生数据竞争,可能会导致出现脏数据或者系统问题,威胁到程序的正常运行。

因此,需要在同一时刻只有一个线程能够访问共享资源

一般可以基于Redis或者ZooKeeper来实现分布式锁。使用MySQL的方式由于数据库性能受限,一般不考虑。
Redis的实现较为通用简易,这里主要介绍Redis的实现方式。


一、简易的分布式锁

1.分布式锁的基本要求

要实现锁,首先要确保锁在同一时间内只能被一个线程拥有,即互斥
其次,该线程修改共享资源的操作结束后需要将锁释放,避免产生死锁。

2.加锁

在Redis中,可以使用SETNX(SET IF NOT EXISTS)来实现简单的互斥。
该方式可以在仅当key不存在时才设置key的值。
也就是只有当锁不存在时,才会尝试加锁。

SETNX lockKey value
(integer) 1
SETNX lockKey value
(integer) 0

Key 值应该是唯一的,以避免不同的锁之间发生冲突。通常使用业务相关的信息来构造 key 值。
例如需要锁定一个订单,可以使用 lock:order:<order_id> 作为 key 值。

Value 值应该是一个唯一标识符,用于标识持有该锁的客户端。这样可以确保在释放锁时,只有持有该锁的客户端才能释放它。
比如可以使用 UUID.randomUUID().toString() 生成一个唯一的值。

3.释放锁

在释放锁时,使用DEL删除对应的key即可。

DEL lockKey
(integer) 1

到这里,一个最简单的分布式锁就实现完成了。虽然实现简单,但是这种方式显然存在一些问题。
比如当服务器出现问题,一个线程获取到锁后,执行代码的过程中遇到错误,导致无法进行释放锁的操作。此时其它线程也无法获取锁,就产生了死锁。

二、给锁增加过期时间

针对上面的问题,就需要给锁增加一个过期时间。让锁无论如何最终都会释放。

1.具体操作

SET lockKey value EX 3 NX

lockKey:加锁的锁名;
uniqueValue:能够唯一标识锁的随机字符串;
NX:只有当 lockKey 对应的 key 值不存在的时候才能 SET 成功;
EX:过期时间设置(秒为单位)EX 3 标示这个锁有一个 3 秒的自动过期时间。
与 EX 对应的是 PX(毫秒为单位),这两个都是过期时间设置。

这段命令确保了设置指定 key 的值和过期时间是一个原子操作,如果分开设置,仍然有可能出现死锁情况。

2.操作时间和过期时间不匹配

这个时候,就会引出一个新的问题:
如果设置过期时间过短,操作共享资源的时间要大于过期时间,就无法保证互斥。
但如果设置过期时间过长,操作早已完成,却仍然占用锁,就会影响性能。

有没有什么两全其美的办法?
有,就是如果操作共享资源的操作还未完成,就进行自动续期。

三、看门狗机制

1.机制原理

为了做到自动续期,就需要用到看门狗机制。
这个机制的使用前提是未指定锁超时时间
在这里插入图片描述

在Redisson中提供的renewExpiration()方法包含了看门狗机制的主要逻辑。

private void renewExpiration() {
         //......
        Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
            @Override
            public void run(Timeout timeout) throws Exception {
                //......
                // 异步续期,基于 Lua 脚本
                CompletionStage<Boolean> future = renewExpirationAsync(threadId);
                future.whenComplete((res, e) -> {
                    if (e != null) {
                        // 无法续期
                        log.error("Can't update lock " + getRawName() + " expiration", e);
                        EXPIRATION_RENEWAL_MAP.remove(getEntryName());
                        return;
                    }

                    if (res) {
                        // 递归调用实现续期
                        renewExpiration();
                    } else {
                        // 取消续期
                        cancelExpirationRenewal(null);
                    }
                });
            }
         // 延迟 internalLockLeaseTime/3(默认 10s,也就是 30/3) 再调用
        }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);

        ee.setTimeout(task);
    }

在默认情况下,看门狗会每10秒进行一次续期操作。
首先判断是否需要续期,如果需要,则将锁的过期时间设置为30秒。

2.最终具体实现

看门狗机制的分布式锁可以直接基于Redisson来实现。
在配置好Redisson后,正常进行加锁和释放锁的操作,自动续期操作会由Redisson代为实现。

1.初始化 Redisson 客户端:
从配置文件 redisson.yaml 中加载配置并创建 RedissonClient 实例。
2.获取锁:
使用 RLock 对象的 tryLock 方法尝试获取锁,并指定租约时间。如果获取成功,锁的租约时间会自动续期,默认每 30 秒续期一次,防止任务长时间执行时锁被意外释放。
3.释放锁:
使用 unlock 方法释放锁,确保只有持有锁的线程可以释放锁。

public class DistributedLock {

    private static final RedissonClient redissonClient = RedissonManager.getRedissonClient();

    public boolean acquireLock(String lockKey, long leaseTime, TimeUnit unit) {
        RLock lock = redissonClient.getLock(lockKey);
        try {
            // 尝试加锁,指定租约时间,自动续期时间为 30 秒
            boolean isLocked = lock.tryLock(0, leaseTime, unit);
            if (isLocked) {
                System.out.println("Lock acquired");
            }
            return isLocked;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        }
    }

    public void releaseLock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
            System.out.println("Lock released");
        }
    }

    public static void main(String[] args) {
        DistributedLock distributedLock = new DistributedLock();

        String lockKey = "myLock";
        long leaseTime = 10;
        TimeUnit unit = TimeUnit.SECONDS;

        if (distributedLock.acquireLock(lockKey, leaseTime, unit)) {
            try {
                // 执行业务逻辑
                System.out.println("Executing business logic");

                // 模拟长时间任务
                Thread.sleep(15000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                distributedLock.releaseLock(lockKey);
            }
        }
    }
  • 23
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值