分布锁替代方案

        synchronized和ReentrantLock等锁机制为Java单体应用提供了在并发情况下正确运行的保障,然应用开发已归治于分布式与微服务久矣,单体时代曾作威作福,号令天下线程的锁机制,在遇到多节点部署后,便只可将其位禅于分布锁。难道Java锁机制在分布环境下就真的一点发挥空间也没有了吗?他是否还有机会从分布锁手中夺回统领群线的权利,或者至少与其分庭抗礼呢?当然有,不过仅凭他自己是做不到的。

        太宗英明神武,若无凌烟之臣,贞观难就。因此,“他”还需一位大将予以辅佐,大将之名不为骠骑,不为车骑,名为路由。

        估计老鸟已经猜到了具体方案,不过,搜了一下关于分布锁的替代方案,庆幸的是,搜索结果全部是分布锁的几种实现方式,可乘之机就在眼前,于是决定先在这片蓝海中扎一猛子。

        分布式环境下,Java锁机制在我们的业务代码中几乎已经变成了摆设。

        在近期几个10万+~40万+行的项目中搜索了一下synchronized关键字,共计找到了二十几个用到了该关键字的方法,当点进去查看具体代码时就会发现,这synchronized用的也是真够牵强。比如下面的代码,问题太多,可优化的更多。先不管其他的,结合该方法的功能来看,synchronized关键字就完全没使用的必要,其最大问题是,无论redis或远程接口超时或正常执行都将阻塞其他线程的执行,而这种同步是完全没有必要的,只是为用而用。

    @Override
    public String getCurrencyName(Long ccyId) {
        String name = null;
        if (ccyId != null && ccyId != 0) {
            String redisKey = RedisConstants.CURRENCY_PREFIX + ccyId;
            if (redisUtil.exists(redisKey)) {
                name = redisUtil.get(redisKey).toString();
            } else {
                synchronized (this) {
                    if (redisUtil.exists(redisKey)) {
                        name = redisUtil.get(redisKey).toString();
                    } else {
                        name = currencyRemoteService.getCurrencyName(ccyId);
                        if (StringUtils.isNotBlank(name)) {
                            redisUtil.set(redisKey, name);
                        }
                    }
                }
            }
        }
        return name;
    }

        而ReentrantLock更惨,没有一个项目用到,concurrent包下唯一用到的类是CountDownLatch,其作用是让主线程等待线程池中的线程执行完毕,不过这种功能用ExecutorCompletionService实现更适合。

        当然,要承认的是,并发包下的类和线程同步机制的使用是和项目的特点息息相关的,只有用到合适地方才能发挥其效用,就像设计模式虽好,但不经思考的乱用反而适得其反。

        列举上述锁机制在实际业务代码中的使用情况,只是觉得有着更大发挥空间的锁机制不应只是扮演这些酱油角色,如果仅仅是现在这样,怎么对得起偏向锁和轻量锁这样的优化;怎么对得起ReentrantLock提供的中断、超时、公平锁这些比synchronized更细粒度的功能,就连面试时问的那些关于锁的问题都对不起。既然这么多对不起,那就来看看有什么办法能将它们利用起来,让这些被埋没的牛X角色重新参与到业务的实现中来。

        分布锁的目的是锁住一个过程,而不是像for update仅仅锁住一条记录。过程是一个持续的连贯的一系列操作,在这个被锁住的过程中要互斥地访问并操作一些资源。

        说到这,我觉得有必要对所操作的资源分下类,分类的依据是看这个资源是否提供了排他操作的能力,比如关系型数据库就具备排他操作的能力,而消息中间件则没有这种能力。因此可以分为以下几种情况:

1.在过程中只操作一个具备排他能力的资源时是不需分布锁的;

2.在过程中操作多个具备排他能力的资源时可以给每个资源加锁,但从复杂度来看,显然是用一个锁控制住所有资源是最简单的;

3.在过程中操作不具备排他能力的资源时,就算这样的资源只有一个也必须用分布锁加以控制;

4.在过程中操作的资源包括上述两种资源时,仍需分布锁辅助。

如果用图画出上述4种情况就是象限,可以简单地概括分布锁适合使用的场景。

        想象这样一个场景,T1表的status字段是指定状态时才可更新为另一状态,更新成功则向T2表插入一条记录(要求是这条记录不应被其他过程看到),然后根据更新和插入结果决定是否发送一条消息(要求是这条消息绝对不能重复发送),接着再接收一条消息,所接收的这条消息是之前发送那条消息的响应,根据响应消息的内容判断T3表status字段是否为指定状态并将其更新。设定这3个表在同一个DB实例中,也就是说先不涉及分布事务。

        上述这个过程如果在多个节点并发执行的话,显然需要一个全局锁控制整个执行过程,否则无法得到正确结果。那么如果不用分布锁,而仅用Java的锁机制是否同样可以锁定整个执行过程呢?从实践的结果来看是可以的。

        通常情况下这样的过程都是由一个用户发起的,那么将这个用户的所有操作都路由到同一个节点上执行不就可以用Java的锁机制代替分布锁来控制整个过程的执行了吗。当然,并不是所有类似的过程都是由一个用户发起的,也可能是由满足一定条件后由系统自动触发的,那就要看这个过程的执行特点,理论上来说,不管这个过程是谁发起的,是怎么发起的,都可以遵循一个维度将其路由给指定的节点进行处理,这样就只需在一个JVM范围内锁住相关资源,从而也就不用再依赖分布锁,让Javaer回到单体应用的田园时代。

        以redis为例,从实现的复杂度来看,用redis实现分布锁有几点需要注意,1.锁超时,2.操作原子性,3.误删其他节点的锁,这些都必须在操作redis的同时在代码中对上述情况进行处理,不出意外的话还要引入lua,因此想写出一个比较健壮的分布锁并非易事,而绝不仅仅是setnx+del那么简单。相对来说用路由+Java锁机制来控制一个过程的执行会简单不少。如果过程的执行是一个用户发起的,则可通过在http的header中添加供gateway进行路由判断的信息即可。如果不是人发起的,就根据过程的发起特点进行路由,无论怎样,肯定能找到一种适合一个过程的路由方式。

        以上是对分布锁替代方案的思考,虽然我用实践证明了替代方案的可行性,但其中肯定是存在坑的,只是我还没踩到而已。

        向伟大的数学家们学习,先猜想,再靠近。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值