分布式事务
项目优化
在网站上加载课程详情信息时如果很慢(排除网速)会影响用户的体验性,为了提高响应速度需要将课程详情信息进行缓存并且将课程信息加入索引库方便全局搜索
- Redis中的课程缓存信息是将课程发布表中的数据转为json进行存储
- Elasticsearch中的课程索引信息是根据搜索需要将课程名称、课程介绍等信息进行索引存储
- MinIO中存储了包含课程信息的静态化页面文件,再通过Nginx请求查看课程详情页面(Tomcat的并发能力比较弱)
教学机构用户执行一次课程发布操作后由四个分散的服务与数据库、Redis、elasticsearch、MinIO四个不同的服务进行网络通信
本地事务(数据库事务): 在程序中通过Spring去控制事务是利用数据库本身ACID
的事务特性实现,这种是基于微服务自己的关系型数据库中的事务(一次连接)
分布式事务:在分布式系统环境下实现某个事务时需要多个服务通过网络交互共同完成,网络通信时可能会出现问题导致响应未到达
场景
: 用户注册送积分,银行转账,创建订单减库存
# 本地事务
begin transaction;
//1.本地数据库操作:张三减少金额
//2.本地数据库操作:李四增加金额
commit transation;
# 分布式事务,张三和李四的账户不在一个数据库中甚至不在一个应用系统里,实现转账事务需要通过远程调用
begin transaction;
//1.本地数据库操作:张三减少金额
//2.远程调用:让李四增加金额
//3.当远程调用让李四增加金额成功了,由于网络问题远程调用并没有返回,此时本地事务提交失败就会回滚张三减少金额的操作,但远程调用李四增加金额的操作却成功了
commit transation;
CAP理论
CAP表示Consistency(一致性)、Availability(可用性)、Partition tolerance(分区容忍性)三大特点,但在分布式系统中这三点不可能全部满足
一致性
: 指用户不管访问哪一个结点拿到的数据都是最新的,如果拿不到最新数据就需要等待同步,不能出现数据没有改变时两次查询结果不一样的情况可用性
: 指任何时候查询用户信息都可以查询到结果,但不能保证查询到最新的数据分区容忍性(分区容错性)
:因为在分布式架构中的服务之间难免出现网络异常,但不能因为局部网络异常导致整个系统不可用, 所以当网络通信异常导致请求中断、消息丢失时系统依然对外提供服务
需求: 添加一个用户小明的信息,该信息先添加到结点1中,再同步到结点2中
满足一致性(CP)
:必须等待小明的信息同步完成系统才可用,在信息同步过程中系统是不可用的,即请求到结点2时可能查询不到数据满足可用性(AP)
:要时刻保证系统可用就不能等待信息同步完成才能查询用户信息
进行分布式事务控制时要在C和A中作出取舍,具体要根据应用场景决定具体方案
-
CP的场景:满足C舍弃A,强调一致性
- 跨行转账:一次转账请求要等待双方银行系统都完成整个事务才算完成,只要其中一个失败另一方执行回滚操作
- 开户操作:如在业务系统开户同时要在运营商开户,任何一方开户失败该用户都不可使用
-
AP的场景(使用较多):满足A舍弃C(最终数据还是会达到一致),强调可用性
- 订单退款:今日退款成功,明日账户到账,只要用户可以接受在一定时间内到账即可
- 注册送积分:注册成功,积分在24小时内到账
- 支付短信通信:支付成功发短信,短信发送可以有延迟,甚至没有发送成功
BASE理论
在实际应用中符合AP的场景较多,虽然AP舍弃了一致性,但这只是暂时的,因为最终数据还是会达到一致只不过会需要等待数据同步
BASE 是 Basically Available(基本可用)、Soft state(软状态)和Eventually consistent (最终一致性)三个短语的缩写
基本可用
:当系统无法满足全部可用时保证核心服务可用即可,如外卖系统:每到中午12点左右系统并发量很高,此时要保证下单流程涉及的服务可用,其它服务暂时不可用软状态
:如当用户发表一条评论时,这条评论并不会立即同步到所有关注者的页面上,而是会先存储在缓存中,并逐渐传播到其他节点,这样就存在了一个中间状态,即某些用户可以看到这条评论,而某些用户还不能看到最终一致性
:如退款操作后没有及时到账,但最终经过一定的时间后一定会到账,舍弃强一致性满足最终一致性
课程发布
界面原型
教育机构用户在课程管理中对本机构内课程进行检索 ,如果审核状态为通过点击课程的发布链接即可发布课程
课程发布后学生可在网站搜索到教学机构发布的课程信息,并查看课程的详细信息进行选课和学习,此时如果再从数据库查询课程信息就会大大增加DB压力
课程发布的事务控制方案
实现CP就是要实现强一致性: 执行课程发布操作后向数据库、redis、elasticsearch、MinIO写四份数据,只要有一份写失败其它的全部回滚
- 使用
Seata框架
基于AT模式实现 - 使用
Seata框架
基于TCC模式实现
实现AP则要保证最终数据一致性: 执行课程发布操作后,先更新数据库中课程信息表
的课程发布状态,更新后向redis、Elasticsearch、MinIO写入课程信息(只要在一定时间内写入数据成功即可)
- 使用消息队列通知方案,通知失败自动重试,达到最大失败次数需要人工处理
- 使用任务调度方案,启动任务调度将课程信息由数据库同步到Elasticsearch、MinIO、Redis中
在内容管理服务的数据库中创建消息(任务)表
:教学机构用户执行课程发布操作后,我们需要告诉调度中心哪个课程的信息需要同步
- 通过
本地事务
控制向课程发布表
写入发布课程的信息(存在则更新),写入成功后可以删除课程预发布表中的记录, 最后在消息表
添加一条课程发布的任务消息
(保证两个操作同时操作成功) - 更新
课程信息表course_base
和课程发布表
的课程发布状态为已发布 - 启动
任务调度系统
定时调度内容管理服务去扫描消息表
的课程任务发布记录,当扫描到课程发布的消息时即开始完成向redis、elasticsearch、MinIO同步数据的操作 - 同步数据的任务完成后可以删除消息表中对应的课程发布任务记录
接口定义
在内容管理模块的content-api
接口工程中定义课程发布的接口
@Api(value = "课程预览发布接口",tags = "课程预览发布接口")
@Controller
public class CoursePublishController {@ApiOperation("课程发布")@ResponseBody@PostMapping ("/coursepublish/{courseId}")public void coursepublish(@PathVariable("courseId") Long courseId){Long companyId = 1232141425L;coursePublishService.publish(companyId,courseId);}
}
业务类
课程审核状态通过后用户点击课程的发布链接即可发布课程,即将课程预发布表的课程信息写入课程发布表(两个表的结构基本一样)
课程发布表
状态字段描述课程发布和下架时间
,课程预发布表
状态字段描述课程的提交和审核状态
**
通过本地事务将课程预发布表中包含的课程信息写入课程发布表中并向消息表插入一条课程发布的任务消息
- 发布约束: 本机构只允许发布本机构的课程,预发布课程审核通过方可发布
/*** @description 课程发布接口* @param companyId 机构id* @param courseId 课程id
*/
public void publish(Long companyId,Long courseId);
@Transactional
@Override
public void publish(Long companyId, Long courseId) {// 获取课程预发布表数据CoursePublishPre coursePublishPre = coursePublishPreMapper.selectById(courseId);if(coursePublishPre == null){XueChengPlusException.cast("请先提交课程审核,审核通过才可以发布");}// 预发布课程审核通过方可发布if(!"202004".equals(coursePublishPre.getStatus()){XueChengPlusException.cast("操作失败,课程审核通过方可发布");}// 本机构只允许提交本机构的课程if(!coursePublishPre.getCompanyId().equals(companyId)){XueChengPlusException.cast("不允许提交其它机构的课程");} // 向课程发布表插入数据,如果存在则更新saveCoursePublish(courseId);// 向消息表插入数据即课程发布的任务saveCoursePublishMessage(courseId);// 课程发布后,可以删除课程预发布表对应记录coursePublishPreMapper.deleteById(courseId);}/*** @description 保存课程发布信息* @param courseId 课程id
*/
private void saveCoursePublish(Long courseId){// 判断课程预发布信息是否存在CoursePublishPre coursePublishPre = coursePublishPreMapper.selectById(courseId);if(coursePublishPre == null){XueChengPlusException.cast("课程预发布数据为空");}// 将课程的预发布信息写入到课程发布表,如果存在则更新CoursePublish coursePublish = new CoursePublish();BeanUtils.copyProperties(coursePublishPre,coursePublish);// 设置课程发布表中课程的发布状态为已发布coursePublish.setStatus("203002");CoursePublish coursePublishUpdate = coursePublishMapper.selectById(courseId);// 有则更新,无则新增if(coursePublishUpdate == null){// 没有指定Id时默认基于雪花算法生成IdcoursePublishMapper.insert(coursePublish);}else{coursePublishMapper.updateById(coursePublish);}// 更新课程基本信息表的发布状态为已发布CourseBase courseBase = courseBaseMapper.selectById(courseId);courseBase.setStatus("203002");courseBaseMapper.updateById(courseBase);
}/*** @description 保存消息表记录,稍后实现* @param courseId 课程id*/
private void saveCoursePublishMessage(Long courseId){}
测试
约束条件测试
:在未提交审核时发布课程,在课程审核未通过时进行发布
正常流程测试
:提交审核课程,手动修改课程预发布表与课程基本信息的审核状态为审核通过,执行课程发布
- 最终结果:课程发布表记录是否正常(消息表暂未实现),课程预发布表记录已经删除,课程基本信息表与课程发布表的发布状态为已发布