Redisson实战:分布式锁的使用与最佳实践
103
类别: 
开发交流

Redisson实战:分布式锁的使用与最佳实践

Redisson是一个基于Redis的Java客户端,它提供了丰富的分布式对象和服务,包括分布式锁、分布式集合、分布式对象等。在分布式系统中,Redisson的分布式锁功能尤为重要,它解决了传统Redis分布式锁实现中的诸多问题,如死锁、性能和可靠性等。

1. Redisson分布式锁的优势

Redisson的分布式锁相比手动实现的Redis分布式锁有以下优势:

  1. 自动续期机制(看门狗):Redisson会在锁即将过期时自动延长锁的有效期,防止因业务逻辑执行时间过期而释放锁。

  2. 可重入性:Redisson支持可重入锁,同一个线程可以多次获取同一把锁,而不会产生死锁。

  3. 公平锁:Redisson提供公平锁的实现,保证线程按照请求锁的顺序依次获得锁。

  4. 丰富的锁类型:Redisson支持多种锁类型,包括可重入锁、公平锁、联锁、红锁、读写锁等。

  5. 原子性保障:Redisson使用Lua脚本确保锁的获取和释放操作的原子性。

2. Redisson分布式锁在项目中的应用

在我们的项目中,可以看到Redisson分布式锁在多个场景中的应用。

2.1 兑奖服务中的分布式锁应用

RedeemServiceImpl类中,我们使用Redisson分布式锁来确保兑奖操作的并发安全性:

// 使用分布式锁确保兑奖操作的并发安全性
String lockKey = DistributedLockUtils.getRedeemLockKey(drawRecordId);
RLock lock = redisson.getLock(lockKey);

try {
    // 尝试获取锁,等待时间3秒,启用看门狗机制
    boolean lockAcquired = lock.tryLock(LOCK_WAIT_TIME, TimeUnit.SECONDS);
    if (!lockAcquired) {
        log.error("获取分布式锁失败: drawRecordId={}, lockKey={}", drawRecordId, lockKey);
        return ApiResponse.error(ApiResponseErrorCode.DISTRIBUTED_LOCK_FAILED);
    }

    return processRedeem();
} catch (InterruptedException e) {
    log.error("获取分布式锁被中断: drawRecordId={}, lockKey={}", drawRecordId, lockKey, e);
    Thread.currentThread().interrupt();
    return ApiResponse.error(ApiResponseErrorCode.DISTRIBUTED_LOCK_FAILED);
} finally {
    // 释放锁(只有当前线程持有锁时才会释放)
    if (lock.isHeldByCurrentThread()) {
        lock.unlock();
        log.info("释放分布式锁: drawRecordId={}, lockKey={}", drawRecordId, lockKey);
    }
}

这段代码展示了如何使用Redisson分布式锁来保护关键业务逻辑。我们通过tryLock方法尝试获取锁,如果获取成功就执行业务逻辑,否则返回错误。在finally块中确保锁被正确释放。

2.2 定时任务中的分布式锁应用

AsyncTaskScheduler类中,我们使用分布式锁来确保在多实例环境下同一时间只有一个实例执行定时任务:

/**
 * 使用分布式锁执行任务
 * 封装获取锁、执行任务、释放锁的通用逻辑
 *
 * @param taskName 任务名称,用于生成锁键和日志
 * @param task 要执行的任务,使用函数式接口
 */
private void executeWithDistributedLock(String taskName, Runnable task) {
    // 生成分布式锁的键名
    String lockKey = getSchedulerLockKey(taskName);
    log.info("开始处理{}任务,尝试获取分布式锁: lockKey={}", taskName, lockKey);

    // 获取Redisson分布式锁
    RLock lock = redisson.getLock(lockKey);
    
    try {
        // 尝试获取锁,等待时间3秒,启用看门狗机制
        boolean lockAcquired = lock.tryLock(LOCK_WAIT_TIME, TimeUnit.SECONDS);
        
        if (!lockAcquired) {
            log.warn("获取分布式锁失败,其他实例正在执行此任务: lockKey={}", lockKey);
            return;
        }
        
        log.info("成功获取分布式锁,开始执行{}任务: lockKey={}", taskName, lockKey);
        
        try {
            // 执行传入的任务
            task.run();
        } catch (Exception e) {
            log.error("处理{}任务异常:", taskName, e);
        }
        
    } catch (InterruptedException e) {
        log.error("获取分布式锁被中断: lockKey={}", lockKey, e);
        Thread.currentThread().interrupt();
    } finally {
        // 释放锁(只有当前线程持有锁时才会释放)
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
            log.info("释放分布式锁: lockKey={}", lockKey);
        }
    }
}

这段代码展示了如何在定时任务中使用分布式锁,确保在集群部署的环境中只有一个实例执行特定任务。

2.3 使用注解方式的分布式锁

