git:https://github.com/Lmdawn11/12306
0.1常见高并发场景
- 商品秒杀
- 微信支付宝平台
- 微博突发热点
- 用户操作日志
- 12306抢票
之所以选择12306的原因是下列原因,解决下列问题培养高并发思维
业务复杂度
-
动态库存(车票):北京--南京--上海 ,此时如果北京--南京,南京--上海的票不受影响
-
选座功能:
-
线上线下并行
持续高并发
-
不停的刷票
-
绝不超卖
0.2 通用优化方案
前端优化
-
静态资源CDN
-
页面静态化
-
倒计时&Loading
-
使用验证码削峰
后端优化
-
微服务-服务拆分-按功能拆分
-
负载均衡
-
限流降级
-
缓存
-
令牌
-
异步处理
数据库
-
分库:业务分库、读写分离
-
分表:横向分表(按地区或时间来划分,比如1月份一张表、二月份一张表)、
-
纵向分表(比如普通字段一张表,大字段一张表)
-
冗余设计,反范式,空间换时间
-
分布式数据库
其它方案
-
分时段秒杀
-
弹性扩容
-
候补+排队
1.系统架构
1.1 系统用户架构
从系统使用用户分析来看,主要有普通用户和管理员。普通用户的功能主要有登录、乘客管理、余票查询、车票购买、我的车票;管理员的功能有基础车次维护、每日车次维护、用户管理、车票管理。具体如下图所示:
1.2 系统功能架构
根据系统的用户、设计了系统的主要功能:用户模块、业务模块、定时任务模块,结合微服务和高可用设计,设计系统的主要功能模块如下:
1.3 系统总体架构
最后系统架构如下所示:
上述主要为web和admin端提供服务支持,通过gateway网关转发前端传的请求到后端对应模块,提取公共的模块组成common,通过自定义依赖的方式注入相关服务中,通过代码生成器的方式生成CRUD代码快速开发。使用redis缓存、rocketmq消息队列、nacos注册中心、seats分布式事务、sentinel服务保护保证系统的持续高可用、数据库分库分表。
2.数据库设计
数据库的设计也分为两个设计模块:用户模块和业务模块
2.1用户模块:
- 会员表:手机号
- 乘客表:会员ID,姓名,身份证,旅客类型
- 车票表:会员ID,乘客ID,乘客姓名,日期,车次信息,座位信息
2.2业务模块:
- 车站表:站名,站名拼音(方便查询)
- 车次表:车次编号,车次类型,始发站,出发时间,终点站,到站时间(车次)
- 到站表:车次编号,站名,进站时间,出站时间,停站时长,里程(上下车时间站点)
- 车箱表:车次编号,箱号,座位类型,座位数,排数,列数(具体信息)
- 座位表:车次编号,箱号,排号,列号(详细)
- 每日车次表:日期,基础车次信息(定时任务更新)
- 每日到站表:日期,基础到站信息
- 每日车箱表:日期,基础车箱信息
- 每日座位表:日期,基础座位信息,销售详情
- 每日余票表:日期,车次编号,出发站,出发时间,到达站,到站时间,各种座位的余票信息
2.3其它
除了普通的数据库设计之外 ,还有定时任务和分布式事务相关表,待后续详细说明
- quartz相关表(执行定时任务每日更新等操作)
- seata相关表
3. 实现
本项目遵循循序渐进的原则,先把框架和基本配置搭建好,在开发集成。
3.1 父项目
首先创建一个Maven的parent项目
需要导入的依赖有如下所示:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
将它们放到pom的<dependencyManagement>下,用于统一管理包的版本,防止依赖冲突。
3.2 通用配置
3.2.1 logback日志
logback更快、更灵活、支持多线程打印输出
由于各个微服务都有日志要输出,需要合理保存日志数据
<property name="PATH" value="./log/gateway"></property>
在各个微服务中引入logback,修改上述value内容,如果你是member模块,则修改成./log/gateway即可
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 修改一下路径-->
<property name="PATH" value="./log/gateway"></property>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!-- <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) %blue(%-50logger{50}:%-4line) %thread %green(%-18X{LOG_ID}) %msg%n</Pattern>-->
<Pattern>%d{mm:ss.SSS} %highlight(%-5level) %blue(%-30logger{30}:%-4line) %thread %green(%-18X{LOG_ID}) %msg%n</Pattern>
</encoder>
</appender>
<appender name="TRACE_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${PATH}/trace.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${PATH}/trace.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<layout>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %-50logger{50}:%-4line %green(%-18X{LOG_ID}) %msg%n</pattern>
</layout>
</appender>
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${PATH}/error.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${PATH}/error.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<layout>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %-50logger{50}:%-4line %green(%-18X{LOG_ID}) %msg%n</pattern>
</layout>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<root level="ERROR">
<appender-ref ref="ERROR_FILE" />
</root>
<root level="TRACE">
<appender-ref ref="TRACE_FILE" />
</root>
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</configuration>
定义了正常日志输出为控制台输出方式,error和trace输出为文件方式,用于运维使用
3.2.2 请求AOP
由于想知道前端请求后端的某个具体方法,使用aop的实现,在调用controller中的路径方法时,使用controller前、后打印日志输出。
具体实现就是创建一个切面类,定义切点和实现方法
如下所示:
@Aspect
@Component
public class LogAspect {
public LogAspect() {
System.out.println("LogAspect");
}
private final static Logger LOG = LoggerFactory.getLogger(LogAspect.class);
/**
* 定义一个切点
*/
@Pointcut("execution(public * com.ming..*Controller.*(..))")
public void controllerPointcut() {
}
@Before("controllerPointcut()")
public void doBefore(JoinPoint joinPoint) {
MDC.put("LOG_ID",System.currentTimeMillis() + RandomUtil.randomString(3));
// 开始打印请求日志
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
Signature signature = joinPoint.getSignature();
String name = signature.getName();
// 打印请求信息
LOG.info("------------- 开始 -------------");
LOG.info("请求地址: {} {}", request.getRequestURL().toString(), request.getMethod());
LOG.info("类名方法: {}.{}", signature.getDeclaringTypeName(), name);
LOG.info("远程地址: {}", request.getRemoteAddr());
// 打印请求参数
Object[] args = joinPoint.getArgs();
// LOG.info("请求参数: {}", JSONObject.toJSONString(args));
// 排除特殊类型的参数,如文件类型
Object[] arguments = new Object[args.length];
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof ServletRequest
|| args[i] instanceof ServletResponse
|| args[i] instanceof MultipartFile) {
continue;
}
arguments[i] = args[i];
}
// 排除字段,敏感字段或太长的字段不显示:身份证、手机号、邮箱、密码等
String[] excludeProperties = {};
PropertyPreFilters filters = new PropertyPreFilters();
PropertyPreFilters.MySimplePropertyPreFilter excludefilter = filters.addFilter();
excludefilter.addExcludes(excludeProperties);
LOG.info("请求参数: {}", JSONObject.toJSONString(arguments, excludefilter));
}
@Around("controllerPointcut()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = proceedingJoinPoint.proceed();
// 排除字段,敏感字段或太长的字段不显示:身份证、手机号、邮箱、密码等
String[] excludeProperties = {};
PropertyPreFilters filters = new PropertyPreFilters();
PropertyPreFilters.MySimplePropertyPreFilter excludefilter = filters.addFilter();
excludefilter.addExcludes(excludeProperties);
LOG.info("返回结果: {}", JSONObject.toJSONString(result, excludefilter));
LOG.info("------------- 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime);
return result;
}
}
由于这是一个通用的切面类,可以放在common服务中,引入到各个微服务中。
3.2.3 配置数据库
先测试用户的数据库,调试代码,mybaits,自动生成持久层代码和实体类,最后将结果用于全局。
引入依赖
<!-- 集成mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- 集成mysql连接 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
再配置文件中配置 相关信息
server:
port: 8001
servlet:
context-path: /member
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/train_member?characterEncoding=UTF8&autoReconnect=true&serverTimezone=Asia/Shanghai
username: train_member
password: train
# mybatis xml路径
mybatis:
mapper-locations: classpath:/mapper/**/*.xml
logging:
level:
com.ming.train.member.mapper: trace
这样数据库即可连接成功。
3.3 代码生成器generator
由于代码生成器的特殊性,只执行一次,所以单独抽象出来作为一个服务进行配置
之后还需要配置持久层和实体层代码自动生成的话,需要引入插件到pom中,内容如下
<build>
<plugins>
<!-- mybatis generator 自动生成代码插件 -->
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.4.0</version>
<configuration>
<configurationFile>src/main/resources/generator-config-member.xml</configurationFile>
<!-- <configurationFile>src/main/resources/generator-config-business.xml</configurationFile>-->
<!-- <configurationFile>src/main/resources/generator-config-batch.xml</configurationFile>-->
<overwrite>true</overwrite>
<verbose>true</verbose>
</configuration>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
需要注意的是,插件中的<configuration>标签需要再路径上配置好自己的微服务xml文件
如下图所示
而generator的内容如下:内容详细说明可看注释
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<context id="Mysql" targetRuntime="MyBatis3" defaultModelType="flat">
<!-- 自动检查关键字,为关键字增加反引号 -->
<property name="autoDelimitKeywords" value="true"/>
<property name="beginningDelimiter" value="`"/>
<property name="endingDelimiter" value="`"/>
<!--覆盖生成XML文件-->
<plugin type="org.mybatis.generator.plugins.UnmergeableXmlMappersPlugin" />
<!-- 生成的实体类添加toString()方法 -->
<plugin type="org.mybatis.generator.plugins.ToStringPlugin"/>
<!-- 不生成注释 -->
<commentGenerator>
<property name="suppressAllComments" value="true"/>
</commentGenerator>
<!-- 配置数据源,需要根据自己的项目修改 -->
<jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
connectionURL="jdbc:mysql://localhost:3306/train_member?serverTimezone=Asia/Shanghai"
userId="train_member"
password="train">
</jdbcConnection>
<!-- domain类的位置 targetProject是相对pom.xml的路径-->
<javaModelGenerator targetProject="../member/src/main/java"
targetPackage="com.ming.train.member.domain"/>
<!-- mapper xml的位置 targetProject是相对pom.xml的路径 -->
<sqlMapGenerator targetProject="../member/src/main/resources"
targetPackage="mapper"/>
<!-- mapper类的位置 targetProject是相对pom.xml的路径 -->
<javaClientGenerator targetProject="../member/src/main/java"
targetPackage="com.ming.train.member.mapper"
type="XMLMAPPER"/>
<table tableName="member" domainObjectName="Member"/>
<!-- <table tableName="passenger" domainObjectName="Passenger"/>-->
<!-- <table tableName="ticket" domainObjectName="Ticket"/>-->
</context>
</generatorConfiguration>
此时 点击maven的generate服务的插件,
双击mybaits-generator插件即可,自动生成代码,根据上述配置类,会生成domain实体类和example类(由于配置查询条件 =可以理解为wrapper),resource包下的mapper的xml文件
默认生成的内容是不改变的。
3.3 配置网关服务
目前使用网关的路由转发功能,由网关的的端口映射到后端的端口。保证了后端安全性
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
引入依赖,
然后再yml配置文件中,配置网关服务的端口,和需要映射的服务,解释可看注释
server:
port: 8000
# 路由转发,将/member/...的请求转发了member模块
spring:
cloud:
gateway:
routes:
- id: member
predicates:
- Path=/member/** #所有的/member/**开头的请求都会被转发到下列uri
uri: http://127.0.0.1:8001
这样配置之后,当我们启动8001端口下的服务和gateway服务时,可以通过网关的方式访问到8001端口下的资源。
4.其它技术点实现
4.1 JWT实现