JavaEE企业级实战项目 智牛股第五天 Netty的使用和项目数据库搭建

交易平台 - Day 5

学习目标

目标1:掌握Netty的使用

目标2:数据库设计规范

目标3:数据库建模理论

目标4:PowerDesigner使用

目标5:数据库调优

目标6:数据库设计之交易模块

第1章 Netty使用

1. 目标

  • 掌握Netty在实际项目当中的集成使用

2. 步骤

  • 工程设计
  • 工程结构
  • 服务端工程实现
  • 客户端工程实现
  • 功能演示与验证

3. 实现

3.1 工程设计

JavaEE企业级实战项目 智牛股第五天 Netty的使用和项目数据库搭建_第1张图片 建立两个工程, 通讯服务端与通讯客户端, 可以相互发送消息, 做个简易聊天演示。

3.2 工程结构

JavaEE企业级实战项目 智牛股第五天 Netty的使用和项目数据库搭建_第2张图片 工程依赖:

   
        netty-server
        netty-client
    

    
        
        
            io.netty
            netty-all
            4.1.16.Final
        

    

3.3 服务端工程

  1. 服务端工程NettyServer
  • 接收客户请求, 并打印客户端发送的消息
  • 消息采用内置String作为编码与解码器
  • 开启信息输入监听线程, 发送消息至客户端
  1. NettyProviderServer类:

    public class NettyProviderServer {
    
    
    
    
    
    
    
        private int port;
    
    
    
        public NettyProviderServer (int port){
    
    
    
            this.port = port;
    
    
    
        }
    
    
    
    
    
    
    
        // netty 服务端启动
    
    
    
        public void runServer() throws Exception{
    
    
    
    
    
    
    
            // 用来接收进来的连接
    
    
    
            EventLoopGroup bossGroup = new NioEventLoopGroup();
    
    
    
            // 用来处理已经被接收的连接,一旦bossGroup接收到连接,就会把连接信息注册到workerGroup上
    
    
    
            EventLoopGroup workerGroup = new NioEventLoopGroup();
    
    
    
    
    
    
    
            try {
    
    
    
                // nio服务的启动类
    
    
    
                ServerBootstrap sbs = new ServerBootstrap();
    
    
    
                // 配置nio服务参数
    
    
    
                sbs.group(bossGroup, workerGroup)
    
    
    
                        .channel(NioServerSocketChannel.class) // 说明一个新的Channel如何接收进来的连接
    
    
    
                        .option(ChannelOption.SO_BACKLOG, 128) // tcp最大缓存链接个数,它是tcp的参数, tcp_max_syn_backlog(半连接上限数量, CENTOS6.5默认是128)
    
    
    
                        .childOption(ChannelOption.SO_KEEPALIVE, true) //保持连接
    
    
    
                        .handler(new LoggingHandler(LogLevel.INFO)) // 打印日志级别
    
    
    
                        .childHandler(new ChannelInitializer() {
    
    
    
                            @Override
    
    
    
                            protected void initChannel(SocketChannel socketChannel) throws Exception {
    
    
    
    
    
    
    
                                //管道注册handler
    
    
    
                                ChannelPipeline pipeline = socketChannel.pipeline();
    
    
    
                                //编码通道处理
    
    
    
                                pipeline.addLast("decode", new StringDecoder());
    
    
    
                                //转码通道处理
    
    
    
                                pipeline.addLast("encode", new StringEncoder());
    
    
    
    
    
    
    
                                // 处理接收到的请求
    
    
    
                                pipeline.addLast(new NettyServerHandler()); // 这里相当于过滤器,可以配置多个
    
    
    
                            }
    
    
    
                        });
    
    
    
    
    
    
    
                System.err.println("-------server 启动------");
    
    
    
    
    
    
    
                // 监听输入端消息并发送给所有客户端
    
    
    
                new Thread(new Runnable() {
    
    
    
                    @Override
    
    
    
                    public void run() {
    
    
    
                        try {
    
    
    
                            while (true) {
    
    
    
                                BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
    
    
    
                                String str = in.readLine();
    
    
    
                                if (NettyServerHandler.channelList.size() > 0) {
    
    
    
                                    for (Channel channel : NettyServerHandler.channelList) {
    
    
    
                                        channel.writeAndFlush(str);
    
    
    
                                    }
    
    
    
                                }
    
    
    
                            }
    
    
    
                        }catch(Exception e) {
    
    
    
                            e.printStackTrace();
    
    
    
                        }
    
    
    
                    }
    
    
    
                }).start();
    
    
    
    
    
    
    
                // 绑定端口,开始接受链接
    
    
    
                ChannelFuture cf = sbs.bind(port).sync();
    
    
    
                cf.channel().closeFuture().sync();
    
    
    
    
    
    
    
            } finally{
    
    
    
                bossGroup.shutdownGracefully();
    
    
    
                workerGroup.shutdownGracefully();
    
    
    
            }
    
    
    
        }
    
    
    
    
    
    
    
        // 开启netty服务线程
    
    
    
        public static void main(String[] args) throws Exception {
    
    
    
            new NettyProviderServer(9911).runServer();
    
    
    
        }
    
    
    
    
    
    
    
    }
    

    服务端监听9911端口, 客户端连接时需指定此端口。

    public class NettyServerHandler extends ChannelInboundHandlerAdapter {
    
    public static List channelList = new ArrayList();
    
    
    
    
    
    
    
    @Override
    
    
    
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
    
    
    
        super.channelActive(ctx);
    
    
    
        channelList.add(ctx.channel());
    
    
    
    }
    
    
    
    
    
    
    
    // 读取数据
    
    
    
    @Override
    
    
    
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    
    
    
        System.out.println("server--收到消息: " + msg);
    
    
    
    }
    
    
    
    
    
    
    
    
    
    
    
    // 出现异常的处理
    
    
    
    @Override
    
    
    
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
    
    
    
        System.err.println("server--读取数据出现异常:");
    
    
    
        cause.printStackTrace();
    
    
    
        ctx.close();
    
    
    
    }
    

    }

    
    处理channel所接收的消息。
    
    

