部署在不同结点上的系统通过网络交互来完成协同工作的系统。
比如:充值加积分的业务,用户在充值系统向自己的账户充钱,在积分系统中自己积分相应的增加。充值系统和积分系统是两个不同的系统,一次充值加积分的业务就需要这两个系统协同工作来完成。
事务是指由一组操作组成的一个工作单元,这个工作单元具有原子性(atomicity)、一致性(consistency)、隔离性(isolation)和持久性(durability)。
原子性:执行单元中的操作要么全部执行成功,要么全部失败。如果有一部分成功一部分失败那么成功的操作要全部回滚到执行前的状态。一致性:执行一次事务会使用数据从一个正确的状态转换到另一个正确的状态,执行前后数据都是完整的。 隔离性:在该事务执行的过程中,任何数据的改变只存在于该事务之中,对外界没有影响,事务与事务之间是完全的隔离的。只有事务提交后其它事务才可以查询到最新的数据。 持久性:事务完成后对数据的改变会永久性的存储起来,即使发生断电宕机数据依然在。
本地事务就是用关系数据库来控制事务,关系数据库通常都具有ACID特性,传统的单体应用通常会将数据全部储在一个数据库中,会借助关系数据库来完成事务控制。
在分布式系统中一次操作由多个系统协同完成,这种一次事务操作涉及多个系统通过网络协同完成的过程称为分布式事务。这里强调的是多个系统通过网络协同完成一个事务的过程,并不强调多个系统访问了不同的数据库,即使多个系统访问的是同一个数据库也是分布式事务,如下图:
另外一种分布式事务的表现是,一个应用程序使用了多个数据源连接了不同的数据库,当一次事务需要操作多个数据源,此时也属于分布式事务,当系统作了数据库拆分后会出现此种情况。
电商系统中的下单扣库存
电商系统中,订单系统和库存系统是两个系统,一次下单的操作由两个系统协同完成
金融系统中的银行卡充值
在金融系统中通过银行卡向平台充值需要通过银行系统和金融系统协同完成。
教育系统中下单选课业务
在线教育系统中,用户购买课程,下单支付成功后学生选课成功,此事务由订单系统和选课系统协同完成。
SNS系统的消息发送
在社交系统中发送站内消息同时发送手机短信,一次消息发送由站内消息系统和手机通信系统协同完成。
如何进行分布式事务控制?CAP
理论是分布式事务处理的理论基础,了解了CAP
理论有助于我们研究分布式事务的处理方案。
CAP
理论是:分布式系统在设计时只能在一致性(Consistency)
、可用性(Availability)
、分区容忍性(PartitionTolerance)
中满足两种,无法兼顾三种。
一致性(Consistency)
:服务A、B、C三个结点都存储了用户数据, 三个结点的数据需要保持同一时刻数据一致性。
可用性(Availability)
:服务A、B、C三个结点,其中一个结点宕机不影响整个集群对外提供服务,如果只有服务A结点,当服务A宕机整个系统将无法提供服务,增加服务B、C是为了保证系统的可用性。
分区容忍性(Partition Tolerance)
:分区容忍性就是允许系统通过网络协同工作,分区容忍性要解决由于网络分区导致数据的不完整及无法访问等问题。
分布式系统不可避免的出现了多个系统通过网络协同工作的场景,结点之间难免会出现网络中断、网延延迟等现象,这种现象一旦出现就导致数据被分散在不同的结点上,这就是网络分区。
在保证分区容忍性的前提下一致性和可用性无法兼顾,如果要提高系统的可用性就要增加多个结点,如果要保证数据的一致性就要实现每个结点的数据一致,结点越多可用性越好,但是数据一致性越差。所以,在进行分布式系统设计时,同时满足“一致性”、“可用性”和“分区容忍性”三者是几乎不可能的。
CAP有哪些组合方式?
为解决分布式系统的数据一致性问题出现了两阶段提交协议(2 Phase Commitment Protocol),两阶段提交由
协调者和参与者组成,共经过两个阶段和三个操作,部分关系数据库如Oracle、MySQL支持两阶段提交协议,本节讲解关系数据库两阶段提交协议。
第一阶段:准备阶段(prepare)
协调者通知参与者准备提交订单,参与者开始投票。协调者完成准备工作向协调者回应Yes。
第二阶段:提交(commit)/回滚(rollback)阶段
协调者根据参与者的投票结果发起最终的提交指令。如果有参与者没有准备好则发起回滚指令。
2PC
示例:以下单为例子
prepare
,两个数据库收到消息分别执行本地事务(记录日志),但不提交,如果执行成功则回复yes,否则回复no。优点:实现强一致性,部分关系数据库支持(Oracle、MySQL等)。
缺点:整个事务的执行需要由协调者在多个节点之间去协调,增加了事务的执行时间,性能低下。
解决方案有:springboot
+ Atomikos
/ Bitronix
TCC
事务补偿是基于2PC
实现的业务层事务控制方案,它是Try
、Confirm
和Cancel
三个单词的首字母,含义如下:
Try 检查及预留业务资源
完成提交事务前的检查,并预留好资源。
Confirm 确定执行业务操作
对try阶段预留的资源正式执行。
Cancel 取消执行业务操作
对try阶段预留的资源释放。
TCC
示例:下单
Try
下单业务由订单服务和库存服务协同完成,在try阶段订单服务和库存服务完成检查和预留资源。订单服务检查当前是否满足提交订单的条件(比如:当前存在未完成订单的不允许提交新订单)。库存服务检查当前是否有充足的库存,并锁定资源。
Confirm
订单服务和库存服务成功完成Try后开始正式执行资源操作。订单服务向订单写一条订单信息。库存服务减去库存。
Cancel
如果订单服务和库存服务有一方出现失败则全部取消操作。订单服务需要删除新增的订单信息。库存服务将减去的库存再还原。
优点:最终保证数据的一致性,在业务层实现事务控制,灵活性好。
缺点:开发成本高,每个事务操作每个参与者都需要实现try
,confirm
,cancel
三个接口。
注意:TCC
的try
,confirm
,cancel
接口都要实现幂等性,在为在try
、confirm
、cancel
失败后要不断重试。
什么是幂等性?
幂等性是指同一个操作无论请求多少次,其结果都相同。
幂等操作实现方式有:
消息队列实现最终一致示例:下单
实现最终事务一致要求:预留资源成功理论上要求正式执行成功,如果执行失败会进行重试,要求业务执行方法实现幂等。
优点 :由MQ
按异步的方式协调完成事务,性能较高。不用实现try
,confirm
,cancel
接口,开发成本比TCC
低。
缺点:此方式基于关系数据库本地事务来实现,会出现频繁读写数据库记录,浪费数据库资源,另外对于高并发操作不是最佳方案。
根据分布式事务的研究结果,订单服务需要定时扫描任务表向MQ发送任务。本节研究定时任务处理的方案,并实现定时任务扫描任务表并向MQ发送消息。
实现定时任务的方案如下:
使用jdk
的Timer
和TimerTask
实现
可以实现简单的间隔执行任务,无法实现按日历去调度执行任务。
使用Quartz
实现
Quartz
是一个异步任务调度框架,功能丰富,可以实现按日历调度。
使用Spring Task
实现
Spring 3.0
后提供Spring Task
实现任务调度,支持按日历调度,相比Quartz
功能稍简单,但是在开发基本够用,支持注解编程方式。
串行任务比较简单,直接使用@Scheduled
注解。
Cron
表达式,也比较简单,不会有人没用过吧,不会吧不会吧不会吧!!!
实在没用过,可以使用在线的Cron
表达式生成器。
注意:需要在
Spring Boot
启动类上添加@EnableScheduling
注解
创建异步任务配置类,配置线程池实现任务的并行执行
package com.xuecheng.order.config;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import java.util.concurrent.Executor;
@Configuration
//@EnableScheduling // 未在启动类添加该注解的话,可以在这里添加
public class AsyncTaskConfig implements SchedulingConfigurer, AsyncConfigurer {
// 线程池线程数量
private int corePoolSize = 5;
@Bean
public ThreadPoolTaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.initialize();// 初始化线程池
scheduler.setPoolSize(corePoolSize);// 线程池容量
return scheduler;
}
@Override
public Executor getAsyncExecutor() {
return taskScheduler();
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return null;
}
@Override
public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
scheduledTaskRegistrar.setTaskScheduler(taskScheduler());
}
}
配置好了,使用
@Scheduled
注解的方法,都将使用线程池运行。
配置RabbitMQ
的queue
和exchange
package com.xuecheng.order.config;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitMQConfig {
//添加选课任务交换机
public static final String EX_LEARNING_ADDCHOOSECOURSE = "ex_learning_addchoosecourse";
//添加选课消息队列
public static final String XC_LEARNING_ADDCHOOSECOURSE = "xc_learning_addchoosecourse";
//完成添加选课消息队列
public static final String XC_LEARNING_FINISHADDCHOOSECOURSE = "xc_learning_finishaddchoosecourse";
//添加选课路由key
public static final String XC_LEARNING_ADDCHOOSECOURSE_KEY = "addchoosecourse";
//完成添加选课路由key
public static final String XC_LEARNING_FINISHADDCHOOSECOURSE_KEY = "finishaddchoosecourse";
/**
* 交换机配置
*
* @return the exchange
*/
@Bean(EX_LEARNING_ADDCHOOSECOURSE)
public Exchange EX_DECLARE() {
return ExchangeBuilder.directExchange(EX_LEARNING_ADDCHOOSECOURSE).durable(true).build();
}
//声明队列
@Bean(XC_LEARNING_FINISHADDCHOOSECOURSE)
public Queue QUEUE_XC_LEARNING_FINISHADDCHOOSECOURSE() {
return new Queue(XC_LEARNING_FINISHADDCHOOSECOURSE);
}
//声明队列
@Bean(XC_LEARNING_ADDCHOOSECOURSE)
public Queue QUEUE_XC_LEARNING_ADDCHOOSECOURSE() {
return new Queue(XC_LEARNING_ADDCHOOSECOURSE);
}
/**
* 绑定队列到交换机
*
* @param queue the queue
* @param exchange the exchange
* @return the binding
*/
@Bean
public Binding BINDING_QUEUE_FINISHADDCHOOSECOURSE(@Qualifier(XC_LEARNING_FINISHADDCHOOSECOURSE) Queue queue,
@Qualifier(EX_LEARNING_ADDCHOOSECOURSE) Exchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with(XC_LEARNING_FINISHADDCHOOSECOURSE_KEY).noargs();
}
@Bean
public Binding BINDING_QUEUE_ADDCHOOSECOURSE(@Qualifier(XC_LEARNING_ADDCHOOSECOURSE) Queue queue,
@Qualifier(EX_LEARNING_ADDCHOOSECOURSE) Exchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with(XC_LEARNING_ADDCHOOSECOURSE_KEY).noargs();
}
}
编写XcTaskRepository
package com.xuecheng.order.dao;
import com.xuecheng.framework.domain.task.XcTask;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.Date;
public interface XcTaskRepository extends JpaRepository<XcTask, String> {
Page<XcTask> findByUpdateTimeBefore(Date updateTime, Pageable pageable);
//更新任务处理时间
@Modifying
@Query("update XcTask t set t.updateTime = :updateTime where t.id = :id ")
int updateTaskTime(@Param(value = "id") String id, @Param(value = "updateTime") Date updateTime);
}
编写TaskService
package com.xuecheng.order.service;
import com.xuecheng.framework.domain.task.XcTask;
import com.xuecheng.order.dao.XcTaskRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
import javax.transaction.Transactional;
import java.util.Date;
import java.util.List;
import java.util.Optional;
@Slf4j
@Service
public class TaskService {
@Autowired
private XcTaskRepository xcTaskRepository;
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 查询指定时间的前 N 条任务
*
* @param updateTime 时间
* @param n 前N条记录
* @return task list
*/
public List<XcTask> findTaskList(Date updateTime, int n) {
Page<XcTask> taskPage = xcTaskRepository.findByUpdateTimeBefore(updateTime, PageRequest.of(0, n));
return taskPage.getContent();
}
/**
* 发送消息
*
* @param xcTask 任务对象
* @param ex 交换机id
* @param routingKey 路由key
*/
@Transactional
public void publish(XcTask xcTask, String ex, String routingKey) {
//查询任务
Optional<XcTask> taskOptional = xcTaskRepository.findById(xcTask.getId());
if (taskOptional.isPresent()) {
XcTask task = taskOptional.get();
//String exchange, String routingKey, Object object
rabbitTemplate.convertAndSend(ex, routingKey, task);
//更新任务时间为当前时间
task.setUpdateTime(new Date());
xcTaskRepository.save(task);
}
}
}
编写任务类
package com.xuecheng.order.task;
import com.xuecheng.framework.domain.task.XcTask;
import com.xuecheng.order.service.TaskService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
/**
* 定时发送消息到MQ
*/
@Slf4j
@Component
public class ChooseCourseTask {
@Autowired
private TaskService taskService;
/**
* 每分钟扫描消息表,发送到MQ
*/
@Scheduled(fixedDelay = 60000)
public void sendChooseCourseTask() {
Calendar calendar = new GregorianCalendar();
calendar.setTime(new Date());
calendar.add(GregorianCalendar.MINUTE, -1);
Date time = calendar.getTime();
List<XcTask> taskList = taskService.findTaskList(time, 1000);
// 发送消息
taskList.forEach(task -> {
taskService.publish(task, task.getMqExchange(), task.getMqRoutingkey());
log.info("[订单微服务] 发送选课消息到MQ, task id :[{}]", task.getId());
});
}
}
与订单服务
RabbitMQ
配置一致
选课dao
package com.xuecheng.learning.dao;
import com.xuecheng.framework.domain.learning.XcLearningCourse;
import org.springframework.data.jpa.repository.JpaRepository;
public interface XcLearningCourseRepository extends JpaRepository<XcLearningCourse, String> {
//根据用户和课程查询选课记录,用于判断是否添加选课
XcLearningCourse findXcLearningCourseByUserIdAndCourseId(String userId, String courseId);
}
历史任务dao
package com.xuecheng.learning.dao;
import com.xuecheng.framework.domain.task.XcTaskHis;
import org.springframework.data.jpa.repository.JpaRepository;
public interface XcTaskHisRepository extends JpaRepository<XcTaskHis,String> {
}
LearningService
新增方法
/**
* 添加选课记录
*
* @param userId 选课用户
* @param courseId 课程ID
* @param valid 是否有效
* @param startTime 开始时间
* @param endTime 结束时间
* @param xcTask 选课任务
* @return 选课结果
*/
@Transactional
public ResponseResult addcourse(String userId, String courseId, String valid,
Date startTime, Date endTime, XcTask xcTask) {
if (StringUtils.isEmpty(courseId)) {
ExceptionCast.cast(LearningCode.LEARNING_GETMEDIA_ERROR);
}
if (StringUtils.isEmpty(userId)) {
ExceptionCast.cast(LearningCode.CHOOSECOURSE_USERISNULL);
}
if (xcTask == null || StringUtils.isEmpty(xcTask.getId())) {
ExceptionCast.cast(LearningCode.CHOOSECOURSE_TASKISNULL);
}
//查询历史任务
Optional<XcTaskHis> optional = xcTaskHisRepository.findById(xcTask.getId());
if (optional.isPresent()) {
return new ResponseResult(CommonCode.SUCCESS);
}
XcLearningCourse xcLearningCourse = xcLearningCourseRepository.findXcLearningCourseByUserIdAndCourseId(userId, courseId);
if (xcLearningCourse == null) {//没有选课记录则添加
xcLearningCourse = new XcLearningCourse();
xcLearningCourse.setUserId(userId);
xcLearningCourse.setCourseId(courseId);
xcLearningCourse.setValid(valid);
xcLearningCourse.setStartTime(startTime);
xcLearningCourse.setEndTime(endTime);
xcLearningCourse.setStatus("501001");
xcLearningCourseRepository.save(xcLearningCourse);
} else {//有选课记录则更新日期
xcLearningCourse.setValid(valid);
xcLearningCourse.setStartTime(startTime);
xcLearningCourse.setEndTime(endTime);
xcLearningCourse.setStatus("501001");
xcLearningCourseRepository.save(xcLearningCourse);
}
//向历史任务表播入记录
Optional<XcTaskHis> optionalXcTaskHis = xcTaskHisRepository.findById(xcTask.getId());
if (!optionalXcTaskHis.isPresent()) {
//添加历史任务
XcTaskHis xcTaskHis = new XcTaskHis();
BeanUtils.copyProperties(xcTask, xcTaskHis);
xcTaskHisRepository.save(xcTaskHis);
}
return new ResponseResult(CommonCode.SUCCESS);
}
package com.xuecheng.learning.mq;
import com.alibaba.fastjson.JSON;
import com.xuecheng.framework.domain.task.XcTask;
import com.xuecheng.framework.model.response.ResponseResult;
import com.xuecheng.learning.config.RabbitMQConfig;
import com.xuecheng.learning.service.LearningService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
@Slf4j
@Component
public class ChooseCourseTask {
@Autowired
LearningService learningService;
@Autowired
RabbitTemplate rabbitTemplate;
/**
* 接收选课任务
*/
@RabbitListener(queues = {RabbitMQConfig.XC_LEARNING_ADDCHOOSECOURSE})
public void receiveChoosecourseTask(XcTask xcTask) throws IOException {
log.info("receive choose course task,taskId:{}", xcTask.getId());
//接收到 的消息id
String id = xcTask.getId();
//添加选课
try {
String requestBody = xcTask.getRequestBody();
Map map = JSON.parseObject(requestBody, Map.class);
String userId = (String) map.get("userId");
String courseId = (String) map.get("courseId");
String valid = (String) map.get("valid");
Date startTime = null;
Date endTime = null;
SimpleDateFormat dateFormat = new SimpleDateFormat("YYYY‐MM‐dd HH:mm:ss");
if (map.get("startTime") != null) {
startTime = dateFormat.parse((String) map.get("startTime"));
}
if (map.get("endTime") != null) {
endTime = dateFormat.parse((String) map.get("endTime"));
}
//添加选课
ResponseResult addcourse = learningService.addcourse(userId, courseId, valid, startTime, endTime, xcTask);
//选课成功发送响应消息
if (addcourse.isSuccess()) {
//发送响应消息
rabbitTemplate.convertAndSend(RabbitMQConfig.EX_LEARNING_ADDCHOOSECOURSE, RabbitMQConfig.XC_LEARNING_FINISHADDCHOOSECOURSE_KEY, xcTask);
log.info("send finish choose course taskId:{}", id);
}
} catch (Exception e) {
e.printStackTrace();
log.error("send finish choose course taskId:{}", id);
}
}
}
定义XcTaskHisRepository
package com.xuecheng.order.dao;
import com.xuecheng.framework.domain.task.XcTaskHis;
import org.springframework.data.jpa.repository.JpaRepository;
public interface XcTaskHisRepository extends JpaRepository<XcTaskHis, String> {
}
TaskService
中新增方法
@Autowired
private XcTaskHisRepository xcTaskHisRepository;
/**
* 删除指定ID的任务并添加到历史任务中
*
* @param taskId 任务id
*/
@Transactional
public void finishTask(String taskId) {
Optional<XcTask> taskOptional = xcTaskRepository.findById(taskId);
if (taskOptional.isPresent()) {
XcTask xcTask = taskOptional.get();
xcTask.setDeleteTime(new Date());
XcTaskHis xcTaskHis = new XcTaskHis();
BeanUtils.copyProperties(xcTask, xcTaskHis);
xcTaskHisRepository.save(xcTaskHis);
xcTaskRepository.delete(xcTask);
}
}
修改ChooseCourseTask
,新增消息监听器方法
/**
* 接收选课响应结果
*/
@RabbitListener(queues = {RabbitMQConfig.XC_LEARNING_FINISHADDCHOOSECOURSE})
public void receiveFinishChoosecourseTask(XcTask task) throws IOException {
log.info("receiveChoosecourseTask...{}",task.getId());
//接收到 的消息id
String id = task.getId();
//删除任务,添加历史任务
taskService.finishTask(id);
}
这个项目算是基本做完了,后面还一天的课程是关于DevOps
的,不打算做了,也不打算写笔记了,之前做十次方的时候,DevOps
基本上做了一整遍了,大致砍了,基本上没什么变化。
呼~~~,终于做完了。后面应该会出一片文章,文章内可以下载到我的代码和项目相关的其他资源。
下一个学习目标的话,目前我有几个打算: