SpringBoot+Dubbo分布式SOA项目骨架搭建
项目介绍
本项目是来自于上一篇文章http://blog.csdn.net/songxinjianqwe/article/details/77478385
中的服务化拆分
这个部分。经过一段时间的学习后,将原有项目在功能不变的基础上,基于Dubbo实现了服务化拆分。
涉及技术
- SpringBoot+多环境配置(dev,proc,test)
- Dubbo
- SpringMVC
- Spring
- MyBaits
- MyBatis Generator
- MyBatis PageHelper
- Druid
- Lombok
- JWT
- Spring Security
- JavaMail
- Thymeleaf
- HttpClient
- FileUpload
- Spring Scheduler
- Hibernate Validator
- Redis Cluster
- MySQL主从复制,读写分离
- Spring Async
- Spring Cache
- Swagger
- Spring Test
- Spring Actuator
- Logback+Slf4j多环境日志
- i18n
- Maven Multi-Module
功能点
用户模块
- 获取图片验证码
- 登录:解决重复登录问题
- 注册
- 分页查询用户信息
- 修改用户信息
- 1
- 2
- 3
- 4
- 5
- 6
站内信模块
- 一对一发送站内信
- 管理员广播
- 读取站内信(未读和已读)
- 一对多发送站内信
- 1
- 2
- 3
- 4
- 5
文件模块
- 文件上传
- 文件下载
- 1
- 2
- 3
邮件模块
- 单独发送邮件
- 群发邮件
- Thymeleaf邮件模板
- 1
- 2
- 3
- 4
安全模块
- 注解形式的权限校验
- 拦截器
- 1
- 2
- 3
实现细节
参考资料
讲解Dubbo相关知识最全面的是阿里巴巴的用户指南。
Dubbo用户指南
另外Dubbo管控平台的搭建参考下面这个链接。
Dubbo管控平台
基础架构
分为Registry(比如Zookeeper注册中心),Monitor(部署在Tomcat中,实时获取Provider和Consumer等的状态),Provider(比如WebProject),Consumer(Services)。
安装Dubbo管控台
主机一台,前提是安装了JDK和Tomcat。
1. 将war(从网络上下载即可)包放到tomcat的webapps下面2
2. . 运行tomcat
/usr/local/apache-tomcat-8.5.20/bin/startup.sh
3. 修改/usr/local/apache-tomcat-8.5.20/webapps/dubbo-admin-2.8.4/WEB-INF/dubbo.properties
dubbo.registry.address=zookeeper://192.168.1.118:2181
dubbo.admin.root.password=root
dubbo.admin.guest.password=guest
- 1
- 2
- 3
192.168.1.118是zookeeper主机的IP地址。
4. 启动zookeeper
5. 重启tomcat
一定要先启动zookeeper启动后再去启动tomcat!
6. 访问
http://192.168.1.121:8080/dubbo-admin-2.8.4
前提是主机的防火墙关闭。
服务拆分
- 表:避免出现A服务关联B服务的表的数据操作;服务一旦划分了,那么数据库即便没分开,也要当成db表分开了来进行编码;否则AB服务难以进行垂直拆库
- 避免服务耦合度高,依赖调用;如果出现,考虑服务调优。
- 避免分布式事务,不要拆分过细。
- 接口尽可能大粒度,接口中的方法不要以业务流程来,这个流程尽量在方法逻辑中调用,接口应代表一个完整的功能对外提供;
- 接口应以业务为单位,业务相近的进行抽象,避免接口数量爆炸
- 参数先做校验,在传入接口。
- 要做到在设计接口时,已经确定这个接口职责、预测调用频率。
- 个人经验总结:
比如模块有user、mail等,原本每个模块下有dao、service、web等包,现在是将每个模块作为一个子项目,统一命名为biz(bussiness的简写)。
- biz-service(接口实现类)
- biz-api(domain实体类、枚举类、异常类、接口)
- biz-web(RESTful Controller,消费Service)
- web依赖于api
- service依赖于api
- service和web没有依赖
项目结构
这里有三个子项目(email邮件服务、mail站内信、user用户),每个项目都有api和service。每个service都是Dubbo的Provider,同时也有可能是Dubbo的Consumer。web子项目一定是Dubbo的Consumer。
代码示例
以user模块为例:
user-api
UserService
/**
* Created by SinjinSong on 2017/4/27.
*/
public interface UserService {
UserDO findByUsername(String username);
UserDO findByPhone(String phone);
UserDO findById(Long id);
void save(UserDO userDO);
void update(UserDO userDO);
PageInfo<UserDO> findAll(int pageNum, int pageSize);
String findAvatarById(Long id);
UserDO findByEmail(String email);
void resetPassword(Long id, String newPassword);
List<Long> findAllUserIds();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
user-service
UserServiceImpl
/**
* Created by SinjinSong on 2017/4/27.
*/
@Service
@Slf4j
public class UserServiceImpl implements UserService {
@Autowired
private UserDOMapper userDOMapper;
@Autowired
private RoleDOMapper roleDOMapper;
@Override
@Cacheable("UserDO")
@Transactional(readOnly = true)
public UserDO findByUsername(String username) {
return userDOMapper.findByUsername(username);
}
@Override
@Cacheable("UserDO")
@Transactional(readOnly = true)
public UserDO findByPhone(String phone) {
return userDOMapper.findByPhone(phone);
}
@Override
@Cacheable("UserDO")
@Transactional(readOnly = true)
public UserDO findById(Long id) {
return userDOMapper.selectByPrimaryKey(id);
}
@Override
@Transactional
@CacheEvict(value = "UserDO",allEntries = true)
public void save(UserDO userDO) {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
//对密码进行加密
userDO.setPassword(passwordEncoder.encode(userDO.getPassword()));
userDO.setRegTime(LocalDateTime.now());
//设置用户状态为未激活
userDO.setUserStatus(UserStatus.UNACTIVATED);
userDOMapper.insert(userDO);
//添加用户的角色,每个用户至少有一个user角色
long roleId = roleDOMapper.findRoleIdByRoleName("ROLE_USER");
roleDOMapper.insertUserRole(userDO.getId(),roleId);
}
@Override
@Transactional
@CacheEvict(value = "UserDO",allEntries = true)
public void update(UserDO userDO) {
userDOMapper.updateByPrimaryKeySelective(userDO);
}
@Override
@Transactional
@CacheEvict(value = "UserDO",allEntries = true)
public void resetPassword(Long id,String newPassword) {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
UserDO userDO = new UserDO();
userDO.setId(id);
userDO.setPassword(passwordEncoder.encode(newPassword));
userDOMapper.updateByPrimaryKeySelective(userDO);
}
@Override
public List<Long> findAllUserIds() {
return userDOMapper.findAllUserIds();
}
@Override
public PageInfo<UserDO> findAll(int pageNum, int pageSize) {
return userDOMapper.findAll(pageNum,pageSize).toPageInfo();
}
@Override
public String findAvatarById(Long id) {
return userDOMapper.findAvatarById(id);
}
@Override
public UserDO findByEmail(String email) {
return userDOMapper.findByEmail(email);
}
}
- 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
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
UserApplication
/**
* Created by SinjinSong on 2017/9/21.
*/
@SpringBootApplication
@EnableConfigurationProperties
@ComponentScan({"cn.sinjinsong"})
@ImportResource("classpath:dubbo.xml")
@Slf4j
public class UserApplication implements CommandLineRunner {
public static void main(String[] args) throws IOException {
SpringApplication app = new SpringApplication(UserApplication.class);
app.setWebEnvironment(false);
app.run(args);
synchronized (UserApplication.class) {
while (true) {
try {
UserApplication.class.wait();
} catch (Throwable e) {
}
}
}
}
@Override
public void run(String... args) throws Exception {
}
}
- 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
dubbo.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://code.alibabatech.com/schema/dubbo
http://code.alibabatech.com/schema/dubbo/dubbo.xsd
">
<bean id="userService"
class="cn.sinjinsong.skeleton.service.impl.UserServiceImpl"/>
<!-- 提供方应用信息,用于计算依赖关系 -->
<dubbo:application name="user_provider"/>
<!-- 使用zookeeper注册中心暴露服务地址 -->
<dubbo:registry protocol="zookeeper" address="192.168.1.118:2181,192.168.1.119:2181,192.168.1.120:2181"/>
<!-- 用dubbo协议在20880端口暴露服务 -->
<dubbo:protocol name="dubbo" port="20880" serialization="java"/>
<!-- 声明需要暴露的服务接口 -->
<dubbo:service interface="cn.sinjinsong.skeleton.service.UserService" version="1.0.0"
ref="userService"/>
</beans>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
web
UserController
/**
* Created by SinjinSong on 2017/4/27.
*/
@RestController
@RequestMapping("/users")
@Api(value = "users", description = "用户API")
@Slf4j
public class UserController {
@Autowired
private UserService service;
@Autowired
private VerificationManager verificationManager;
@Autowired
private EmailService emailService;
@Autowired
private AuthenticationProperties authenticationProperties;
/**
* mode 支持id、username、email、手机号
* 只有管理员或自己才可以查询某用户的完整信息
*
* @param key
* @param mode id、username、email、手机号
* @return
*/
@RequestMapping(value = "/query/{key}", method = RequestMethod.GET)
@PostAuthorize("hasRole('ADMIN') or (returnObject.username == principal.username)")
@ApiOperation(value = "按某属性查询用户", notes = "属性可以是id或username或email或手机号", response = UserDO.class, authorizations = {@Authorization("登录权限")})
@ApiResponses(value = {
@ApiResponse(code = 401, message = "未登录"),
@ApiResponse(code = 404, message = "查询模式未找到"),
@ApiResponse(code = 403, message = "只有管理员或用户自己能查询自己的用户信息"),
})
public UserDO findByKey(@PathVariable("key") @ApiParam(value = "查询关键字", required = true) String key, @RequestParam("mode") @ApiParam(value = "查询模式,可以是id或username或phone或email", required = true) String mode) {
QueryUserHandler handler = SpringContextUtil.getBean("QueryUserHandler", StringUtils.lowerCase(mode));
if (handler == null) {
throw new QueryUserModeNotFoundException(mode);
}
UserDO userDO = handler.handle(key);
if (userDO == null) {
throw new UserNotFoundException(key);
}
return userDO;
}
@ResponseStatus(HttpStatus.CREATED)
@RequestMapping(method = RequestMethod.POST)
@ApiOperation(value = "创建用户,为用户发送验证邮件,等待用户激活,若24小时内未激活需要重新注册", response = Void.class)
@ApiResponses(value = {
@ApiResponse(code = 409, message = "用户名已存在"),
@ApiResponse(code = 400, message = "用户属性校验失败")
})
public void createUser(@RequestBody @Valid @ApiParam(value = "用户信息,用户的用户名、密码、昵称、邮箱不可为空", required = true) UserDO user, BindingResult result) {
log.info("{}", user);
if (isUsernameDuplicated(user.getUsername())) {
throw new UsernameExistedException(user.getUsername());
} else if (result.hasErrors()) {
throw new ValidationException(result.getFieldErrors());
}
//生成邮箱的激活码
String activationCode = UUIDUtil.uuid();
//保存用户
service.save(user);
verificationManager.createVerificationCode(activationCode, String.valueOf(user.getId()), authenticationProperties.getActivationCodeExpireTime());
log.info("{} {}", user.getEmail(), user.getId());
//发送邮件
Map<String, Object> params = new HashMap<>();
params.put("id", user.getId());
params.put("activationCode", activationCode);
emailService.sendHTML(user.getEmail(), "activation", params, null);
}
@RequestMapping(value = "/{id}/avatar", method = RequestMethod.GET)
@ApiOperation(value = "获取用户的头像图片", response = Byte.class)
@ApiResponses(value = {
@ApiResponse(code = 404, message = "文件不存在"),
@ApiResponse(code = 400, message = "文件传输失败")
})
public void getUserAvatar(@PathVariable("id") Long id, HttpServletRequest request, HttpServletResponse response) {
String relativePath = service.findAvatarById(id);
FileUtil.download(relativePath, request.getServletContext(), response);
}
@RequestMapping(value = "/{id}/activation", method = RequestMethod.GET)
@ApiOperation(value = "用户激活,前置条件是用户已注册且在24小时内", response = Void.class)
@ApiResponses(value = {
@ApiResponse(code = 401, message = "未注册或超时或激活码错误")
})
public void activate(@PathVariable("id") @ApiParam(value = "用户Id", required = true) Long id, @RequestParam("activationCode") @ApiParam(value = "激活码", required = true) String activationCode) {
UserDO user = service.findById(id);
//获取Redis中的验证码
if (!verificationManager.checkVerificationCode(activationCode, String.valueOf(id))) {
verificationManager.deleteVerificationCode(activationCode);
throw new ActivationCodeValidationException(activationCode);
}
user.setUserStatus(UserStatus.ACTIVATED);
verificationManager.deleteVerificationCode(activationCode);
service.update(user);
}
// 更新
@RequestMapping(method = RequestMethod.PUT)
@PreAuthorize("#user.username == principal.username or hasRole('ADMIN')")
@ApiOperation(value = "更新用户信息", response = Void.class, authorizations = {@Authorization("登录权限")})
@ApiResponses(value = {
@ApiResponse(code = 401, message = "未登录"),
@ApiResponse(code = 404, message = "用户属性校验失败"),
@ApiResponse(code = 403, message = "只有管理员或用户自己能更新用户信息"),
})
public void updateUser(@RequestBody @Valid @ApiParam(value = "用户信息,用户的用户名、密码、昵称、邮箱不可为空", required = true) UserDO user, BindingResult result) {
if (result.hasErrors()) {
throw new ValidationException(result.getFieldErrors());
}
service.update(user);
}
@RequestMapping(value = "/{key}/password/reset_validation", method = RequestMethod.GET)
@ApiOperation(value = "发送忘记密码的邮箱验证", notes = "属性可以是id,sername或email或手机号", response = UserDO.class)
public void forgetPassword(@PathVariable("key") @ApiParam(value = "关键字", required = true) String key, @RequestParam("mode") @ApiParam(value = "验证模式,可以是username或phone或email", required = true) String mode) {
UserDO user = findByKey(key, mode);
//user 一定不为空
String forgetPasswordCode = UUIDUtil.uuid();
verificationManager.createVerificationCode(forgetPasswordCode, String.valueOf(user.getId()), authenticationProperties.getForgetNameCodeExpireTime());
log.info("{} {}", user.getEmail(), user.getId());
//发送邮件
Map<String, Object> params = new HashMap<>();
params.put("id", user.getId());
params.put("forgetPasswordCode", forgetPasswordCode);
emailService.sendHTML(user.getEmail(), "forgetPassword", params, null);
}
@RequestMapping(value = "/{id}/password", method = RequestMethod.PUT)
@ApiOperation(value = "忘记密码后可以修改密码")
public void resetPassword(@PathVariable("id") Long id, @RequestParam("forgetPasswordCode") @ApiParam(value = "验证码", required = true) String forgetPasswordCode, @RequestParam("password") @ApiParam(value = "新密码", required = true) String password) {
//获取Redis中的验证码
if (!verificationManager.checkVerificationCode(forgetPasswordCode, String.valueOf(id))) {
verificationManager.deleteVerificationCode(forgetPasswordCode);
throw new ActivationCodeValidationException(forgetPasswordCode);
}
verificationManager.deleteVerificationCode(forgetPasswordCode);
service.resetPassword(id, password);
}
@RequestMapping(value = "/{username}/duplication", method = RequestMethod.GET)
@ApiOperation(value = "查询用户名是否重复", response = Boolean.class)
@ApiResponses(value = {@ApiResponse(code = 401, message = "未登录")})
public boolean isUsernameDuplicated(@PathVariable("username") String username) {
if (service.findByUsername(username) == null) {
return false;
}
return true;
}
@RequestMapping(method = RequestMethod.GET)
@PreAuthorize("hasRole('ADMIN')")
@ApiOperation(value = "分页查询用户信息", response = PageInfo.class, authorizations = {@Authorization("登录权限")})
@ApiResponses(value = {@ApiResponse(code = 401, message = "未登录")})
public PageInfo<UserDO> findAllUsers(@RequestParam(value = "pageNum", required = false, defaultValue = PageProperties.DEFAULT_PAGE_NUM) @ApiParam(value = "页码,从1开始", defaultValue = PageProperties.DEFAULT_PAGE_NUM) Integer pageNum, @RequestParam(value = "pageSize", required = false, defaultValue = PageProperties.DEFAULT_PAGE_SIZE) @ApiParam(value = "每页记录数", defaultValue = PageProperties.DEFAULT_PAGE_SIZE) Integer pageSize) {
return service.findAll(pageNum, pageSize);
}
}
- 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
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
- 166
- 167
WebApplication
/**
* Created by SinjinSong on 2017/9/22.
*/
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@EnableConfigurationProperties
@ComponentScan({"cn.sinjinsong"})
@ImportResource("classpath:dubbo.xml")
@Slf4j
public class WebApplication extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(WebApplication.class, args);
synchronized (WebApplication.class) {
while (true) {
try {
WebApplication.class.wait();
} catch (Throwable e) {
}
}
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
dubbo.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://code.alibabatech.com/schema/dubbo
http://code.alibabatech.com/schema/dubbo/dubbo.xsd
">
<!-- 消费者应用信息,用于提供依赖关系 -->
<dubbo:application name="web_consumer"/>
<!-- 注册地址,用于消费者寻找服务 -->
<dubbo:registry protocol="zookeeper" address="192.168.1.118:2181,192.168.1.119:2181,192.168.1.120:2181"/>
<dubbo:consumer timeout="5000"/>
<!-- 引用的服务 -->
<dubbo:reference id="userService" interface="cn.sinjinsong.skeleton.service.UserService" version="1.0.0"/>
<dubbo:reference id="mailService" interface="cn.sinjinsong.skeleton.service.MailService" version="1.0.0"/>
<dubbo:reference id="emailService" interface="cn.sinjinsong.skeleton.service.EmailService" version="1.0.0"/>
</beans>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
总结
在项目中引入Dubbo有几种方式,最常用的是基于XML和基于注解的。就个人感觉而言基于XML虽然有点low,但是对代码的侵入性很小,可以说完全不需要对代码有任何改变,只需要在dubbo.xml写bean即可,而且官方的用户手册中的示例都是以XML为例的,所以就通用性而言XML更好一些。
坑
spring-boot-starter-dubbo
我尝试了阿里的一位技术员扩展的spring-boot-starter-dubbo,但是不知什么原因Provider不能连接Zookeeper,而Consumer可以,后来换了原生的API就都可以连接了。
序列化
dubbo默认的序列化方式是hession2,但是它不支持Java8的时间日期API(LocalDateTime、LocalDate等)的序列化,在序列化时会抛出StackOverflowError异常。使用Java默认的序列化方式不会出现这种问题。
<dubbo:protocol name="dubbo" port="20880" serialization="java"/>
- 1
- 2
其他序列化方式
dubbo=com.alibaba.dubbo.common.serialize.support.dubbo.DubboSerialization
hessian2=com.alibaba.dubbo.common.serialize.support.hessian.Hessian2Serialization
java=com.alibaba.dubbo.common.serialize.support.java.JavaSerialization
compactedjava=com.alibaba.dubbo.common.serialize.support.java.CompactedJavaSerialization
json=com.alibaba.dubbo.common.serialize.support.json.JsonSerialization
fastjson=com.alibaba.dubbo.common.serialize.support.json.FastJsonSerialization
nativejava=com.alibaba.dubbo.common.serialize.support.nativejava.NativeJavaSerialization
kryo=com.alibaba.dubbo.common.serialize.support.kryo.KryoSerialization
fst=com.alibaba.dubbo.common.serialize.support.fst.FstSerialization
jackson=com.alibaba.dubbo.common.serialize.support.json.JacksonSerialization
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
事务
我做的这个项目骨架距离真正的分布式项目尚有一些区别,最主要的是应用拆分与数据库拆分是同时进行的。数据库的水平拆分可以减轻单库的压力,通常情况下单个应用和它所依赖的表是放在同一个机器上的,而我现在是把数据库集中到某几台机器上了。
数据库的拆分一定会产生分布式事务的问题,而如果数据库没有进行水平拆分,都放在同一台机器上,各个应用都连接同一个数据库,是不存在分布式事务的问题的。分布式事务目前Dubbo本身尚不支持,可以通过一些其他的方式比如2PC、TCC、MQ等解决。
另外数据库拆分后的访问本身就比较麻烦,需要依赖于一些数据库的中间件比如MyCat等,目前尚未加入到本项目骨架中。