为什么要用分布式锁?分布式锁的实现?

分布式锁介绍

为什么要用锁?为什么要用分布式锁?
首先带着标题的两个问题,为什么要用锁?为什么要用分布式锁?

第一:为什么要用锁,很简单就一句话保证数据的安全性。在单机系统中,如果有多个线程同时操作同一个资源,就会出现数据安全问题。在Java中AtomicInteger、AtomicBoolean等java.util.concurrent包下的类能够保证单机系统的数据安全,同时还可以配合synchronized,lock使用。

第二:为什么要用分布式锁,因为现在大部分互联网系统都采用分布式部署,提升系统总体性能。在分布式系统中,一个方法在同一时间只能被一个机器的一个线程运行,在多个机器(也可以同一个机器起两个应用)中java.util.concurrent包和synchronized,lock就不能保证数据的安全性了,这时候就需要引入分布式锁。当然单机系统也可以使用分布式锁,但没有必要,会增加系统复杂度和降低系统可用性(中间件万一挂了-_-!)。

例如,12306购票系统,在面对高并发的情况下,首先采用限流,控制系统并发处理量,其次一张票只能出售给一位顾客,假如有n位顾客同时对同一张表下单,只会有一位顾客购买成功,这时候就需要通过锁来保证票只被第一位提交订单的客户购买。

分布式锁原理

简单画了一张图,服务A、B、C同时申请对资源A的使用权限,服务A申请成功,服务A可以使用资源A,服务B、C则等待A使用完或拒绝本次请求。

分布式锁实现思路

Redis实现分布式锁
通过资源的唯一id,生成一个key,缓存在redis中,在使用完资源后,移除该key,每次使用该资源前判断资源的key在redis中是否存在。
以上存在一个弊端,假如程序卡死或宕机,就不能释放锁了。这时候可以对key设置一个生效时间,需要评估好程序执行时间。
这时候还有一个弊端,假如时间设置过小了,有可能导致锁提前释放,也会引起数据安全性问题。这时候我们可以设置一个watchdog线程,在程序执行时对资源动态调整key生效时间。
这时候同样会有问题,程序卡死会无限续时的问题,这时候就需要根据具体业务去分析如何进行取舍。

Zookeeper实现分布式锁
zookeeper简称zk,zk是通过生成临时有序节点来实现分布式锁的,首先会在/lock目录下一个临时有序节点,后续请求会在节点后面继续创建临时节点。新的子节点后面,会添加一个次序编号,这个生成的编号,会在上一次的编号进行 +1 操作。

zk节点监听机制:每个线程抢占锁之前,先尝试创建自己的ZNode。同样,释放锁的时候,就需要删除创建的Znode。创建成功后,如果不是排号最小的节点,就处于等待通知的状态。等谁的通知呢?不需要其他人,只需要等前一个Znode的通知就可以了。前一个Znode删除的时候,会触发Znode事件,当前节点能监听到删除事件,就是轮到了自己占有锁的时候。第一个通知第二个、第二个通知第三个依次向后。

zk临时节点自动删除:当我们客户端断开连接之后,我们出创建的临时节点会进行自动删除操作,所以我们在使用分布式锁的时候,一般都是会去创建临时节点,这样可以避免因为网络异常等原因,造成的死锁。

分布式锁案例
本次案例使用springboot + redisson实现

/**
 * 锁类型
 *
 * @Author h-bingo
 * @Date 2023-04-25 10:20
 * @Version 1.0
 */
public enum LockType implements DescEnum {
    /**
     * 有一个线程加锁,其他线程直接结束
     * <p>
     * 此时需注意评估好程序执行时间,若锁持有时间小于程序执行时间,则会提前释放锁,可能引起锁失效问题
     */
    MUTEX("互斥锁"),
    /**
     * 有一个线程加锁,则等待一定时间
     * <p>
     * 此时需注意评估好程序执行时间,若锁持有时间小于程序执行时间,则会提前释放锁,可能引起锁失效问题
     */
    SYNC("同步锁"),
    /**
     * 有一个线程加锁,其他线程直接结束
     * <p>
     * 无需设置锁持有时间,需注意评估好程序不会一直卡死的问题,否则会出现锁无法释放,慎用
     */
    AUTO_RENEWAL_MUTEX("互斥锁(自动续期)"),
    /**
     * 有一个线程加锁,则等待一定时间
     * <p>
     * 无需设置锁持有时间,需注意评估好程序不会一直卡死的问题,否则会出现锁无法释放,慎用
     */
    AUTO_RENEWAL_SYNC("同步锁(自动续期)"),
    ;

    private String desc;

    LockType(String desc) {
        this.desc = desc;
    }

    @Override
    public String getDesc() {
        return desc;
    }
}

 

/**
 * RedisLockAspect 实现
 *
 * @Author h-bingo
 * @Date 2023-04-24 17:37
 * @Version 1.0
 */
@Slf4j
@Aspect
@ConditionalOnMissingBean(RedisLockAspect.class)
public class RedisLockAspect implements InitializingBean {

    private static final String REDIS_KEY_PREFIX = "redisLock:";

    @Autowired
    private RedissonClient redissonClient;

    @Pointcut("@annotation(com.bingo.study.common.component.lock.annotation.RedisLock)")
    public void redisLock() {
    }