3.4 客户端工程

  1. 客户端工程NettyClient
  • 发起请求, 与服务端建立连接。
  • 监听服务端下发消息, 并将信息打印出来。
  • 开启信息输入监听线程, 将消息发送至服务端。
  1. NettyClientServer类

    public class NettyClientServer {
    
    
    
    
    
    
    
        // 要请求的服务器的ip地址
    
    
    
        private String ip;
    
    
    
        // 服务器的端口
    
    
    
        private int port;
    
    
    
    
    
    
    
        public NettyClientServer(String ip, int port){
    
    
    
            this.ip = ip;
    
    
    
            this.port = port;
    
    
    
        }
    
    
    
    
    
    
    
        // 启动服务
    
    
    
        private void runServer() throws Exception {
    
    
    
    
    
    
    
            EventLoopGroup bossGroup = new NioEventLoopGroup();
    
    
    
    
    
    
    
            Bootstrap bs = new Bootstrap();
    
    
    
    
    
    
    
            bs.group(bossGroup)
    
    
    
                    .channel(NioSocketChannel.class)
    
    
    
                    .option(ChannelOption.SO_KEEPALIVE, true)
    
    
    
                    .handler(new ChannelInitializer() {
    
    
    
                        @Override
    
    
    
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
    
    
    
                            //管道注册handler
    
    
    
                            ChannelPipeline pipeline = socketChannel.pipeline();
    
    
    
                            //编码通道处理
    
    
    
                            pipeline.addLast("decode", new StringDecoder());
    
    
    
                            //转码通道处理
    
    
    
                            pipeline.addLast("encode", new StringEncoder());
    
    
    
    
    
    
    
                            // 处理来自服务端的响应信息
    
    
    
                            socketChannel.pipeline().addLast(new NettyClientHandler());
    
    
    
                        }
    
    
    
                    });
    
    
    
            System.out.println("-------client 启动------");
    
    
    
            // 客户端开启
    
    
    
            ChannelFuture cf = bs.connect(ip, port).sync();
    
    
    
    
    
    
    
            String reqStr = "客户端发起连接请求";
    
    
    
    
    
    
    
            Channel channel = cf.channel();
    
    
    
    
    
    
    
            // 发送客户端的请求
    
    
    
            channel.writeAndFlush(reqStr);
    
    
    
    
    
    
    
            new Thread(new Runnable() {
    
    
    
                @Override
    
    
    
                public void run() {
    
    
    
                    try {
    
    
    
                        while(true) {
    
    
    
                            BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
    
    
    
                            String msg = in.readLine();
    
    
    
                            channel.writeAndFlush(msg);
    
    
    
                        }
    
    
    
                    }catch(Exception e) {
    
    
    
                        e.printStackTrace();
    
    
    
                    }
    
    
    
                }
    
    
    
            }).start();
    
    
    
    
    
    
    
    
    
    
    
        }
    
    
    
    
    
    
    
        public static void main(String[] args) throws Exception {
    
    
    
            new NettyClientServer("127.0.0.1", 9911).runServer();
    
    
    
        }
    
    
    
    }
    
    
    
    
    
  2. NettyClientHandler类

    public class NettyClientHandler extends ChannelInboundHandlerAdapter {
    
    public static Channel serverChannel = null;
    
    
    
    
    
    
    
    @Override
    
    
    
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
    
    
    
        super.channelActive(ctx);
    
    
    
        serverChannel = ctx.channel();
    
    
    
    }
    
    
    
    
    
    
    
    @Override
    
    
    
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    
    
    
        System.out.println("client--收到消息:" + msg);
    
    
    
    }
    
    
    
    
    
    
    
    // 出现异常的处理
    
    
    
    @Override
    
    
    
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
    
    
    
        System.err.println("client 读取数据出现异常");
    
    
    
        cause.printStackTrace();
    
    
    
        ctx.close();
    
    
    
    }
    

    }

    
    

3.5 工程演示

  1. 启动服务

    服务端: JavaEE企业级实战项目 智牛股第五天 Netty的使用和项目数据库搭建_第3张图片

    客户端: JavaEE企业级实战项目 智牛股第五天 Netty的使用和项目数据库搭建_第4张图片

  2. 发送验证

    客户端发送消息: 我是客户端client

    服务端发送消息: 我是服务端server

    查看结果:

    JavaEE企业级实战项目 智牛股第五天 Netty的使用和项目数据库搭建_第5张图片

    JavaEE企业级实战项目 智牛股第五天 Netty的使用和项目数据库搭建_第6张图片

    从结果可以看到, 服务端和客户端均能正常收发相互之间消息。

4. 总结

  • 理解服务端与客户端的工程配置, 关键实现代码, 编码与解码器的使用, 客户端发送消息与服务端接收消息的处理; 这里采用的String类型的内置编码器, 在实际工作中, 我们还可以自定义编码器,比如二进制数据结构, 头部(数据包长度,请求类型, CRC32校验等)与包体, 能够更灵活高效的封装处理数据。

第2章 数据库设计规范与模型

1. 目标

  • 了解数据库的设计规范
  • 如何去设计数据库, 遵循的原则
  • 掌握数据库的具体规范与设计原则
  • 了解数据库的模型分类,与各模型的概念和作用

2. 分析

  • 为什么需要规范
  • 如何去设计数据库
  • 数据库设计的具体规范
  • 数据库设计所遵循的原则
  • 数据库模型概念
  • 概念模型
  • 逻辑模型
  • 物理模型

3. 讲解

3.1 为什么需要规范

在互联网项目,分工明确,拆分细致, 但一个项目往往会涉及众多参与人员,包括架构师、高级工程师、研发工程师、测试工程师、DBA等不同角色。 数据库是一个项目的最底层设计, 如果设计不清晰,结构模糊, 规范混乱,是很难做成一个成功的项目,而这些需要我们从最基本的开始,对数据库有一套明确的设计规范。

