12306简单设计和实现

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实现

链接:JWT单点登录-CSDN博客

        

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值