    @Around(value = "redisLock()&&@annotation(lock)")
    public Object doAround(ProceedingJoinPoint joinPoint, RedisLock lock) throws Throwable {
        Object lockId = checkParam(joinPoint, lock);
        String lockKey = getLockKey(joinPoint, lockId, lock);

        if (lock.lockType() == LockType.MUTEX) {
            return doLock(joinPoint, 0, lock.leaseTime(), lockKey, joinPoint::proceed);
        } else if (lock.lockType() == LockType.AUTO_RENEWAL_MUTEX) {
            return doLock(joinPoint, 0, -1, lockKey, joinPoint::proceed);
        } else if (lock.lockType() == LockType.SYNC) {
            return doLock(joinPoint, lock.waitTime(), lock.leaseTime(), lockKey, joinPoint::proceed);
        } else if (lock.lockType() == LockType.AUTO_RENEWAL_SYNC) {
            return doLock(joinPoint, lock.waitTime(), -1, lockKey, joinPoint::proceed);
        }
        String methodName = AspectUtil.getMethodIntactName(joinPoint);
        log.warn("RedisLock锁类型异常[{}]", methodName);
        throw new RedisLockException(String.format("RedisLock锁类型异常[%s]", methodName));
    }

    private Object doLock(ProceedingJoinPoint joinPoint, long waitTime, long leaseTime, String lockKey,
            RedisLockCallBack callBack) throws Throwable {
        log.info("RedisLock Key: {}", lockKey);

        RLock rLock = redissonClient.getLock(lockKey);
        try {
            boolean tryLock = rLock.tryLock(waitTime, leaseTime, TimeUnit.MILLISECONDS);
            if (tryLock) {
                // 执行方法
                return callBack.doWork();
            } else {
                String methodName = AspectUtil.getMethodIntactName(joinPoint);
                log.info("RedisLock获取锁失败[{}]", methodName);
                throw new RedisLockException(String.format("RedisLock获取锁失败[%s]", methodName));
            }
        } finally {
            unLock(rLock);
        }
    }

    private void unLock(RLock rLock) {
        if (rLock.isLocked() && rLock.isHeldByCurrentThread()) {
            rLock.unlock();
        }
    }

    /**
     * 若hasId为true,必须要有一个唯一的参数作为加锁key的一部分,并且这个参数要用 {@link LockId} 注解标识
     *
     * @Param [joinPoint, lock]
     * @Return void
     * @Date 2023-04-25 11:02
     */
    private Object checkParam(ProceedingJoinPoint joinPoint, RedisLock lock) {
        if (lock.hasId()) {
            Object[] args = joinPoint.getArgs();
            MethodSignature ms = (MethodSignature) joinPoint.getSignature();
            Method method = ms.getMethod();
            Parameter[] parameters = method.getParameters();
            if (parameters != null && parameters.length > 0) {
                for (int i = 0; i < parameters.length; i++) {
                    LockId annotation = parameters[i].getAnnotation(LockId.class);
                    if (annotation != null) {
                        return args[i];
                    }
                }
            }
            String methodName = AspectUtil.getMethodIntactName(joinPoint);
            log.error("缺少 @LockId 标识的唯一参数,method = {},args = {}", methodName, Arrays.toString(args));
            throw new RedisLockException("缺少 @LockId 标识的唯一参数");
        }
        return null;
    }

    /**
     * 如果 {@link RedisLock#hasId()} 为false
     * key 组成 applicationName + {@link RedisLockAspect#REDIS_KEY_PREFIX} + {@link RedisLock#key()}
     * <p>
     * 如果 {@link RedisLock#hasId()} 为true
     * key 组成 applicationName + {@link RedisLockAspect#REDIS_KEY_PREFIX} + {@link RedisLock#key()} + 方法 @LockId 参数
     * <p>
     * {@link RedisLock#key()} 为空则用方法名取代
     *
     * @Param [joinPoint, lockId, lock]
     * @Return java.lang.String
     * @Date 2023-04-25 10:54
     */
    private String getLockKey(ProceedingJoinPoint joinPoint, Object lockId, RedisLock lock) throws NoSuchMethodException {
        StringBuilder key = new StringBuilder(REDIS_KEY_PREFIX);

        if (StringUtils.isBlank(lock.key())) {
            key.append(AspectUtil.getMethodIntactName(joinPoint));
        } else {
            key.append(lock.key());
        }

        if (lock.hasId()) {
            key.append(":").append(lockId);
        }

        return RedisKeyUtil.getCacheKey(key.toString(), false, true);
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        log.info("Redis分布式锁功能已开启,请在加锁方法添加: @RedisLock");
    }
}

/**
 * 标记参数为 lockKey 的一部分
 *
 * @Author h-bingo
 * @Date 2023-06-08 14:40
 * @Version 1.0
 */
@Documented
@Retention(value = RetentionPolicy.RUNTIME)
@Target(value = {ElementType.PARAMETER})
public @interface LockId {
}
/**
 * 开启 {@link RedisLock} 注解功能
 *
 * @Author h-bingo
 * @Date 2023-04-24 17:39
 * @Version 1.0
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({RedisLockAspect.class})
public @interface EnableRedisLock {
}

测试结果

接口如下:这里设置睡眠 3 秒

可以看到测试日志,有 4 个获取锁失败,只有一个成功执行

 

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值