3.2 如何设计

  1. 传统数据库设计往往会遵循设计范式(三范式), 随着不断发展, 范式设计往往难以满足现在项目的要求, 出现了反范式设计。

    • 范式设计:

      第一范式:数据库表中的每一列都是不可分割的基本数据项, 比如股票名称、股票代码。

      第二范式:数据库表所有属性都必须和主键有完全依赖关系, 比如用户编号为主键,那么与之依赖的有用户名、用户地址、用户性别和身份证等, 这些属性完全与具体用户产生依赖。

      第三范式:数据库表中属性不能存在传递关系, 比如A->B->C, 属性之间存在这样关系则不符合第三范式。再具体点, 比如股票表(股票编号, 股票名称,股票市场,股票交易手续费),这个表就不符合第三范式,因为股票交易手续费是依赖股票市场来确定, 需要再拆分为(股票编号, 股票名称)和(股票市场,股票交易手续费)多张表。

    • 反范式设计:

      范式设计虽然关系层次明确, 但是给业务查询带来了相当的复杂度,中间关系表膨胀, 查询需要连接更多的表, 导致性能严重下降。

      反范式并不是完全打破范式设计, 而是在范式设计与查询性能上找到平衡点,适合业务的设计才是好的设计。

  2. 在实际业务中, 如何去设计呢? 做好需求分析, 理解业务流程, 弄清业务概念, 技术是生产工具, 服务于业务才能体现其价值,所以明白业务需求才知道怎么去设计。 业务设计三步法则:

    • 先拆解: 哪些是业务系统的核心模块, 核心模块具体包含哪些对象, 哪些又是非核心模块, 必须先列清楚。
    • 画关系: 将拆解的模块串接起来,形成一个整体, 哪些是依赖关系, 哪些是包含关系,哪些是聚合, 哪些又是组合。
    • 再细化:有上面的清晰脉络, 再去填充对象的属性, 仔细考量, 适当加入反范式设计, 冗余一些常用信息,这些就需再仔细去细化, 斟酌与权衡。

3.3 命名与表设计规范

​ 规范并不是千遍一律, 但要在自己的项目体系内形成一套有效规范, 以下规范可作参考:

  1. 命名规范

    1.命名有意义,一眼知道这张表是干什么用的
    
    2.数据库,表都用小写
    
    数据库形如:backend
    
    数据表形如:client_device_info(客户端设备信息),不要缩写,字母全小写
    
    3.索引命名以idx_为前缀
    
    4.命名不要过长(应尽量少于25字符)
    
    5.不要使用保留字
    
    6.同一字段在不同的表中也应是相同的类型和长度
    
    7.同一数据库下有不同的模块,可以考虑对表名用不同的前缀标识
    
    8.备份表时加上时间标识
    
  2. 表设计规范

    1.如果没有特殊情况,建议选择InnoDB引擎
    
    2.每个表都应该有主键,可选择自增字段,或长整型字段。例外情况,如果有多个字段可以组成唯一, 并且需要频繁查询的, 那么可以采用复合主键,比如用户编号+交易商编号做为复合主键,因为可以确保不产生重复数据, 同时节省空间。 
    
    3.(不做强制要求)尽量将字段设置为NOT NULL。因为NULL值的存储需要额外的空间,且会导致比较运算更为复杂,会使得优化器更难以优化sql。空值是不占用空间, 但NULL列需要占用一个额外字节。
    
    4.使用更短小的列,比如整型列。整型列的执行速度往往更快。
    
    5.存储精确浮点数必须使用DECIMAL代替float和double。
    
    6.建议使用unsigned类型存储非负值
    
    7.建议使用 int unsigned存储ipv4
    
    8.整型定义中不添加显示长度的值,使用int,而不是int(4)
    
    9.尽可能不要使用text,blob类型
    
    10.varchar(n) n表示字符数而不是字节数,比如varchar(255)最大可存储255个汉字,需根据实际字符长度选择n的值。
    
    11.字符集建议选择utf-8
    
    12.存储年时使用year类型
    
    13.存储日期时使用date类型
    
    14.存储时间时,建议使用timestamp类型,因为timestamp使用的是4字节,datetime使用的是8字节。
    
    15.不要在数据库中使用varbinary或blob存储图片及文件,mysql 并不适合大量存储这类型文件
    
    16.join 操作的字段,在不同表中的类型及命名要一致
    
    17.如果更改表结构会影响性能,需要找DBA、表设计人员进行联合评审。
    

3.4 数据库设计原则

  1. 核心原则

    不在数据库做运算; cpu计算务必移至业务层;

    控制每个表的列数量(字段少而精,字段数建议在20以内);

    平衡范式与冗余(效率优先;往往牺牲范式)

    拒绝3B(拒绝大sql语句:big sql、拒绝大事物:big transaction、拒绝大批量:big batch);

  2. 字段类原则

    用好数值类型(用合适的字段类型节约空间);

    字符转化为数字(能转化的最好转化,同样节约空间、提高查询性能);

    避免使用NULL字段(NULL字段很难查询优化、NULL字段的索引需要额外空间、NULL字段的复合索引无效);

    少用text类型(尽量使用varchar代替text字段);

  3. 索引类原则

    合理使用索引(改善查询,减慢更新,索引一定不是越多越好);
    字符字段大量重复属性时, 尽量建前缀索引(比如地区chinaXXX, alter table x_test add index(x_city(6)) ); 不在索引做列运算;
    innodb主键推荐使用自增列(主键建立聚簇索引,主键不应该被修改,字符串不应该做主键) 不用外键(由程序保证约束);

  4. SQL类原则

    sql语句尽可能简单(一条sql只能在一个cpu运算,大语句拆小语句,减少锁时间,一条大sql可以堵死整个库);
    避免使用trig/func(触发器、函数, 采用程序取而代之);
    大批量SQL的更新操作要开启事务,越多效果性能越明显。

3.5 什么是数据库模型

数据模型是数据库的基础,数据是对客观事物的符号表示,模型是现实世界的抽象。数据模型是对数据特征的抽象。数据库模型主要包含概念数据模型、逻辑数据模型与物理数据模型。

数据库的类型是根据数据模型来划分的,而任何一个DBMS也是根据数据模型有针对性地设计出来的,这就意味着必须把数据库组织成符合DBMS规定的数据模型。目前成熟地应用在数据库系统中的逻辑数据模型有:层次模型(树状结构表示的数据模型)、网状模型(有向图表示)和关系模型(表格表示)。

3.6 数据库建模三步曲

概念模型->逻辑模型->物理模型

3.7 概念模型

概念模型是面向用户、面向现实世界的数据模型, 与DBMS无关,与业务相关,按用户的观点来对数据和信息建模。

了解用户的需求,用户的业务领域工作情况, 经过分析和总结,提炼出来的用以描述用户业务需求的一些概念的东西。例如金融交易市场中存在众多概念, 一个完整的交易,能够抽取出的概念有, 客户、 股票、订单、持仓等。

简单概括为 是什么

3.8 逻辑模型

