场景
多线程批量向用户发起合同,根据联系方式寻找系统中的用户,如果存在就判断姓名等信息有无变更,发生变化需要更新为合同上写的;如果系统中没有该联系方式用户,则需要创建相应用户并发送合同。
原始代码
@Override
public User init(UserDTO dto) {
String contact = dto.getContact();
String name = dto.getName();
String password = dto.getPassword();
// 先查询数据库
User user = this.getUser(contact);
if (user != null) {
if(needUpdate) {
userService.update(user);
}
return user;
}
// 数据库不存在,创建用户逻辑
user = new User();
user.setOpenUserId(openUserId);
user.setName(name);
// 。。。
// 创建账户
accountService.create(contact, password);
return user;
}
并发就会导致数据库存在同一个联系方式的两个用户,由于数据库对联系方式字段设置了唯一索引,导致程序报错,批量发起合同失败。🥲
修改后的代码
@Override
public User init(UserDTO dto) {
String contact = dto.getContact();
String name = dto.getName();
String password = dto.getPassword();
// 正常流程、先查询数据库
User user = this.getUser(contact);
if (user != null) {
// 需要变更用户信息,触发修改逻辑
if(needUpdate) {
userService.update(user);
}
return user;
}
boolean isCreate = false; // 用于标记是否进行了用户创建步骤
String lockKey = getLockKey(contact);
try {
// 加锁
cacheClient.lockInfinite(lockKey);
// 先查询缓存
user = cacheClient.getValue(INIT_USER_PRE_FIX + lockKey);
if (user != null) {
return user;
}
sqlSession.clearCache();
// 在查询数据库
user = this.getUser(contact);
if (user != null) {
return user;
}
// 创建用户
user = new User();
user.setName(name);
// ...
isCreate = true;
} finally {
if (isCreate) {
cacheClient.set(INIT_USER_PRE_FIX + lockKey, user);
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCompletion(int status) {
cacheClient.delete(INIT_USER_PRE_FIX + lockKey);
}
});
}
cacheClient.unlock(lockKey);
}
return user;
}
疑难解答
为什么要在加锁的代码中再查询一次数据库呢,上面已经有一步查库了
答:并发场景下两个线程同一个联系方式,如果其中一个在等待锁的时间,另外一个线程已经创建好了该联系方式对应的用户,等待线程获取到锁后,如果不进行查询,则会出发数据库唯一索引报错,导致程序运行失败。
为什么需要 sqlSession.clearCache()
答:因为上面在加锁前已经先查询了一次数据库,存在一级缓存(能走到下面创建用户逻辑,肯定没触发更新逻辑,所以一级缓存存在值为nul),所以先删除一级缓存,不然下面查询始终为null 。
为什么要缓存一个用户对象呢
答:缓存存在的意义就是,因为这个方法执行完成并不代表当前联系方式对应的用户真正创建出来了,因为该方法是被调用方,需要等到上层事务结束才会提交,所以先创建一个缓存表示用户创建成功了。同样也解答了需要在 TransactionSynchronizationManager 中注册一个事务完成事件,来删除缓存,因为在事务结束,缓存也没啥意义了。
这个方法为什么不使用 required_new 与上层事务隔离开呢。
答:因为上层是批量发起的合同,如果是指定了一万个用户,并且都不在当前系统中,required_new 新开事务的话,需要考虑数据库连接池的问题,一不小心就给连接池占满。
依然存在的问题:因为缓存和数据库的不一致性,导致会存在一种情况就是,线程一添加用户缓存成功了,线程二取到了缓存用户并执行后续业务逻辑,但是线程一在执行后续逻辑的时候报错了,导致回滚,这样线程二就拿了个假用户处理。最终的问题就是,用户收到了合同,点击链接进入系统,发现登录不上。 😅
目前对于这种情况没有做处理,这边经过测试了不同的 合同用户比 数据,发现都没报错,就没管了。 😀
评论区