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进行路由判断的信息即可。如果不是人发起的,就根据过程的发起特点进行路由,无论怎样,肯定能找到一种适合一个过程的路由方式。
以上是对分布锁替代方案的思考,虽然我用实践证明了替代方案的可行性,但其中肯定是存在坑的,只是我还没踩到而已。
向伟大的数学家们学习,先猜想,再靠近。