SpringAOP分布式锁(可以适配redis,zk等多种中间件实现)

19 篇文章 5 订阅
1 篇文章 0 订阅

关于分布式锁,我也在网上找了很多文章,但始终没有找到一个我想要的解决方案,于是参考前辈的思想和加入自己的想法手写了一个。

什么是分布式锁:

就是在多个服务端都能访问到的中间件上打上一个标记。例如:redis, zookeeper, mysql

本文是基于redissetnx 实现分布式锁,但是我写的分布式锁解决方案,还可以适配其他中间件实现。这个才是重点。


先说下分布式锁需要解决的问题:

  • 有一个线程获取到了锁,其他线程是阻塞等待,还是直接以失败响应。
  • 获取锁失败,是否需要重试一会。
  • 锁必需要具备有可重入性质。
  • 服务端获取锁后,业务异常或宕机了,没有释放锁,导致死锁如何处理。
  • 获取锁了,但是业务执行过长,如何处理。(锁续命
  • 锁失效后(redis key过期),业务还没有执行完,如何处理。
  • A服务端释放了锁,B服务端阻塞等待获取锁的线程,如何唤醒。
  • 线程A获取的锁,线程B不能释放。
  • 阻塞等待被唤醒获取锁的线程,超时如何处理。
  • 锁是公平还是不公平的。
  • AOP 执行获取锁处理,异常后事务怎么处理。
  • AOP 执行获取锁处理,获取失败后,如何返回错误信息,如何通知给开发者。
  • AOP 执行获取锁处理,它本身是否线程安全,这点非常重要

直接上效果图看看:
goods 表的 goodsId = 1 有 100个库存
在这里插入图片描述
业务操作数据库的代码
在这里插入图片描述
并发200测试

不应用分布式锁效果:库存直接 负100 了。
在这里插入图片描述
应用分布式锁效果:库存始终不会超出限制。
在这里插入图片描述


进入正文,开始分享我设计的分布式锁的思路。自我感觉还是写得蛮好的。😂😂😂

应用的技术点:必须依赖Spring

设计原则:

  • 依赖倒转(倒置)原则
  • 开闭原则
  • 迪米特法则
  • 合成复用原则

设计模式:

  • 单列模式
  • 外观模式
  • 工厂模式
  • 模板方法模式
  • 桥接模式

围绕以下设计,展开程序代码的编写

在这里插入图片描述
获取锁成功处理:

  1. 获取锁成功,需要开启一个续命线程,重置锁的过期时间。
  2. 当前线程是可重入的,重入次数加一。
  3. 如果没有应用Aop事务,则手动开启事务。
  4. 业务执行完毕,释放锁,重入次数减一,如果重入次数等于0,则删除redis key,并唤醒阻塞等待的线程。
  5. 如果没有应用Aop事务,则手动提交事务。

获取锁不成功处理:

  1. 进行重试获取锁,有次数限制,且每隔多少时间重试一次。
  2. 重试获取锁失败,如果需要阻塞等待,则阻塞当前线程,并加入到阻塞队列集合中,等待被唤醒。
  3. 开启一个定时任务 ,清理阻塞队列中,获取锁超时的线程。

被唤醒去获取锁的线程处理:

  1. 检查当前唤醒的线程,是否有超时。

获取锁失败或异常处理:

  1. 调用获取锁失败的回调接口方法。
  2. 如果获取锁失败的回调方法,返回了自定义的异常,则以该异常抛出。
  3. 否则以获取锁的异常抛出。

Redis分布式锁处理:

  1. 获取锁之前,订阅redis key删除事件监听。
  2. 获取锁成功,订阅redis key过期事件监听。

重点说明:

Aop分布式事务失败或者业务执行异常全部以异常抛出,所以需要全局捕获异常处理。


为什么要使用AOP来实现分布式锁

既然是获取锁,才能执行业务逻辑。那就不应该让获取锁流程,参与到主业务中,强力实现解耦。
也使得代码阅读起来,通俗易懂。而且将获取锁,释放锁全部交给Aop。如果在主业务中获取锁,那还需要去关心他的释放锁。

@DistributedLock 使用分布式锁注解 演示:

/* 注解全部属性 */
@DistributedLock(lockKey = "goods::goodsId-${goodsId}",         // 锁定的 key 支持表达式在方法参数中取值             必须指定
            trySleep = 60,                                          // 重试获取锁的间隔                              这是默认值
            tryRestrict = 5,                                        // 重试获取锁的次数限制                           这是默认值
            resetRestrict = 3,                                      // 续命次数限制                                  这是默认值
            expire = 3000,                                          // 锁的失效时间 单位毫秒                           这是默认值
            transaction = true,                                     // 是否需要以事务执行                              这是默认值
            lockFailHandler = GoodsLockFailHandler.class,           // 获取锁失败的回调, 该类型必须要注入到Spring容器中    默认无(没有回调)
            lockProcessed = RedisLockProcessedImpl.class,           // 锁的解决方案, 必须是一个由Spring容器创建的类型      这是默认值
            await = @DistributedLock.Await(                         // 获取锁的阻塞等待策略
                    isAwait = false,                                // 是否需要阻塞, 获取锁失败后阻塞, 等待被唤醒.         这是默认值
                    timeout = 10000,                                // 等待超时时间 单位毫秒 从线程等待开始计算            这是默认值
                    fair = false,                                   // 等待中的线程 唤醒是否公平                        这是默认值
                    awaitRestrict = Integer.MAX_VALUE               // 阻塞等待获取锁的队列 大小限制                     这是默认值
            ),
            filter = @DistributedLock.Filter(                       // 获取锁之前的过滤
                    GoodsLockFilter.class                           // 该类型必须要注入到Spring容器中                   默认无(过滤)
            )
    )

	/**
     * 实际只需要以下属性即可
     * 甚至获取锁失败的回调 lockFailHandler 和 获取锁之前的过滤 filter 都可以不指定
     * 因为其他属性都有默认值
     */
    @DistributedLock(lockKey = "goods::goodsId-${goodsId}",         // 锁定的 key 支持表达式在方法参数中取值             必须指定
            lockFailHandler = GoodsLockFailHandler.class,           // 获取锁失败的回调, 该类型必须要注入到Spring容器中    默认无(没有回调)
            filter = @DistributedLock.Filter(                       // 获取锁之前的过滤
                    GoodsLockFilter.class                           // 该类型必须要注入到Spring容器中                   默认无(过滤)
            )
    )

使用案例演示:

1:必须全局捕获 LockException 异常,给前端响应信息。否则响应的是一堆错误。

@ControllerAdvice
@ResponseBody
public class ExceptionController {
    /**
     * 分布式锁异常 全局捕获
     *
     * @param e
     * @return
     */
    @ExceptionHandler(LockException.class)
    public Result lockException(LockException e) {
        return new Result(-1, e.getMessage(), null);
    }
}

2:必须先启用分布式锁@EnableDistributedLock

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import({LockRegister.class, LockImportRegister.class})
public @interface EnableDistributedLock {

    /**
     * 指定 分布式锁 的处理器
     */
    Class<? extends LockProcessed> defLockProcessed() default LockProcessed.class;
}

我这里用的是 Redis 实现分布式锁,所以直接使用@EnableRedisLock这个注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
/* 启用分布式锁 */
@EnableDistributedLock(defLockProcessed = RedisLockProcessedImpl.class)
@Import({RedisLockRegister.class})
public @interface EnableRedisLock {
}


Demo01:

指定一个获取锁之前的过滤器,该过滤器需要注入到Spring容器中

	// lockKey 支持表达式  goodsId = 1 那么 lockKey = goods::goodsId-1
    @DistributedLock(lockKey = "goods::goodsId-${goodsId}",
            // 指定获取锁之前的过滤
            filter = @DistributedLock.Filter(GoodsLockFilter.class)
    )
    public boolean buy(Integer goodsId) {
        return true;
    }

GoodsLockFilter

@Component
public class GoodsLockFilter implements LockFilter {
    /**
     * 过期锁之前的过滤  是否予许获取锁
     * @param methodParams 方法参数
     */
    @Override
    public boolean allow(Map<String, Object> methodParams) {
        /* 不予许获取锁 false */
        return false;
    }
    /**
     * 不予许获取锁的提示信息
     * @param methodParams 方法参数
     * @return
     */
    @Override
    public String errorMessage(Map<String, Object> methodParams) {
        return "没有库存了";
    }
}

Postman 测试结果 http://localhost:8080/buy?goodsId=1
在这里插入图片描述


Demo02:

全局捕获异常添加一个自定义的异常

@ControllerAdvice
@ResponseBody
public class ExceptionController {
    /**
     * 自定义异常捕获
     * @param e
     * @return
     */
    @ExceptionHandler(MyException.class)
    public Result myException(MyException e) {
        return new Result(-1, e.getMessage(), null);
    }
}

分布式锁指定一个获取锁失败的回调

// lockKey 支持表达式  goodsId = 1 那么 lockKey = goods::goodsId-1
    @DistributedLock(lockKey = "goods::goodsId-${goodsId}",
            /* 指定一个获取锁失败的回调 */
            lockFailHandler = GoodsLockFailHandler.class
    )
    public boolean buy(Integer goodsId, Integer userId) {
        /* 故意抛出异常 */
        int i = 1 / 0;
        return true;
    }

GoodsLockFailHandler

@Component
public class GoodsLockFailHandler implements LockFailHandler {
    @Override
    public LockFailResult handle(LockFailInfo lockFailInfo) throws Exception {
        System.out.println("获取锁失败了 " + lockFailInfo);
        return new LockFailResult(new MyException("获取锁失败了, 抛出自定义的异常 " + lockFailInfo));
        /* 如果不想抛出自定义的异常, 返回 null 即可 */
        //return LockFailResult.RESULT_EMPTY;
    }
}

Postman 测试结果 http://localhost:8080/buy?goodsId=1
在这里插入图片描述

总结:

  • 如果想在获取锁之前,检查一些数据,可以指定一个过滤器。
  • 如果获取锁失败了(结束了),想记录一些信息,比如日志,可以指定一个回调。
  • 如果想要让获取锁不成功的线程,阻塞等待,可以开启 阻塞策略。

过滤器和失败回调器 都是从Spring中获取,所以大可放心的应用Spring的功能。
此分布式锁功能面还是蛮多的,需要源码的可以私信我,或扫下面的码加我微信。


项目结构:
在这里插入图片描述
1:common.support.spring.aop Aop基础。
2:common.support.spring.lock 分布式锁支持。
3:common.support.spring.redislock Redis分布式锁实现。

以上目录结构划分,是为了区分各个模板,实现解耦,在项目移值的时候便捷。哪怕复制粘贴,都可以不用改代码,直接使用。
还有就是要应用到我们现成的开发框架中。此项目存在公共模块中。

分布式锁 项目结构:
在这里插入图片描述

1:annotation 注解相关。

  • DistributedLock 使用分布式锁 的注解。
  • DistributedLockAspect 分布式锁的Aop切面执行器。
  • EnableDistributedLock 启用分布式锁的注解。
  • LockData DistributedLock注解的属性包装。
  • LockImportRegister 注册相关的类至Spring。
  • LockRegister 注册相关的类至Spring。

2:exception 异常。分布式锁获取锁失败都是以异常抛出。

  • LockAwaitRestrictException 阻塞队列超过限制异常。
  • LockException 分布式锁的顶级异常,所有异常继承自他。
  • LockFailException 获取锁失败异常。
  • LockFilterException 在获取锁, 过滤时, 不予许获取锁的异常。
  • LockProcessedException 解析/包装/注解信息异常。
  • LockReentryRestrictException 重入次数达到限制异常。
  • LockTimeoutException 获取锁超时异常。
  • UnlockException 释放锁异常。

3:factory 生产对象实例工厂。

  • LockFailHandlerFactory生产获取锁失败回调实例。
  • LockFilterFactory 生产获取锁之前过滤实例。
  • LockProcessedFactory 生产分布式锁处理器实例。

4:info 描述信息

  • AwaitInfo 阻塞等待被唤醒获取锁的信息。
  • LockFailInfo 获取锁失败的回调描述信息。
  • LockFailResult 获取锁失败的回调的返回信息。
  • LockInfo 获取锁的信息。
  • LockResult 获取锁/释放锁的结果。

5:info 跟目录

  • AbstractDistributedLockProcessed 分布式锁的处理器,已经提供好了算法骨架。
  • LockFailHandler 获取锁失败的回调接口。
  • LockFilter 获取锁之前的回调接口。
  • LockProcessed 分布式锁处理接口。
  • TransactionStatusWrapper 事务包装类。

总结:

如果想要更换 分布式锁的 实现, 只需要继承该AbstractDistributedLockProcessed抽象类,重写他的抽象方法,对获取锁/释放锁的细节完善即可,
然后在启用分布式锁@EnableRedisLock的属性defLockProcessed标记上。
或者在使用分布式锁注解DistributedLock属性lockProcessed标记也可以
符合开闭原则,完全不需要更改原有代码逻辑,拓展非常容易。


Redis实现分布式锁 项目结构:

在这里插入图片描述
1:annotation 注解相关。

  • EnableRedisLock 启用Redis分布式锁。
  • RedisLockRegister 向Spring容器注册相关类。

2:handle redis key的监听

  • AbstractKeyspaceEventMessageListener key 监听的抽象类。
  • DeleteKeyspaceEventMessageListener key删除监听
  • ExpiredKeyspaceEventMessageListener key 过期监听
  • KeyListenerHandler key 监听回调接口

3:redislock 跟目录

  • AbstractRedisDistributedLockProcessed Redsi 分布式锁的抽象类。
  • RedisLockProcessedImpl Redsi 分布式锁的实现。

总结:

Redis实现分布式锁非常简单,只需要实现获取锁/释放锁/续命锁的细节,因为其他的业务逻辑在父类中AbstractDistributedLockProcessed 已经完善好了。

RedisLockProcessedImpl源码

public class RedisLockProcessedImpl extends AbstractRedisDistributedLockProcessed {
    @Autowired
    protected StringRedisTemplate stringRedisTemplate;
    public RedisLockProcessedImpl(LockData lockData) throws LockProcessedException {
        super(lockData);
    }
    // 获取锁
    @Override
    protected boolean doLock() {
        return stringRedisTemplate.opsForValue()
                .setIfAbsent(getLockKey(), getLockValue(), getExpire(), TimeUnit.MILLISECONDS);
    }
    // 续命锁
    @Override
    protected void doReset() {
        stringRedisTemplate.expire(getLockKey(), getExpire(), TimeUnit.MILLISECONDS);
    }
    // 释放锁
    @Override
    protected boolean doUnlock() {
        return stringRedisTemplate.delete(getLockKey());
    }
}

AbstractRedisDistributedLockProcessed源码

public abstract class AbstractRedisDistributedLockProcessed extends AbstractDistributedLockProcessed {
    public static final String LOCK_KEY_PREFIX = "DISTRIBUTED-LOCK::";
    @Autowired
    private DeleteKeyspaceEventMessageListener delKeyListener;
    @Autowired
    private ExpiredKeyspaceEventMessageListener expiredKeyListener;
    public AbstractRedisDistributedLockProcessed(LockData lockData) throws LockProcessedException {
        super(lockData);
    }
    @Override
    public String getLockKey() {
        String lockKey = super.getLockKey();
        return lockKey == null ? "" : LOCK_KEY_PREFIX + lockKey;
    }
    @Override
    protected void prepareLock() {
        /**
         * 监听删除key的监听
         * 从而可以让其他<code>服务端</code>监听到 锁定的key 被删除了,也就是(锁释放了),可以作唤醒等待获取线程操作
         */
        if (!delKeyListener.exist(getLockKey())) {
            delKeyListener.register(getLockKey(), key -> {
                try {
                    /* 唤醒阻塞线程 */
                    wakeup();
                } catch (Exception e) {
                }
            });
        }
    }

    /**
     * 获取锁成功的回调
     */
    @Override
    protected void doSuccess() {
        /**
         * 监听 锁定key 失效事件
         */
        if (!expiredKeyListener.exist(getLockKey())) {
            expiredKeyListener.register(getLockKey(), key -> {
                try {
                    /* redis 锁的key过期了 但是服务端没有释放锁 */
                    if (isLock()) {
                        /* 以异常业务结束当前还获取锁的线程 */
                        unusualService();
                    }
                    /* 唤醒等待的线程 */
                    wakeup();
                } catch (Exception e) {
                }
            });
        }
    }
}
觉得对您有帮助,就点个赞呗。😀

有需要源码的可以扫码加找我拿,或者私聊我。
在这里插入图片描述

  • 5
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Me_Liu_Q

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值