逻辑模型就是要将概念模型具体化,就是要实现概念模型所描述的东西,需要哪些具体的功能,处理哪些具体的信息,是对概念模型的进一步细化。例如金融交易市场中的客户,信息包含: 客户名称, 客户账号,客户电话、客户地址等属性; 股票包含:股票代码, 股票名称, 股票市场,股票单位等; 订单包含客户账户, 股票代码,交易手数, 交易金额等。

这个阶段需要清楚系统要划分多少个模块, 按计算机系统的观点对数据建模,要建立多少个数据表, 每张表具体包含的属性特征, 主要用于DBMS的实现。

简单概括为 做什么

3.9 物理模型

物理模型是对真实数据库的描述, 对数据最低层的抽象, 描述数据在系统内部的表示方式和存取方法,是面向计算机系统的。如关系数据库中的一些对象: 表、视图、字段、数据类型、长度、主键、外键、索引、约束、是否可为空、默认值。

这个阶段需要采用数据库语言来描述真实世界的模型, 形成具体的SQL语句。

简单概括为 怎么做

4. 总结

  • 明白数据库设计为什么需要规范, 具体有哪些规范, 该如何设计,具体把握的要点, 理解掌握数据库设计的基本原则, 奠定良好的基础。
  • 数据库模型作为数据库设计的理论基础, 我们需要知道有哪些主要模型, 各模型的主要作用和应用场景, 在实际工作中能更好的去运用。

第3章 PowerDesigner使用

1. 目标

  • 掌握PowerDesigner的使用, 学会通过建模工具来设计数据库表。

2. 步骤

  • PowerDesigner简介
  • PD建模类型介绍
  • PD工具如何去创建物理模型, 并生成SQL脚本

3. 实现

3.1 简介

PowerDesigner是一款功能非常强大的建模工具软件,也是最流行的建模软件之一。它的发展历程是怎样? 相比其他主流工具有什么区别? 比如Rational Rose, 它是专攻UML对象模型的建模工具,之后才向数据库建模发展;而PowerDesigner正好相反,它是以数据库建模起家,后来才发展为一款综合全面的Case工具。

3.2 PD建模类型

  1. 概念数据模型 (CDM) 对数据和信息进行建模,利用实体-关系图(E-R图)的形式组织数据,检验数据设计的有效性和合理性。
  2. 逻辑数据模型 (LDM) 逻辑模型中一方面显示了实体、实体的属性和实体之间的关系;另一方面又将继承、实体关系中的引用等在实体的属性中进行展示。逻辑模型介于概念模型和物理模型之间,具有物理模型方面的特性,逻辑模型主要是使得整个概念模型更易于理解,同时又不依赖于具体的数据库实现,使用逻辑模型可以生成针对具体数据库管理系统的物理模型。逻辑模型并不是在整个步骤中必须的,可以直接从概念模型来生成物理模型。
  3. 物理数据模型 (PDM) 基于特定DBMS,在概念数据模型、逻辑数据模型的基础上进行设计。由物理数据模型生成数据库,或对数据库进行逆向工程得到物理数据模型。
  4. 面向对象模型 (OOM) 描述业务和操作员之间的关系, 通过UML图进行表示, 比如常见的图形:类图、对象图、包图、用例图、时序图、协作图、交互图、活动图、状态图、组件图、复合结构图、部署图(配置图)。OOM 本质上是软件系统的一个静态的概念模型。
  5. 业务程序模型 (BPM) BPM 描述业务的不同内在任务与流程,帮助大家识别、描述和分解业务流程。PM 是从业务合伙人的观点来看业务逻辑和规则的概念模型,使用一个图表描述程序,流程,信息和合作协议之间的交互作用。
  6. 信息流模型(ILM) ILM是一个高层的信息流模型,主要用于分布式数据库之间的数据复制。
  7. 企业架构模型(EAM) 从业务层、应用层以及技术层的对企业的体系架构进行全方面的描述。包括:组织结构图、业务通信图、进程图、城市规划图、应用架构图、面向服务图、技术基础框架图。

3.3 PD如何物理建模

  1. 选择物理模型

    JavaEE企业级实战项目 智牛股第五天 Netty的使用和项目数据库搭建_第7张图片

  2. 选择数据库类型为MySQL JavaEE企业级实战项目 智牛股第五天 Netty的使用和项目数据库搭建_第8张图片

  3. 新建表 JavaEE企业级实战项目 智牛股第五天 Netty的使用和项目数据库搭建_第9张图片

  4. 创建表信息 JavaEE企业级实战项目 智牛股第五天 Netty的使用和项目数据库搭建_第10张图片 name可以填写为中文,code 为表名称, comment为表注释

  5. 创建表属性 JavaEE企业级实战项目 智牛股第五天 Netty的使用和项目数据库搭建_第11张图片

    也可双击进行编辑: JavaEE企业级实战项目 智牛股第五天 Netty的使用和项目数据库搭建_第12张图片

    填写完成,可以看到会生成对应的SQL信息: JavaEE企业级实战项目 智牛股第五天 Netty的使用和项目数据库搭建_第13张图片

  6. 创建索引 要形成良好习惯, 表建完之后, 考虑哪些是常用的字段, 建立好索引,一般一个表的索引不超过4个。 JavaEE企业级实战项目 智牛股第五天 Netty的使用和项目数据库搭建_第14张图片

    可以看到SQL的索引语句已经生成:
    JavaEE企业级实战项目 智牛股第五天 Netty的使用和项目数据库搭建_第15张图片

  7. 去除drop index脚本

    drop index 不支持 if exists, 直接运行生成的脚本会出现错误, 这里我们把它删掉。 JavaEE企业级实战项目 智牛股第五天 Netty的使用和项目数据库搭建_第16张图片

    选择“Edit Current DBMS"

    JavaEE企业级实战项目 智牛股第五天 Netty的使用和项目数据库搭建_第17张图片

    清空此处脚本语句, 点击确定。

  8. 生成SQL JavaEE企业级实战项目 智牛股第五天 Netty的使用和项目数据库搭建_第18张图片

    选择“Generate Database” 或者直接快捷键“Ctrl + G” JavaEE企业级实战项目 智牛股第五天 Netty的使用和项目数据库搭建_第19张图片

    如果出现错误, 请将“Check model”的勾选去除。

    出现此提示为成功生成

    JavaEE企业级实战项目 智牛股第五天 Netty的使用和项目数据库搭建_第20张图片

  9. 执行脚本

    JavaEE企业级实战项目 智牛股第五天 Netty的使用和项目数据库搭建_第21张图片

    脚本执行成功, 没有出现错误。

