synchronized双重检查+本地分段锁提高查询效率的方案
背景
有这样的场景,业务系统接入了几百个下游服务,这些下游服务会根据自己的服务id频繁请求一些信息。一开始的方案是redis缓存,每次从redis去获取,但是实际生产环境发现redis也扛不住会出现获取连接池失败的错误。就想到二级缓存,本级+redis实现,但是这个有个问题是本地没有而redis有,需要将redis的值更新到本地缓存,如果不加锁限制会出现多个线程反复设置本地缓存也就是线程不安全,如果直接加载方法上,那么对于不同自服务全都堵塞了,最终想出来下面的这个synchronized双重检查+本地分段锁提高查询效率的方案。
代码
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class DoubleCheckTest {
// 模拟一级缓存
public static ConcurrentHashMap<String, String> localCacheMap = new ConcurrentHashMap<>();
// 模拟二级缓存
public static ConcurrentHashMap<String, String> redisCacheMap = new ConcurrentHashMap<>();
// 本地锁
public static ConcurrentHashMap<String, Object> lockMap = new ConcurrentHashMap<>();
static {
redisCacheMap.put("1", "a");
}
public static void main(String[] args) {
// 模拟并发场景下,下游服务同一时间多个请求获取,预期结果是只有一次设置值,其他都是查询
ExecutorService threadPool = Executors.newFixedThreadPool(10);
CountDownLatch downLatch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
String serverId = "1";
threadPool.execute(() -> {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
System.out.println(getServerValue(serverId));
} catch (Exception e) {
e.printStackTrace();
} finally {
downLatch.countDown();
}
});
}
try {
downLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
threadPool.shutdown();
}
/**
* 当多个下游服务同i时间通过各自唯一的服务id请求服务端资源时
* 1.采用二级缓存提高查询效率
* 2.如果一级缓存没有就查询二级缓存,并将值更新到一级缓存
* 3.对于步骤2的优化,
* 参考分段锁的思想只锁住每个服务Id在缓存数据的部分,保证同个服务在同一时间的多个请求只有一次更新并且相互隔离
* 考虑到如果缓存中一直都没有,那么会频繁查询二级缓存,所有如果二级缓存没有就设置默认值null字符串来避免后续查询
* @param serverId 下游服务的唯一标识id
* @return String 值
*/
private static String getServerValue(String serverId) {
// 尝试创建锁
lockMap.putIfAbsent(serverId, new Object());
// 查询一级缓存
String serverValue = localCacheMap.get(serverId);
// 一级缓存没有获取到,查询二级缓存并更新一级缓存
if (serverValue == null) {
synchronized (lockMap.get(serverId)) {
// 双重检查,保证只有一个线程设置值
serverValue = localCacheMap.get(serverId);
if (serverValue == null) {
serverValue = redisCacheMap.get(serverId);
System.out.println(serverId + "设置值");
localCacheMap.put(serverId, serverValue == null ? "null" : serverValue);
}
}
}
return serverValue;
}
}
测试截图
模拟测试可以看出10个线程,只有一个线程设置,其他都是从一级缓存拿到