分布式事务与 RabbitMQ, 项目完结感言
分布式事务
概述
什么是分布式系统
部署在不同结点上的系统通过网络交互来完成协同工作的系统。
比如:充值加积分的业务,用户在充值系统向自己的账户充钱,在积分系统中自己积分相应的增加。充值系统和积分系统是两个不同的系统,一次充值加积分的业务就需要这两个系统协同工作来完成。
什么是事务
事务是指由一组操作组成的一个工作单元,这个工作单元具有原子性(atomicity)、一致性(consistency)、隔离性(isolation)和持久性(durability)。
原子性:执行单元中的操作要么全部执行成功,要么全部失败。如果有一部分成功一部分失败那么成功的操作要全部回滚到执行前的状态。一致性:执行一次事务会使用数据从一个正确的状态转换到另一个正确的状态,执行前后数据都是完整的。 隔离性:在该事务执行的过程中,任何数据的改变只存在于该事务之中,对外界没有影响,事务与事务之间是完全的隔离的。只有事务提交后其它事务才可以查询到最新的数据。 持久性:事务完成后对数据的改变会永久性的存储起来,即使发生断电宕机数据依然在。
什么是本地事务
本地事务就是用关系数据库来控制事务,关系数据库通常都具有ACID特性,传统的单体应用通常会将数据全部储在一个数据库中,会借助关系数据库来完成事务控制。
什么是分布式事务
在分布式系统中一次操作由多个系统协同完成,这种一次事务操作涉及多个系统通过网络协同完成的过程称为分布式事务。这里强调的是多个系统通过网络协同完成一个事务的过程,并不强调多个系统访问了不同的数据库,即使多个系统访问的是同一个数据库也是分布式事务,如下图:
另外一种分布式事务的表现是,一个应用程序使用了多个数据源连接了不同的数据库,当一次事务需要操作多个数据源,此时也属于分布式事务,当系统作了数据库拆分后会出现此种情况。
分布式场景例子
电商系统中的下单扣库存
电商系统中,订单系统和库存系统是两个系统,一次下单的操作由两个系统协同完成
金融系统中的银行卡充值
在金融系统中通过银行卡向平台充值需要通过银行系统和金融系统协同完成。
教育系统中下单选课业务
在线教育系统中,用户购买课程,下单支付成功后学生选课成功,此事务由订单系统和选课系统协同完成。
SNS系统的消息发送
在社交系统中发送站内消息同时发送手机短信,一次消息发送由站内消息系统和手机通信系统协同完成。
CAP理论
如何进行分布式事务控制?CAP
理论是分布式事务处理的理论基础,了解了CAP
理论有助于我们研究分布式事务的处理方案。
CAP
理论是:分布式系统在设计时只能在一致性(Consistency)
、可用性(Availability)
、分区容忍性(PartitionTolerance)
中满足两种,无法兼顾三种。
一致性(Consistency)
:服务A、B、C三个结点都存储了用户数据, 三个结点的数据需要保持同一时刻数据一致性。
可用性(Availability)
:服务A、B、C三个结点,其中一个结点宕机不影响整个集群对外提供服务,如果只有服务A结点,当服务A宕机整个系统将无法提供服务,增加服务B、C是为了保证系统的可用性。
分区容忍性(Partition Tolerance)
:分区容忍性就是允许系统通过网络协同工作,分区容忍性要解决由于网络分区导致数据的不完整及无法访问等问题。
分布式系统不可避免的出现了多个系统通过网络协同工作的场景,结点之间难免会出现网络中断、网延延迟等现象,这种现象一旦出现就导致数据被分散在不同的结点上,这就是网络分区。
分布式系统能否兼顾C、A、P?
在保证分区容忍性的前提下一致性和可用性无法兼顾,如果要提高系统的可用性就要增加多个结点,如果要保证数据的一致性就要实现每个结点的数据一致,结点越多可用性越好,但是数据一致性越差。所以,在进行分布式系统设计时,同时满足“一致性”、“可用性”和“分区容忍性”三者是几乎不可能的。
CAP有哪些组合方式?
- CA:放弃分区容忍性,加强一致性和可用性,关系数据库按照CA进行设计。
- AP:放弃一致性,加强可用性和分区容忍性,追求最终一致性,很多NoSQL数据库按照AP进行设计。
说明:这里放弃一致性是指放弃强一致性,强一致性就是写入成功立刻要查询出最新数据。追求最终一致性是指允许暂时的数据不一致,只要最终在用户接受的时间内数据 一致即可。
- CP:放弃可用性,加强一致性和分区容忍性,一些强一致性要求的系统按CP进行设计,比如跨行转账,一次转账请求要等待双方银行系统都完成整个事务才算完成。
分布式事务解决方案
两阶段提交(2PC)
为解决分布式系统的数据一致性问题出现了两阶段提交协议(2 Phase Commitment Protocol),两阶段提交由
协调者和参与者组成,共经过两个阶段和三个操作,部分关系数据库如Oracle、MySQL支持两阶段提交协议,本节讲解关系数据库两阶段提交协议。
第一阶段:准备阶段(prepare)
协调者通知参与者准备提交订单,参与者开始投票。协调者完成准备工作向协调者回应Yes。
第二阶段:提交(commit)/回滚(rollback)阶段
协调者根据参与者的投票结果发起最终的提交指令。如果有参与者没有准备好则发起回滚指令。
2PC
示例:以下单为例子
- 应用程序连接两个数据源。
- 应用程序通过事务协调器向两个库发起
prepare
,两个数据库收到消息分别执行本地事务(记录日志),但不提交,如果执行成功则回复yes,否则回复no。
- 事务协调器收到回复,只要有一方回复no则分别向参与者发起回滚事务,参与者开始回滚事务。
- 事务协调器收到回复,全部回复yes,此时向参与者发起提交事务。如果参与者有一方提交事务失败则由事务协调器发起回滚事务。
优点:实现强一致性,部分关系数据库支持(Oracle、MySQL等)。
缺点:整个事务的执行需要由协调者在多个节点之间去协调,增加了事务的执行时间,性能低下。
解决方案有:springboot
+ Atomikos
/ Bitronix
事务补偿(TCC)
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通知库存服务执行减库存操作。
- 库存服务执行减少库存,并且记录执行消息状态(为避免重复执行消息,在执行减库存之前查询是否执行过此消息)。
- 库存服务向MQ发送完成减少库存的消息。
- 订单服务接收到完成库存减少的消息后删除原来添加的“减少库存任务消息”。
实现最终事务一致要求:预留资源成功理论上要求正式执行成功,如果执行失败会进行重试,要求业务执行方法实现幂等。
优点 :由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
注解
并行任务
创建异步任务配置类,配置线程池实现任务的并行执行
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
| 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
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配置
配置RabbitMQ
的queue
和exchange
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
| 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";
public static final String XC_LEARNING_ADDCHOOSECOURSE_KEY = "addchoosecourse";
public static final String XC_LEARNING_FINISHADDCHOOSECOURSE_KEY = "finishaddchoosecourse";
@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); }
@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(); }
}
|
查询待执行任务列表
dao
编写XcTaskRepository
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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); }
|
service
编写TaskService
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
| 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;
public List<XcTask> findTaskList(Date updateTime, int n) { Page<XcTask> taskPage = xcTaskRepository.findByUpdateTimeBefore(updateTime, PageRequest.of(0, n)); return taskPage.getContent(); }
@Transactional public void publish(XcTask xcTask, String ex, String routingKey) { Optional<XcTask> taskOptional = xcTaskRepository.findById(xcTask.getId()); if (taskOptional.isPresent()) { XcTask task = taskOptional.get(); rabbitTemplate.convertAndSend(ex, routingKey, task); task.setUpdateTime(new Date()); xcTaskRepository.save(task); } }
}
|
task
编写任务类
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
| 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;
@Slf4j @Component public class ChooseCourseTask {
@Autowired private TaskService taskService;
@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配置
与订单服务RabbitMQ
配置一致
dao
选课dao
1 2 3 4 5 6 7 8 9
| 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
1 2 3 4 5 6 7
| 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> { }
|
service
LearningService
新增方法
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
|
@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); }
|
task
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
| 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()); 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); } } }
|
订单服务结束任务
dao
定义XcTaskHisRepository
1 2 3 4 5 6 7
| 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> { }
|
service
TaskService
中新增方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @Autowired private XcTaskHisRepository xcTaskHisRepository;
@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); } }
|
task
修改ChooseCourseTask
,新增消息监听器方法
1 2 3 4 5 6 7 8 9 10 11
|
@RabbitListener(queues = {RabbitMQConfig.XC_LEARNING_FINISHADDCHOOSECOURSE}) public void receiveFinishChoosecourseTask(XcTask task) throws IOException { log.info("receiveChoosecourseTask...{}",task.getId()); String id = task.getId(); taskService.finishTask(id); }
|
代码获取
代码获取
完结
这个项目算是基本做完了,后面还一天的课程是关于DevOps
的,不打算做了,也不打算写笔记了,之前做十次方的时候,DevOps
基本上做了一整遍了,大致砍了,基本上没什么变化。
呼~~~,终于做完了。后面应该会出一片文章,文章内可以下载到我的代码和项目相关的其他资源。
下一个学习目标的话,目前我有几个打算:
补一下数据结构和算法
因为之前大学,这块没好好学
尚硅谷:谷粒商城
技术比较新,而且好像知识面涉及的挺全的,虽然商城项目做腻了,但是还想看看。
技术涵盖:微服务架构+分布式+全栈+集群+部署+自动化运维+可视化CICD
JVM、源码、分布式理论、设计模式详解等……
更深层次的内容,很有必要。
大数据
后面有转大数据的想法,emmm,不知道,到时候再看吧,还有很多东西没学。哎!谁叫自己懒呢。