项目中还提供了基于注解的分布式锁实现方式。在DistributedLockAspect类中,通过AOP切面来实现分布式锁:

@Aspect
public class DistributedLockAspect {
    // ... 其他代码

    @Around(value = "distributedLockAspect()")
    public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
        // 得到使用注解的方法。可使用Method.getAnnotation(Class<T>
        // annotationClass)获取指定的注解,然后可获得注解的属性
        Method method = ((MethodSignature) pjp.getSignature()).getMethod();
        Object[] arguments = pjp.getArgs();
        final String lockName = getLockName(method, arguments);
        return lock(pjp, method, lockName);
    }

    public Object lock(ProceedingJoinPoint pjp, Method method, final String lockName)
            throws UnableToAquireLockException, InterruptedException, Exception {

        DistributedLock annotation = method.getAnnotation(DistributedLock.class);

        boolean fairLock = annotation.fairLock();

        boolean tryLock = annotation.tryLock();

        if (tryLock) {
            return tryLock(pjp, annotation, lockName, fairLock);
        } else {
            return lock(pjp, lockName, fairLock);
        }
    }

    public Object lock(ProceedingJoinPoint pjp, final String lockName, boolean fairLock)
            throws UnableToAquireLockException, InterruptedException, Exception {

        return lock.lock(lockName, new AquiredLockWorker<Object>() {
            @Override
            public Object invokeAfterLockAquire() throws Exception {
                return proceed(pjp);
            }

        });
    }
}

通过@DistributedLock注解,我们可以很方便地为方法添加分布式锁保护:

@DistributedLock(lockName = "myLock", waitTime = 10, leaseTime = 30)
public void doSomething() {
    // 业务逻辑
}

3. Redisson分布式锁的使用示例

3.1 基本用法

@Service
public class MyService {
    @Autowired
    private RedissonClient redissonClient;

    public void doSomething() throws InterruptedException {
        String lockKey = "myLock";
        RLock lock = redissonClient.getLock(lockKey);
        
        try {
            // 尝试获取锁,最多等待10秒,如果获取到锁,则持有锁30秒
            boolean isLocked = lock.tryLock(10, 30, TimeUnit.SECONDS);
            if (isLocked) {
                try {
                    System.out.println("获取到锁,开始执行业务逻辑...");
                    // 模拟业务逻辑执行时间
                    Thread.sleep(5000);
                    System.out.println("业务逻辑执行完毕...");
                } finally {
                    lock.unlock();  // 释放锁
                    System.out.println("释放锁...");
                }
            } else {
                System.out.println("获取锁失败...");
            }
        } catch (InterruptedException e) {
            // 处理中断异常
            throw e;
        }
    }
}

3.2 公平锁

@Service
public class FairLockService {
    @Autowired
    private RedissonClient redissonClient;

    public void doSomethingWithFairLock() {
        RLock fairLock = redissonClient.getFairLock("myFairLock");
        try {
            fairLock.lock();
            // 执行需要加锁的业务逻辑
        } finally {
            fairLock.unlock();
        }
    }
}

3.3 可重入锁

@Service
public class ReentrantLockService {
    @Autowired
    private RedissonClient redissonClient;

    public void doSomethingWithReentrantLock() {
        RLock reentrantLock = redissonClient.getLock("myReentrantLock");
        try {
            reentrantLock.lock();
            reentrantLock.lock();  // 同一个线程可以多次获取
            // 执行需要加锁的业务逻辑
        } finally {
            reentrantLock.unlock();
            reentrantLock.unlock();  // 必须相应解锁多次
        }
    }
}

4. 最佳实践与注意事项

4.1 锁的命名规范

锁的key要唯一,比如用"stock:商品ID",这样不同商品的锁不会互相影响,同一商品的所有请求都抢同一把锁。

4.2 锁的释放

使用lock.isHeldByCurrentThread()判断当前线程是否持有锁,避免释放别人的锁(比如线程没拿到锁,却执行了unlock)。

4.3 锁的超时设置

合理设置等待时间和租约时间,防止死锁和资源浪费。

4.4 Redis集群环境下的考虑

如果Redis是主从集群,主节点挂了,从节点还没同步到锁的信息,新的主节点起来后,可能会让其他线程拿到锁。解决办法是使用Redisson的"红锁"(RedLock),多个Redis节点同时加锁,只有多数节点加锁成功,才算整体加锁成功。

5. 总结

Redisson作为基于Redis的Java客户端,提供了丰富的分布式锁实现,解决了传统Redis分布式锁实现中的诸多问题。通过自动续期机制、可重入性、公平锁等特性,Redisson大大简化了分布式锁的使用,提高了系统的可靠性和安全性。

在实际项目中,我们应该根据具体业务场景选择合适的锁类型,并遵循最佳实践,确保分布式锁的正确使用。

标签:
评论 0
/ 1000
0
0
收藏