记录一次解决并发、循环依赖的问题

我们系统架构现在真的拉,一个独立的子系统,有自己独立的数据库数据表,但是最后看上去只是将一部分的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会占用太多的内存,因为这个服务的用户量会非常的少,所有的手段都是为了数据安全合法。

其他

一些其他的东西,比如建立本地缓存,对已查询的、已初始化的进行缓存,加快下次请求的响应时间,这些都是必不可少的,哈哈。