4. 总结

  • PowerDesigner是一款多年来,广泛应用于数据库建模的工具, 学习了解其所能支持的建模类型,掌握基本的建模使用, 帮助将来在实际工作中, 做出合理规范的数据库设计。

第4章 数据库调优

1. 目标

  • 掌握更为深入的数据库调优方法与分析思路。

2. 分析

  • 调优简介
  • 如何调优
  • 数据库执行计划分析

3. 讲解

3.1 调优简介

MySQL是目前使用最多的开源数据库,但是MySQL数据库的默认设置性能并非最优,特别在生产环境当中, 如果不去合理优化,不能发挥最佳性能, 甚至会产生各种各样的问题。 因此数据库必须进行必要的调整与优化。

那么如何进行调优? 有哪些方法? 每个系统环境配置不一样, 如何去设置? 线上出现的慢查询, 又如何去定位分析? 带着这些问题, 我们逐一去分析解决。

3.2 如何调优

  1. 影响数据库性能原因:

JavaEE企业级实战项目 智牛股第五天 Netty的使用和项目数据库搭建_第22张图片

联机分析处理(OLAP,On-line Analytical Processing),数据量大,DML少。

联机事务处理(OLTP,On-line Transaction Processing),数据量少,DML频繁。

JavaEE企业级实战项目 智牛股第五天 Netty的使用和项目数据库搭建_第23张图片

  1. 调优方法

    • 业务设计的合理性

      业务设计并不是满足一切业务需求, 在碰到性能瓶颈之处, 要学会权衡取舍, 比如一些业务数据报表, 不需要很强的实时性, 那么就不用直接查询核心数据表增加压力, 采用预生成方式, 这样既满足业务要求, 又能保持核心数据表的操作性能。

    • 增加索引

      索引可以极大优化查询性能, 索引包含主键索引(PRIMAY KEY)、唯一索引(UNIQUE)、常规索引(INDEX)和全文索引(FULLTEXT)。

      索引底层可以采用Btree索引和Hash索引:

      *B-TREE*, B树是一种自平衡的树,能够保持数据有序。这种数据结构能够让查找数据、顺序访问、插入数据及删除的动作,都在对数时间内完成。B树为系统大块数据的读写操作做了优化。B树可以减少定位记录时所经历的中间过程,从而加快存取速度。

      B-TREE特征

      B-TREE通常意味着所有的值都是按顺序存储的,并且每一个叶子页到根的距离都相同。

      B-TREE索引能够加快访问数据的速度,是因为存储引擎不再需要进行全表扫描来获取需要的数据,取而代之的是从索引的根节点开始搜索。

      B-TREE对索引列是顺序组织存储的,所以很适合查找范围数据。

      Hash索引

      Hash是采用哈希算法计算,得到哈希值,排序在哈希数组上。所以Hash索引可以一次定位,其效率很高,而Btree索引需要经过多次的磁盘IO,但是Hash索引也存在缺点:

      • 因为Hash索引比较的是经过Hash计算的值,所以只能进行等式比较,不能用于范围查询
      • 由于哈希值是按照顺序排列的,但是哈希值映射的真正数据在哈希表中就不一定按照顺序排列,所以无法利用Hash索引来加速任何排序操作
      • 当哈希值大量重复且数据量非常大时,其检索效率并没有Btree索引高的。

      注意: 索引并不是一味的增加, 一般一张表索引的数量控制4-5个之内。

    • 分库分表

      分库可以分解数据压力, 采用集群方式,或业务拆分, 从根本上提升整个系统的I/O吞吐能力。

      分表在于单表集中压力的化解, 一般可通过垂直/水平方式进行拆分。分表一般都是单维度拆分, 利用某个字段取模或者根据时间字段进行拆分; 对于多维度的拆分,可采用基因法, 基因法是根据某个字段的特征, 再融入到其他维度字段中,利用基因进行数据定位。 例如, 通过uid分库,分为8个库,采用uid%8的方式进行路由,此时是由uid的最后3bit来决定这行User数据具体落到哪个库上,那么这3bit可以看为分库基因。

      分表也并不是万能, 比如关联查询、分页查询、以及数据路由带来的开销。

    • 读写分离

      读写分离,基本的原理是让主数据库处理事务性的增、改、删操作(INSERT、UPDATE、DELETE),而从数据库处理SELECT查询操作。

      在实际的生产环境中,对数据库的读和写都在同一个数据库服务器中,是不能满足实际需求的。无论是在安全性、高可用性还是高并发等各个方面都是完全不能满足实际需求的。 因此,通过主从复制的方式来同步数据,再通过读写分离来提升数据库的并发负载能力。

    • SQL语句调优

      通过SQL语句的调整来优化响应时间。常见的SQL语句优化点包:

      • 调整Where字句中的连接顺序,一般采用自下而上的顺序解析where字句。
      • 尽量将多条SQL语句压缩到一句, 减少网络开销, IO执行开销。
      • 用where语句替换HAVING语句,因为HAVING只会在检索出所有记录之后才对结果集进行过滤。
      • 使用表的别名,减少属性解析的时间。
      • 在in和exists中通常情况下使用EXISTS,因为in不走索引。
      • 用union all替换union,union会尝试进行合并,然后在输出最终结果前进行排序。
    • mysql配置优化

      • 连接数
      • 缓存
      • 吞吐量
      • IO线程
      • 闲置连接回收
    • 其他优化

      • 索引重建优化
      • 磁盘碎片优化
      • 数据库新特性评估使用,版本升级。

