我们系统架构现在真的拉,一个独立的子系统,有自己独立的数据库数据表,但是最后看上去只是将一部分的Controller以项目为单位分出来了;需求上,还是很多需要连表查询,所以不得不将主项目中的数据冗余一份到这个独立项目中来。
解决循环依赖问题
冗余的时机是什么?冗余哪些数据?考虑到这个系统使用的人会比较少,没有必要将大量用户的数据导入到自己的库中,我决定在用户首次访问时拉取这些用户的信息。
如何确保用户的首次访问?我们的系统目前没有办法实现这一点,所以我决定拦截所有的请求,然后判断发起这个请求的用户是否是首次访问。
拦截请求我使用了Spring MVC相关的拦截器:
1
2
3
4
5
6
7
8
9
10
11
|
@Configuration
@RequiredArgsConstructor
public class CompanyInitInterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new CompanyInitInterceptor()).order(10);
}
}
|
1
2
3
4
5
6
7
8
9
10
11
|
@Slf4j
public class CompanyInitInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
}
}
|
核心代码在CompanyInitInterceptor中,由于在CompanyInitInterceptor中需要调用FeignClient拉取一些数据,而FeignClient又依赖于Web配置,导致这块我们一定会形成循环依赖。
如何解决循环依赖?我决定让CompanyInitInterceptor脱离IOC容器的管理,由我们自己进行初始化,而在CompanyInitInterceptor中用到的FeignClient在我们在运行的时候从从容器中获取。考虑到并发问题,这儿使用了一个double check。代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
private volatile CompanyClientManager companyClientManager;
if (companyClientManager == null) {
synchronized (CompanyInitInterceptor.class) {
if (companyClientManager == null) {
companyClientManager = BeanUtils.getBean(CompanyClientManager.class);
}
}
}
// do something else
}
|
BeanUtils是如何开发的,其代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
public class BeanUtils {
private static ConfigurableApplicationContext configurableApplicationContext;
public static void init(ConfigurableApplicationContext configurableApplicationContext) {
if (BeanUtils.configurableApplicationContext != null) {
return;
}
BeanUtils.configurableApplicationContext = configurableApplicationContext;
}
public static <T> T getBean(Class<T> clazz) {
if (configurableApplicationContext == null) {
return null;
}
return configurableApplicationContext.getBean(clazz);
}
}
|
其初始则是在启动类中:
1
2
3
4
5
6
7
8
9
10
|
@EnableFeignClients
@SpringBootApplication
public class SRMApplication {
public static void main(String[] args) {
ConfigurableApplicationContext configurableApplicationContext = SpringApplication.run(SRMApplication.class, args);
BeanUtils.init(configurableApplicationContext);
}
}
|
解决并发问题
由于需要进行一次请求,这个地方的并发问题很突出,很容易就发生了并发问题。如果将整个初始化操作都加上锁,则又会影响用户的体验,所以我决定根据用户的companyId进行上锁。代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
private static final ConcurrentHashMap<String, Lock> COMPANY_ID_TO_LOCK = new ConcurrentHashMap<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// do something else
Lock lock = COMPANY_ID_TO_LOCK.computeIfAbsent(companyId, tmp -> new ReentrantLock());
lock.lock();
try {
// 查库,判断是否已初始化
try {
// 方案一:调用第三方数据进行初始化
return true;
} catch (Exception e) {
try {
// 方案二:直接插入记录进行初始化(稍后等待定时任务进行同步)
return true;
} catch (Exception e2) {
// 初始化失败了,放行该请求,等待下次请求中初始化
return true;
}
}
} finally {
lock.unlock();
}
}
|
我的思路就是用建立companyId到Lock的映射,这样初始化时,只会阻塞某个用户的请求,不会影响到其他的用户。不必担心COMPANY_ID_TO_LOCK会占用太多的内存,因为这个服务的用户量会非常的少,所有的手段都是为了数据安全合法。
其他
一些其他的东西,比如建立本地缓存,对已查询的、已初始化的进行缓存,加快下次请求的响应时间,这些都是必不可少的,哈哈。