背景
最近隔壁业务组新上了一个全新的系统,主要是提供了一批dubbo接口给我们组调用,预计qps大概在40k左右,由于是一个比较轻的微服务,压测阶段的P98大概在7ms左右,我们dubbo的超时时间设置的100ms,已经给足了阈值。但是在灰度验证阶段,发现大量的请求出现超时的情况,和压测结果完全不符,一顿排查和交流下来发现他们居然没有做预热。
最后他们又重新上了一版带预热的代码,刚上线时的超时问题随之解决。事后我大概cr了一下他们的预热代码,大概方案就是通过配置中心配置需要预热的dubbo接口和对应请求参数和一些其他的预热参数,他们通过线程池起了几个线程(线程数=预热接口数),各自的线程通过从配置中心获取的配置开始调用服务。在机器刚起来时发布系统会自动调用预热接口(如果存在),当预热效果达到设定结果时则停止预热,系统开始正式上线接受线上请求。
ConcurrentHashMap的使用还有必要加锁吗?
cr代码时发现了他们的以下代码:
private static final Map<String, GenericService> SERVICE_MAP = Maps.newConcurrentMap();
public static GenericService getGenericService(final DubboCaseInfo info) {
final String key = info.getService() + "_" + info.getMethod() + "_" + info.getTypes().length;
GenericService genericService = SERVICE_MAP.get(key);
if (genericService == null) {
synchronized (ReferenceRegister.class) {
genericService = SERVICE_MAP.get(key);
if (genericService == null) {
genericService = createGenericService(info);
SERVICE_MAP.put(key, genericService);
}
}
}
return genericService;
}
上面这段代码主要是根据配置中心得到的配置初始化一个 GenericService 对象,后续再通过反射调用对应的dubbo服务。
GenericService
是 Apache Dubbo 中提供的一个接口,用于进行泛化调用(Generic Invocation)。泛化调用是一种不依赖具体接口类的调用方式,主要用于服务测试、网关开发等场景。通过GenericService
,客户端可以动态地调用服务提供者的任意方法,而无需在编译期依赖具体的服务接口。
可以看到使用了ConcurrentMap用来存储对应的key及dubbo服务,但是这里不仅使用了ConcurrentMap,还使用了一个双重校验锁来保证多线程下不会创建多个genericService 示例。看到这里其实心里是有一个疑问的:ConcurrentHashMap的使用还有必要加锁吗?因为ConcurrentHashMap
可以保证线程安全,因为它内部通过分段锁与CAS已经实现了高效的并发控制。
随后看了一些资料与博文,感觉结论还是仁者见仁智者见智,个人还是偏向于不用再使用双重校验锁,原因在于这只是一个简单的预热场景,使用双重检查锁定会增加代码复杂度。在某些情况下,简单的 putIfAbsent
操作就可以满足需求,同时保持代码简洁。
public static GenericService getGenericService(final DubboCaseInfo info) {
final String key = info.getService() + "_" + info.getMethod() + "_" + info.getTypes().length;
return SERVICE_MAP.computeIfAbsent(key, k -> createGenericService(info));
}
通过阅读源码也可以发现ConcurrentHashMap的putIfAbsent
内部实现使用了synchronized锁和CAS自旋来保证线程安全,相比只想爱HashMap的putIfAbsent方法则是不能保证线程安全的。