3.3 执行计划分析

  1. 什么是执行计划

    执行计划就是分析执行Sql语句的过程,包括Sql查询的顺序、是否使用索引、以及使用的索引信息等内容。

    一个执行计划由若干基本操作组成. 例如, 遍历整张表, 利用索引, 执行一个嵌套循环或Hash连接等等。

  2. explain关键字与输出项说明

    要生成执行计划, 在相应的语句之前加上explain关键字:

    explain select ...
    

    explain只是预估, 并非实际执行。

    EXPLAIN输出项

    Column JSON Name Meaning
    id select_id The SELECT identifier(选择标识符)
    select_type None The SELECT type (查询类型)
    table table_name The table for the output row (输出结果的表名)
    partitions partitions The matching partitions(匹配的分区信息)
    type access_type The join type(表示的连接类型)
    possible_keys possible_keys The possible indexes to choose (可能使用到的索引)
    key key The index actually chosen(实际使用的索引)
    key_len key_length The length of the chosen key(索引字段的长度)
    ref ref The columns compared to the index(表的连接匹配条件, 哪些列或常量被用于查找索引列上的值)
    rows rows Estimate of rows to be examined(扫描估算的行数)
    filtered filtered Percentage of rows filtered by table condition(按条件过滤的行百分比)
    Extra None Additional information(执行计划的情况的描述和说明)

    注意:在5.7以前的版本中,想要显示partitions需要使用explain partitions命令;想要显示filtered需要使用explain extended命令。在5.7版本后,默认explain直接显示partitions和filtered中的信息。

  3. 通过实际SQL来解读执行计划信息

    通过脚本,创建好用于SQL执行计划的数据。

    下面分析两条SQL语句的执行计划:

    第一条语句是要查询用户所关联的角色信息, USER_ACCOUNT为唯一索引。

    EXPLAIN SELECT
    
    
    
        u.id,
    
    
    
    u.USER_ACCOUNT,
    
    
    
        r.ROLE_NAME 
    
    
    
    FROM
    
    
    
        s_authority_user u
    
    
    
    LEFT JOIN s_authority_user_role ur ON u.id = ur.USER_ID
    
    
    
        LEFT JOIN s_authority_role r ON ur.role_id = r.id 
    
    
    
    WHERE
    
    
    
    u.USER_ACCOUNT = 'admin'
    

    JavaEE企业级实战项目 智牛股第五天 Netty的使用和项目数据库搭建_第24张图片

    第二条语句是一个多SQL查询:

      EXPLAIN SELECT
    
    
    
        u1.* 
    
    
    
      FROM
    
    
    
        s_authority_user u1 UNION
    
    
    
      SELECT
    
    
    
        u2.* 
    
    
    
      FROM
    
    
    
        s_authority_user u2
    

    JavaEE企业级实战项目 智牛股第五天 Netty的使用和项目数据库搭建_第25张图片

    下面, 详细分析执行计划各字段含义:

    • id属性

      id为SELECT的标识符, 如果这一行表示其他行的union结果,这个值可以为空。在这种情况下,table列会显示为形如,表示它是id为M和N的查询行的联合结果。例如第二条SQL的查询。

      • id相同执行顺序由上至下。
      • id不同,id值越大优先级越高,越先被执行。
      • id为null时表示一个结果集,不需要使用它查询,常出现在包含union等查询语句中。
    • select_type属性

      常见的属性值

      id select_type description
      1 SIMPLE 不包含任何子查询或union等查询
      2 PRIMARY 包含子查询最外层查询就显示为 PRIMARY
      3 SUBQUERY selectwhere字句中包含的查询 (in匹配不会生效, 要采用等号)
      4 DERIVED from字句中包含的查询
      5 UNION 出现在union后的查询语句中
      6 UNION RESULT 从UNION中获取结果集,例如上面的例子

      除此之外还有, derived:from字句中出现的子查询; materialized:被物化的子查询等, 这些用得相对较少。

    • table属性

      显示的查询表名,如果查询使用了别名,那么这里显示的是别名,如果不涉及对数据表的操作,那么这显示为null,如果显示为尖括号括起来的就表示这个是临时表,后边的N就是执行计划中的id,表示结果来自于这个查询产生。如果是尖括号括起来的,与类似,也是一个临时表,表示这个结果来自于union查询的id为M,N的结果集。

      例如:

      EXPLAIN SELECT
      
      
      
          u.id,
      
      
      
      u.USER_ACCOUNT,
      
      
      
          r.ROLE_NAME 
      
      
      
      FROM
      
      
      
          s_authority_user u
      
      
      
      LEFT JOIN (select USER_ID, role_id from s_authority_user_role group by user_id) ur ON u.id = ur.USER_ID
      
      
      
          LEFT JOIN s_authority_role r ON ur.role_id = r.id 
      
      
      
      WHERE
      
      
      
          u.USER_ACCOUNT = 'admin'
      
      
      
          
      
    • partitions属性

      该列显示的为分区表命中的分区情况,非分区表该字段为空(null)。

      例如, 创建一个支持分区的表:

      drop table if exists tmp_partitions;
      
      
      
      create table tmp_partitions (
      
      
      
          id int unsigned not null AUTO_INCREMENT,
      
      
      
          name varchar(255),
      
      
      
          PRIMARY KEY (id)
      
      
      
      ) engine = innodb
      
      
      
      partition by key (id) partitions 3;
      

      查看其执行计划:

      1563292895174

    • type属性

      表示表查询的连接类型

      id type description
      1 ALL 扫描全表数据
      2 index 遍历索引
      3 range 索引范围查找
      4 index_subquery 在子查询中使用 ref
      5 unique_subquery 在子查询中使用 eq_ref
      6 UNION ref_or_null 对Null进行索引的优化的 ref
      7 fulltext 使用全文索引
      8 ref 使用非唯一索引查找数据
      9 eq_ref 在join查询中使用PRIMARY KEY或UNIQUE NOT NULL索引关联
      10 const 使用主键或者唯一索引,且匹配的结果只有一条记录
      11 system const 连接类型的特例,查询的表为系统表
      • 性能依次从好到差:system,const,eq_ref,ref,fulltext,ref_or_null,unique_subquery,index_subquery,range,index_merge,index,ALL。
      • index:索引全表扫描,把索引从头到尾扫一遍,常见于使用索引列就可以处理不需要读取数据文件的查询、可以使用索引排序或者分组的查询。按照官方文档的说法。
      • ref:不像eq_ref那样要求连接顺序,也没有主键和唯一索引的要求,只要使用相等条件检索时就可能出现,常见于辅助索引的等值查找; 或者多列主键、唯一索引中,除第一个列之外的列, 作为等值查找也会出现,总之,返回数据不唯一的等值查找就可能出现。
      • range:索引范围扫描,常见于使用 =, <>, >, >=, <, <=, IS NULL, <=>, BETWEEN, IN()或者like等运算符的查询中。
    • possible_keys属性

      可能使用的索引,注意不一定会使用。查询涉及到的字段上若存在索引,则该索引将被列出来。当该列为 null时就要考虑当前的SQL是否需要优化了。

    • key属性

      显示查询使用到的索引,select_type为index_merge时,这里可能出现两个以上的索引,其他的select_type这里只会出现一个。

    • key_len属性

      索引长度,字符索引长度的计算公式:

      (Character Set:utf8mb4=4,utf8=3,gbk=2,latin1=1) * 列长度 + 1(允许null) + 2(变长列)
      

      key_len只计算where条件用到的索引长度,而排序和分组就算用到了索引,也不会计算到key_len中。

    • ref属性

      如果是使用的常数等值查询,这里会显示const,如果是连接查询,被驱动表的执行计划这里会显示驱动表的关联字段,如果是条件使用了表达式或者函数,或者条件列发生了内部隐式转换,这里可能显示为func。

    • rows属性

      返回估算的结果集数目, 因为explain并非真正执行语句, 所以并非精确值。

    • extra属性 常见的有:

      • Using index: 使用覆盖索引
      • Using where: 使用了where子句来过滤结果集, 表示存储引擎返回的记录并不是所有的都满足查询条件,需要在server层进行过滤。
      • Using filesort: 使用文件排序,使用非索引列进行排序时出现,非常消耗性能,尽量优化。
      • Using temporary: 表示使用了临时表存储中间结果

      注意: extra辅助信息中的using filesort和using temporary,这两项非常消耗性能,需要谨慎。

4. 总结

  • 除了基本的SQL调优之外, 我们应该还要学会掌握更深入的数据库调优方法, 从数据库配置, 设计层面着手, 发挥更大的数据库性能。
  • 掌握执行计划分析, 能够帮助我们在实际生产环境中, 分析解决各种复杂查询问题。

第5章 数据库设计之交易模块

1. 目标

  • 了解数据库交易模块的设计, 掌握核心表结构。

2. 分析

  • 交易平台设计
  • 交易模块物理模型设计
  • 核心表结构设计

3. 讲解

3.1 交易平台设计

金融交易整个平台主要分为交易模块和非交易(业务为主)模块, 交易模块是整个系统的核心,也是整个数据层的脊骨,那么交易平台如何去设计? 整个设计遵循FIX国际金融标准协议, 借鉴行业内的标杆, 恒生和金证柜台交易系统, 并作扩充改进, 支撑更广泛的金融产品, 如外汇、期货、CFD等交易。

可参考附件《FixApi文档》。

3.2 交易模块物理模型

JavaEE企业级实战项目 智牛股第五天 Netty的使用和项目数据库搭建_第26张图片

订单表、成交记录表和仓位表组成整个交易模块的核心,此设计可以支撑多次成交,仓位合并,订单合并等复杂业务场景。

3.3 主要表设计

  • 用户表:

    属性 名称 数据类型 是否为空 注释
    id 主键标识 bigint(20) NO 主键标识
    userNo 用户编号 varchar(16) NO 用户编号
    name 用户名称 varchar(16) YES 用户名称
    userPwd 用户密码 varchar(16) NO 用户密码
    phone 电话号码 varchar(16) YES 电话号码
    companyId 公司ID bigint(20) YES 公司ID
    email 邮箱 varchar(32) YES 邮箱
    address 地址 varchar(255) YES 地址
    lastLoginIp 最近一次用户登陆IP varchar(32) YES 最近一次用户登陆IP
    lastLoginTime 最近一次登陆时间 datetime YES 最近一次登陆时间
    status 状态(0:有效, 1:锁定, 2:禁用) tinyint(3) NO 状态(0:有效, 1:锁定, 2:禁用)
    craeteTime 创建时间 datetime YES 创建时间
    institutionTypeId 机构类型id varchar(32) YES 机构类型id
    institutionId 机构id bigint(19) YES 机构ID
    companyName 公司名称 varchar(64) YES 公司名称
  • 用户账号表

    属性 名称 数据类型 是否为空 注释
    id 主键标识 bigint(20) NO 主键标识
    userId 用户表ID bigint(20) NO 用户表的ID
    accountNo 交易账号 varchar(16) NO 交易账号
    balance 余额 bigint(21) YES 余额
    tradeGroupId 账户组ID bigint(20) NO 账户组ID
    activeTime 开户时间 datetime YES 开户时间
    status 状态(0:有效, 1:锁定, 2:禁用) tinyint(3) NO 状态(0:有效, 1:锁定, 2:禁用)
    companyId 公司ID bigint(20) YES 公司ID
    userName 用户名称 varchar(32) YES 用户名称
    institutionId 对应机构类型下的机构id bigint(19) YES 对应机构类型下的机构id
    institutionTypeId 机构类型id varchar(32) YES 机构类型id
    tradeGroupName 账户组名称 varchar(32) YES 账户组名称
  • 用户账户组表

    属性 名称 数据类型 是否为空 注释
    id 主键标识 bigint(20) NO 主键标识
    groupNo 账户组编号 varchar(16) NO 账户组编号
    groupName 账户组名称 varchar(16) NO 账户组名称
    currency 交易币种(CNY:人民币, HKD:港币,USD:美元) varchar(12) YES 交易币种(CNY:人民币, HKD:港币,USD:美元)
    companyId 公司ID bigint(20) NO 公司ID
    commissionRate 佣金比例, 单位:% double(4,2) YES 佣金比例, 单位:%
    taxesRate 税费比例, 单位:% double(4,2) YES 税费比例, 单位:%
    level 账户等级(0:普通,1:VIP) tinyint(3) YES 账户等级(0:普通,1:VIP)
    status 状态(0:启用, 1:禁用) tinyint(3) NO 状态(0:启用, 1:禁用)
    craeteTime 创建时间 datetime YES 创建时间
    companyName 公司名称 varchar(64) YES 公司名称
  • 订单表

    属性 名称 数据类型 是否为空 注释
    id 主键标识 bigint(20) NO 主键标识
    accountId 交易账号ID bigint(20) NO 交易账号ID
    stockId 股票ID bigint(20) NO 股票ID
    status 状态(0:待成交, 1:部分成交, 2:完全成交, 3:手工撤单, 4:系统撤单) tinyint(3) NO 状态(0:待成交, 1:部分成交, 2:完全成交, 3:手工撤单, 4:系统撤单)
    type 挂单类型(0:市价单, 1:限价单) tinyint(3) NO 挂单类型(0:市价单, 1:限价单)
    direction 买卖方向 tinyint(2) NO 买卖方向
    initVolume 挂单的数量 int(11) NO 挂单的数量
    execVolume 成交数量 int(11) YES 成交数量
    requestPrice 请求价格 double(8,2) YES 请求价格
    execPrice 成交价格 double(8,2) YES 成交价格
    execTime 成交时间 datetime YES 成交时间
    accountno 账户编号 varchar(32) YES 账户编号
    stockcode 股票编号 varchar(32) YES 股票编号
    stockname 股票名称 varchar(32) YES 股票名称
  • 成交记录表

    属性 名称 数据类型 是否为空 注释
    id 主键, 唯一标识 bigint(20) NO 主键, 唯一标识
    tradeOrderId 交易订单号 bigint(20) NO 交易订单号
    positionId 持仓记录ID bigint(20) NO 持仓记录ID
    accountId 交易账号ID bigint(20) NO 交易账号ID
    stockId 股票ID bigint(20) NO 股票ID
    stockMarket 股票市场(0: 上交所, 1:深交所,2:港股, 3:美股) tinyint(3) YES 股票市场(0: 上交所, 1:深交所,2:港股, 3:美股)
    execVolume 成交数量 int(11) YES 成交数量
    execPrice 成交价格 double(8,2) YES 成交价格
    execTime 成交时间 datetime YES 成交时间
    direction 买卖方向 tinyint(2) NO 买卖方向
    profit 盈亏 double(8,2) YES 盈亏
    commission 佣金 double(6,2) YES 佣金
    taxes 税费 double(6,2) YES 税费
    status 状态(0:有效,1:无效) tinyint(2) NO 状态(0:有效,1:无效)
  • 仓位表

    属性 名称 数据类型 是否为空 注释
    id 主键, 唯一标识 bigint(20) NO 主键, 唯一标识
    accountId 交易账号ID bigint(20) NO 交易账号ID
    stockId 股票ID bigint(20) NO 股票ID
    costPrice 成本价 double(8,2) YES 成本价
    volume 持仓数量 int(11) NO 持仓数量
    stockMarket 股票市场(0: 上交所, 1:深交所,2:港股, 3:美股) tinyint(3) YES 股票市场(0: 上交所, 1:深交所,2:港股, 3:美股)
    avgPrice 成交均价 double(6,2) NO 成交均价
    validSellVolume 可卖数量(T+1制度使用) int(11) YES 可卖数量(T+1制度使用)
    profit 盈亏 double(8,2) YES 盈亏
    execTime 成交时间 datetime YES 成交时间
    status 状态(0:有效,1:无效) tinyint(2) NO 状态(0:有效,1:无效)
    interest 持仓累计利息 double(8,2) YES 持仓累计利息
  • 股票信息表

    属性 名称 数据类型 是否为空 注释
    id 主键标识 bigint(20) NO 主键标识
    name 名称 varchar(16) YES 名称
    code 股票编号 varchar(12) YES 股票编号
    categoryId 股票分类ID bigint(20) NO 股票分类ID
    marketNo 股票市场(0: 上交所, 1:深交所,2:港股, 3:美股) tinyint(3) YES 股票市场(0: 上交所, 1:深交所,2:港股, 3:美股)
    currency 交易币种(CNY:人民币, HKD:港币,USD:美元) double(12,2) YES 交易币种(CNY:人民币, HKD:港币,USD:美元)
    unit 股票单位(默认为:100) int(11) NO 股票单位(默认为:100)
    tag 标签(0:普通, 1:热门) tinyint(3) YES 标签(0:普通, 1:热门)
    status 状态(0:启用, 1:禁用) tinyint(2) NO 状态(0:启用, 1:禁用)
    marketSource 行情来源 varchar(32) YES 行情来源
    display_order 显示顺序, 升序排列 int(11) YES 显示顺序, 升序排列
    updateTime 修改时间 datetime YES 修改时间
    craeteTime 创建时间 datetime YES 创建时间
  • 股票分类表

    属性 名称 数据类型 是否为空 注释
    id 主键标识 bigint(20) NO 主键标识
    name 名称 varchar(16) YES 名称
    code 分类编号 varchar(12) YES 分类编号
    status 状态(0:启用, 1:禁用) tinyint(2) NO 状态(0:启用, 1:禁用)
    startMins 交易开始时间(分钟) int NO 交易开始时间(分钟)
    endMins 交易结束时间(分钟) int NO 交易结束时间(分钟)
    commissionRate 佣金比例(%) int NO 佣金比例(%)

4. 总结

 |

| unit | 股票单位(默认为:100) | int(11) | NO | 股票单位(默认为:100) |
| tag | 标签(0:普通, 1:热门) | tinyint(3) | YES | 标签(0:普通, 1:热门) |
| status | 状态(0:启用, 1:禁用) | tinyint(2) | NO | 状态(0:启用, 1:禁用) |
| marketSource | 行情来源 | varchar(32) | YES | 行情来源 |
| display_order | 显示顺序, 升序排列 | int(11) | YES | 显示顺序, 升序排列 |
| updateTime | 修改时间 | datetime | YES | 修改时间 |
| craeteTime | 创建时间 | datetime | YES | 创建时间 |

  • 股票分类表

    属性 名称 数据类型 是否为空 注释
    id 主键标识 bigint(20) NO 主键标识
    name 名称 varchar(16) YES 名称
    code 分类编号 varchar(12) YES 分类编号
    status 状态(0:启用, 1:禁用) tinyint(2) NO 状态(0:启用, 1:禁用)
    startMins 交易开始时间(分钟) int NO 交易开始时间(分钟)
    endMins 交易结束时间(分钟) int NO 交易结束时间(分钟)
    commissionRate 佣金比例(%) int NO 佣金比例(%)

4. 总结

  • 交易系统的设计是整个设计的核心部分, 如何拆分, 建立表关联, 既不能太复杂, 也不能过于简单, 可以参考行业的规范, 这是经过多年的累积, 且经历过长期的验证, 能够满足业务功能要求, 同时又具备较强的扩展性, 不仅适用于股票, 还可适用于外汇, 贵金属, 期货等产品, 在报表加工统计方面, 结构层次清晰, 利于呈现。

你可能感兴趣的:(java,教程,数据库,后端,架构,java)