分类: mysql

  • 美团:为什么 MySQL 不推荐使用 join?

    快乐分享,Java干货及时送达👇

    来源:cnblogs.com/liboware/p/12740901.html

    • 一、应用层关联的优势
    • 二、应用层关联的使用场景
    • 三、不推荐使用join的原因
    • 四、不使用join的解决方案
    • 五、join查询的优势

    1.对于mysql,不推荐使用子查询和join是因为本身join的效率就是硬伤,一旦数据量很大效率就很难保证,强烈推荐分别根据索引单表取数据,然后在程序里面做join,merge数据。

    2.子查询就更别用了,效率太差,执行子查询时,MYSQL需要创建临时表,查询完毕后再删除这些临时表,所以,子查询的速度会受到一定的影响,这里多了一个创建和销毁临时表的过程。

    3.如果是JOIN的话,它是走嵌套查询的。小表驱动大表,且通过索引字段进行关联。如果表记录比较少的话,还是OK的。大的话业务逻辑中可以控制处理。

    4.数据库是最底层的,瓶颈往往是数据库。建议数据库只是作为数据store的工具,而不要添加业务上去。

    一、应用层关联的优势

    让缓存的效率更高。许多应用程序可以方便地缓存单表查询对应的结果对象。如果关联中的某个表发生了变化,那么就无法使用查询缓存了,而拆分后,如果某个表很少改变,那么基于该表的查询就可以重复利用查询缓存结果了。

    • 将查询分解后,执行单个查询可以减少锁的竞争。
    • 在应用层做关联,可以更容易对数据库进行拆分,更容易做到高性能和可扩展。
    • 查询本身效率也可能会有所提升。查询id集的时候,使用IN()代替关联查询,可以让MySQL按照ID顺序进行查询,这可能比随机的关联要更高效。
    • 可以减少冗余记录的查询。在应用层做关联查询,意味着对于某条记录应用只需要查询一次,而在数据库中做关联查询,则可能需
    • 要重复地访问一部分数据。从这点看,这样的重构还可能会减少网络和内存的消艳。
    • 更进一步,这样做相当于在应用中实现了哈希关联,而不是使用MySQL的嵌套循环关联。某些场景哈希关联的效率要高很多。

    二、应用层关联的使用场景

    • 当应用能够方便地缓存单个查询的结果的时候
    • 当可以将数据分布到不同的MySQL服务器上的时候
    • 当能够使用IN()的方式代替关联查询的时候
    • 并发场景多,DB查询频繁,需要分库分表

    三、不推荐使用join的原因

    1.DB承担的业务压力大,能减少负担就减少。当表处于百万级别后,join导致性能下降;

    2.分布式的分库分表。这种时候是不建议跨库join的。目前mysql的分布式中间件,跨库join表现不良。

    3.修改表的schema,单表查询的修改比较容易,join写的sql语句要修改,不容易发现,成本比较大,当系统比较大时,不好维护。

    四、不使用join的解决方案

    在业务层,单表查询出数据后,作为条件给下一个单表查询。也就是子查询。会担心子查询出来的结果集太多。mysql对in的数量没有限制,但是mysql限制整条sql语句的大小。

    通过调整参数max_allowed_packet ,可以修改一条sql的最大值。建议在业务上做好处理,限制一次查询出来的结果集是能接受的。

    五、join查询的优势

    关联查询的好处是可以做分页,可以用副表的字段做查询条件,在查询的时候,将副表匹配到的字段作为结果集,用主表去in它。

    但是问题来了,如果匹配到的数据量太大就不行了,也会导致返回的分页记录跟实际的不一样,解决的方法可以交给前端,一次性查询,让前端分批显示就可以了,这种解决方案的前提是数据量不太,因为sql本身长度有限。

  • SpringBoot + Druid DataSource 实现监控 MySQL 性能

    快乐分享,Java干货及时送达👇

    来源:blog.csdn.net/lvoelife/article/details/128092586

    1. 基本概念

    我们都使用过连接池,比如C3P0,DBCP,hikari, Druid,虽然HikariCP的速度稍快,但Druid能够提供强大的监控和扩展功能,也是阿里巴巴的开源项目。

    Druid是阿里巴巴开发的号称为监控而生的数据库连接池,在功能、性能、扩展性方面,都超过其他数据库连接池,包括DBCP、C3P0、BoneCP、Proxool、JBoss DataSource等等,秒杀一切。

    Druid可以很好的监控DB池连接和SQL的执行情况,天生就是针对监控而生的DB连接池。

    Spring Boot默认数据源HikariDataSourceJdbcTemplate中已经介绍Spring Boot 2.x默认使用Hikari数据源,可以说Hikari与Driud都是当前Java Web上最优秀的数据源。

    而Druid已经在阿里巴巴部署了超过600个应用,经过好几年生产环境大规模部署的严苛考验!

    • stat: Druid内置提供一个StatFilter,用于统计监控信息。
    • wall: Druid防御SQL注入攻击的WallFilter就是通过Druid的SQL Parser分析。Druid提供的SQL Parser可以在JDBC层拦截SQL做相应处理,比如说分库分表、审计等。
    • log4j2: 这个就是 日志记录的功能,可以把sql语句打印到log4j2供排查问题。

    2. 相关配置

    2.1 添加依赖


        1.8
        1.2.11



        com.alibaba
        druid-spring-boot-starter
        ${alibabaDruidStarter.version}

    2.2 配置属性

    • 配置Druid数据源(连接池): 如同c3p0、dbcp数据源可以设置数据源连接初始化大小、最大连接数、等待时间、最小连接数 等一样,Druid数据源同理可以进行设置。
    • 配置Druid web监控filter(WebStatFilter): 这个过滤器的作用就是统计web应用请求中所有的数据库信息,比如 发出的sql语句,sql执行的时间、请求次数、请求的url地址、以及seesion监控、数据库表的访问次数等等。
    • 配置Druid后台管理Servlet(StatViewServlet): Druid数据源具有监控的功能,并提供了一个web界面方便用户查看,类似安装 路由器 时,人家也提供了一个默认的web页面;需要设置Druid的后台管理页面的属性,比如 登录账号、密码等。

    【注意】:Druid Spring Boot Starter配置属性的名称完全遵照Druid,可以通过Spring Boot配置文件来配置Druid数据库连接池和监控,如果没有配置则使用默认值,如下在application.yml配置相关属性:

    # spring 配置
    spring:
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        password: 123456
        username: root
        url: jdbc:mysql://localhost:3306/superjson?useUnicode=true&characterEncoding=utf8&useSSL=false
        # 连接池配置
        druid:
          # 初始化大小,最小,最大
          initial-size: 5
          min-idle: 5
          max-active: 20
          # 配置获取连接等待超时的时间
          max-wait: 60000
          # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位毫秒
          time-between-eviction-runs-millis: 60000
          # 配置一个连接在池中最小生存时间
          min-evictable-idle-time-millis: 300000
          validation-query: SELECT 1 FROM user
          test-while-idle: true
          test-on-borrow: false
          test-on-return: false
          # 打开 PSCache,并且指定每个连接上 PSCache 的大小
          pool-prepared-statements: true
          max-pool-prepared-statement-per-connection-size: 20
          # 配置监控统计拦截的 Filter,去掉后监控界面 SQL 无法统计,wall 用于防火墙
          filters: stat,wall,slf4j
          # 通过 connection-properties 属性打开 mergeSql 功能;慢 SQL 记录
          connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
          # 配置 DruidStatFilter
          web-stat-filter:
            enabled: true
            url-pattern: /*
            exclusions: .js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*
          # 配置 DruidStatViewServlet
          stat-view-servlet:
            url-pattern: /druid/*
            # IP 白名单,没有配置或者为空,则允许所有访问
            allow: 127.0.0.1
            # IP 黑名单,若白名单也存在,则优先使用
            deny: 192.168.31.253
            # 禁用 HTML 中 Reset All 按钮
            reset-enable: false
            # 登录用户名/密码
            login-username: root
            login-password: 123456
            # 需要设置enabled=true,否则会报出There was an unexpected error (type=Not Found, status=404).错误,或者将druid-spring-boot-starter的版本降低到1.1.10及以下
            # 是否启用StatViewServlet(监控页面)默认值为false(考虑到安全问题默认并未启动,如需启用建议设置密码或白名单以保障安全)
            enabled: true

    上述配置文件的参数可以在com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatPropertiesorg.springframework.boot.autoconfigure.jdbc.DataSourcePropertie中找到。

    2.3 配置Filter

    可以通过spring.datasource.druid.filters=stat,wall,log4j ...的方式来启用相应的内置Filter,不过这些Filter都是默认配置。如果默认配置不能满足需求,可以放弃这种方式,通过配置文件来配置Filter,如下所示:

    # 配置StatFilter 
    spring.datasource.druid.filter.stat.enabled=true
    spring.datasource.druid.filter.stat.db-type=h2
    spring.datasource.druid.filter.stat.log-slow-sql=true
    spring.datasource.druid.filter.stat.slow-sql-millis=2000

    # 配置WallFilter 
    spring.datasource.druid.filter.wall.enabled=true
    spring.datasource.druid.filter.wall.db-type=h2
    spring.datasource.druid.filter.wall.config.delete-allow=false
    spring.datasource.druid.filter.wall.config.drop-table-allow=false

    目前为以下Filter提供了配置支持,根据(spring.datasource.druid.filter.*)进行配置。

    • StatFilter
    • WallFilter
    • ConfigFilter
    • EncodingConvertFilter
    • Slf4jLogFilter
    • Log4jFilter
    • Log4j2Filter
    • CommonsLogFilter

    不想使用内置的Filters,要想使自定义Filter配置生效需要将对应Filter的enabled设置为true,Druid Spring Boot Starter默认禁用StatFilter,可以将其enabled设置为true来启用它。

    3 监控页面

    1. 启动项目后,访问http://localhost:8081/druid/login.html来到登录页面,输入用户名密码登录,如下所示:

    图片
    1. 数据源页面 是当前DataSource配置的基本信息,上述配置的Filter可以在里面找到,如果没有配置 Filter(一些信息会无法统计,例如SQL监控会无法获取JDBC相关的SQL执行信息)

    图片
    1. SQL监控页面,统计了所有SQL语句的执行情况

    图片
    1. URL监控页面,统计了所有Controller接口的访问以及执行情况

    图片
    1. Spring监控页面,利用aop对指定接口的执行时间,jdbc数进行记录

    图片
    1. SQL防火墙页面

    druid提供了黑白名单的访问,可以清楚的看到sql防护情况。

    1. Session监控页面

    可以看到当前的session状况,创建时间、最后活跃时间、请求次数、请求时间等详细参数。

    1. JSONAPI页面

    通过api的形式访问Druid的监控接口,api接口返回Json形式数据。

    4. sql监控

    配置Druid web监控filter(WebStatFilter)这个过滤器,作用就是统计web应用请求中所有的数据库信息,比如 发出的sql语句,sql执行的时间、请求次数、请求的url地址、以及seesion监控、数据库表的访问次数,如下配置:

    spring:
      datasource:
        druid:
          ########## 配置WebStatFilter,用于采集web关联监控的数据 ##########
          web-stat-filter:
            enabled: true                   # 启动 StatFilter
            url-pattern: /*                 # 过滤所有url
            exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*" # 排除一些不必要的url
            session-stat-enable: true       # 开启session统计功能
            session-stat-max-count: 1000    # session的最大个数,默认100

    5. 慢sql记录

    有时候,系统中有些SQL执行很慢,我们希望使用日志记录下来,可以开启Druid的慢SQL记录功能,如下配置:

    spring:
      datasource:
        druid:
          filter:
            stat:
              enabled: true         # 开启DruidDataSource状态监控
              db-type: mysql        # 数据库的类型
              log-slow-sql: true    # 开启慢SQL记录功能
              slow-sql-millis: 2000 # 默认3000毫秒,这里超过2s,就是慢,记录到日志

    启动后,如果遇到执行慢的SQL,便会输出到日志中

    6. spring 监控

    访问之后spring监控默认是没有数据的,但需要导入SprngBoot的AOP的Starter,如下所示:



        org.springframework.boot
        spring-boot-starter-aop

    同时需要在application.yml按如下配置:

    Spring监控AOP切入点,如com.springboot.template.dao.*,配置多个英文逗号分隔

    spring.datasource.druid.aop-patterns="com.springboot.template.dao.*"

    7. 去广告(Ad)

    访问监控页面的时候,你可能会在页面底部(footer)看到阿里巴巴的广告,如下所示:

    图片

    原因:引入的druid的jar包中的common.js(里面有一段js代码是给页面的footer追加广告的)

    如果想去掉,有两种方式:

    1. 直接手动注释这段代码

    如果是使用Maven,直接到本地仓库中,查找这个jar包,注释如下代码:

    // this.buildFooter();

    common.js的位置:

    com/alibaba/druid/1.1.23/druid-1.1.23.jar!/support/http/resources/js/common.js

    2. 使用过滤器过滤

    注册一个过滤器,过滤common.js的请求,使用正则表达式替换相关的广告内容,如下代码所示:

    @Configuration
    @ConditionalOnWebApplication
    @AutoConfigureAfter(DruidDataSourceAutoConfigure.class)
    @ConditionalOnProperty(name = "spring.datasource.druid.stat-view-servlet.enabled",
    havingValue = "true", matchIfMissing = true)
    public class RemoveDruidAdConfig {

        /**
        * 方法名: removeDruidAdFilterRegistrationBean
        * 方法描述 除去页面底部的广告
        * @param properties com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties
        * @return org.springframework.boot.web.servlet.FilterRegistrationBean
        */
        @Bean
        public FilterRegistrationBean removeDruidAdFilterRegistrationBean(DruidStatProperties properties) {

            // 获取web监控页面的参数
            DruidStatProperties.StatViewServlet config = properties.getStatViewServlet();
            // 提取common.js的配置路径
            String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*";
            String commonJsPattern = pattern.replaceAll("\*""js/common.js");

            final String filePath = "support/http/resources/js/common.js";

            //创建filter进行过滤
            Filter filter = new Filter() {
                @Override
                public void init(FilterConfig filterConfig) throws ServletException {}

                @Override
                public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
                    chain.doFilter(request, response);
                    // 重置缓冲区,响应头不会被重置
                    response.resetBuffer();
                    // 获取common.js
                    String text = Utils.readFromResource(filePath);
                    // 正则替换banner, 除去底部的广告信息
                    text = text.replaceAll("
    "
    "");
                    text = text.replaceAll("powered.*?shrek.wang""");
                    response.getWriter().write(text);
                }

                @Override
                public void destroy() {}
            };

            FilterRegistrationBean registrationBean = new FilterRegistrationBean();
            registrationBean.setFilter(filter);
            registrationBean.addUrlPatterns(commonJsPattern);
            return registrationBean;
        }
    }

    两种方式都可以,建议使用的是第一种,从根源解决。

    8. 获取 Druid 的监控数据

    Druid的监控数据可以在开启StatFilter后,通过DruidStatManagerFacade进行获取;

    DruidStatManagerFacade#getDataSourceStatDataList该方法可以获取所有数据源的监控数据,除此之外DruidStatManagerFacade还提供了一些其他方法,可以按需选择使用。

    @RestController
    @RequestMapping(value = "/druid")
    public class DruidStatController {

        @GetMapping("/stat")
        public Object druidStat(){
            // 获取数据源的监控数据
            return DruidStatManagerFacade.getInstance().getDataSourceStatDataList();
        }
    }
    
    

  • ES+Redis+MySQL,这个高可用架构设计太顶了!

    快乐分享,Java干货及时送达👇

    来源:dbaplus

    • 一、背景
    • 二、ES高可用方案
    • 三、会员Redis缓存方案
    • 四、高可用会员主库方案
    • 五、异常会员关系治理
    • 六、展望:更精细化的流控和降级策略

    一、背景


    会员系统是一种基础系统,跟公司所有业务线的下单主流程密切相关。如果会员系统出故障,会导致用户无法下单,影响范围是全公司所有业务线。所以,会员系统必须保证高性能、高可用,提供稳定、高效的基础服务。


    随着同程和艺龙两家公司的合并,越来越多的系统需要打通同程APP、艺龙APP、同程微信小程序、艺龙微信小程序等多平台会员体系。例如微信小程序的交叉营销,用户买了一张火车票,此时想给他发酒店红包,这就需要查询该用户的统一会员关系。因为火车票用的是同程会员体系,酒店用的是艺龙会员体系,只有查到对应的艺龙会员卡号后,才能将红包挂载到该会员账号。除了上述讲的交叉营销,还有许多场景需要查询统一会员关系,例如订单中心、会员等级、里程、红包、常旅、实名,以及各类营销活动等等。所以,会员系统的请求量越来越大,并发量越来越高,今年五一小长假的秒并发tps甚至超过2万多。在如此大流量的冲击下,会员系统是如何做到高性能和高可用的呢?这就是本文着重要讲述的内容。


    二、ES高可用方案


    1. ES双中心主备集群架构


    同程和艺龙两家公司融合后,全平台所有体系的会员总量是十多亿。在这么大的数据体量下,业务线的查询维度也比较复杂。有的业务线基于手机号,有的基于微信unionid,也有的基于艺龙卡号等查询会员信息。这么大的数据量,又有这么多的查询维度,基于此,我们选择ES用来存储统一会员关系。ES集群在整个会员系统架构中非常重要,那么如何保证ES的高可用呢?


    首先我们知道,ES集群本身就是保证高可用的,如下图所示:


    当ES集群有一个节点宕机了,会将其他节点对应的Replica Shard升级为Primary Shard,继续提供服务。但即使是这样,还远远不够。例如ES集群都部署在机房A,现在机房A突然断电了,怎么办?例如服务器硬件故障,ES集群大部分机器宕机了,怎么办?或者突然有个非常热门的抢购秒杀活动,带来了一波非常大的流量,直接把ES集群打死了,怎么办?面对这些情况,让运维兄弟冲到机房去解决?这个非常不现实,因为会员系统直接影响全公司所有业务线的下单主流程,故障恢复的时间必须非常短,如果需要运维兄弟人工介入,那这个时间就太长了,是绝对不能容忍的。那ES的高可用如何做呢?我们的方案是ES双中心主备集群架构。


    我们有两个机房,分别是机房A和机房B。我们把ES主集群部署在机房A,把ES备集群部署在机房B。会员系统的读写都在ES主集群,通过MQ将数据同步到ES备集群。此时,如果ES主集群崩了,通过统一配置,将会员系统的读写切到机房B的ES备集群上,这样即使ES主集群挂了,也能在很短的时间内实现故障转移,确保会员系统的稳定运行。最后,等ES主集群故障恢复后,打开开关,将故障期间的数据同步到ES主集群,等数据同步一致后,再将会员系统的读写切到ES主集群。


    2. ES流量隔离三集群架构


    双中心ES主备集群做到这一步,感觉应该没啥大问题了,但去年的一次恐怖流量冲击让我们改变了想法。那是一个节假日,某个业务上线了一个营销活动,在用户的一次请求中,循环10多次调用了会员系统,导致会员系统的tps暴涨,差点把ES集群打爆。这件事让我们后怕不已,它让我们意识到,一定要对调用方进行优先级分类,实施更精细的隔离、熔断、降级、限流策略。首先,我们梳理了所有调用方,分出两大类请求类型。第一类是跟用户的下单主流程密切相关的请求,这类请求非常重要,应该高优先级保障。第二类是营销活动相关的,这类请求有个特点,他们的请求量很大,tps很高,但不影响下单主流程。基于此,我们又构建了一个ES集群,专门用来应对高tps的营销秒杀类请求,这样就跟ES主集群隔离开来,不会因为某个营销活动的流量冲击而影响用户的下单主流程。如下图所示:



    3. ES集群深度优化提升


    讲完了ES的双中心主备集群高可用架构,接下来我们深入讲解一下ES主集群的优化工作。有一段时间,我们特别痛苦,就是每到饭点,ES集群就开始报警,搞得每次吃饭都心慌慌的,生怕ES集群一个扛不住,就全公司炸锅了。那为什么一到饭点就报警呢?因为流量比较大, 导致ES线程数飙高,cpu直往上窜,查询耗时增加,并传导给所有调用方,导致更大范围的延时。那么如何解决这个问题呢?通过深入ES集群,我们发现了以下几个问题: 


    • ES负载不合理,热点问题严重。ES主集群一共有几十个节点,有的节点上部署的shard数偏多,有的节点部署的shard数很少,导致某些服务器的负载很高,每到流量高峰期,就经常预警。


    • ES线程池的大小设置得太高,导致cpu飙高。我们知道,设置ES的threadpool,一般将线程数设置为服务器的cpu核数,即使ES的查询压力很大,需要增加线程数,那最好也不要超过“cpu core * 3 / 2 + 1”。如果设置的线程数过多,会导致cpu在多个线程上下文之间频繁来回切换,浪费大量cpu资源。


    • shard分配的内存太大,100g,导致查询变慢。我们知道,ES的索引要合理分配shard数,要控制一个shard的内存大小在50g以内。如果一个shard分配的内存过大,会导致查询变慢,耗时增加,严重拖累性能。


    • string类型的字段设置了双字段,既是text,又是keyword,导致存储容量增大了一倍。会员信息的查询不需要关联度打分,直接根据keyword查询就行,所以完全可以将text字段去掉,这样就能节省很大一部分存储空间,提升性能。


    • ES查询,使用filter,不使用query。因为query会对搜索结果进行相关度算分,比较耗cpu,而会员信息的查询是不需要算分的,这部分的性能损耗完全可以避免。


    • 节约ES算力,将ES的搜索结果排序放在会员系统的jvm内存中进行。


    • 增加routing key。我们知道,一次ES查询,会将请求分发给所有shard,等所有shard返回结果后再聚合数据,最后将结果返回给调用方。如果我们事先已经知道数据分布在哪些shard上,那么就可以减少大量不必要的请求,提升查询性能。


    经过以上优化,成果非常显著,ES集群的cpu大幅下降,查询性能大幅提升。ES集群的cpu使用率: 



    会员系统的接口耗时:


    三、会员Redis缓存方案


    一直以来,会员系统是不做缓存的,原因主要有两个:第一个,前面讲的ES集群性能很好,秒并发3万多,99线耗时5毫秒左右,已经足够应付各种棘手的场景。第二个,有的业务对会员的绑定关系要求实时一致,而会员是一个发展了10多年的老系统,是一个由好多接口、好多系统组成的分布式系统。所以,只要有一个接口没有考虑到位,没有及时去更新缓存,就会导致脏数据,进而引发一系列的问题,例如:用户在APP上看不到微信订单、APP和微信的会员等级、里程等没合并、微信和APP无法交叉营销等等。那后来为什么又要做缓存呢?是因为今年机票的盲盒活动,它带来的瞬时并发太高了。虽然会员系统安然无恙,但还是有点心有余悸,稳妥起见,最终还是决定实施缓存方案。


    1. ES近一秒延时导致的Redis缓存数据不一致问题的解决方案


    在做会员缓存方案的过程中,遇到一个ES引发的问题,该问题会导致缓存数据的不一致。我们知道,ES操作数据是近实时的,往ES新增一个Document,此时立即去查,是查不到的,需要等待1秒后才能查询到。如下图所示:



    ES的近实时机制为什么会导致redis缓存数据不一致呢?具体来讲,假设一个用户注销了自己的APP账号,此时需要更新ES,删除APP账号和微信账号的绑定关系。而ES的数据更新是近实时的,也就是说,1秒后你才能查询到更新后的数据。而就在这1秒内,有个请求来查询该用户的会员绑定关系,它先到redis缓存中查,发现没有,然后到ES查,查到了,但查到的是更新前的旧数据。最后,该请求把查询到的旧数据更新到redis缓存并返回。就这样,1秒后,ES中该用户的会员数据更新了,但redis缓存的数据还是旧数据,导致了redis缓存跟ES的数据不一致。如下图所示:



    面对该问题,如何解决呢?我们的思路是,在更新ES数据时,加一个2秒的redis分布式并发锁,为了保证缓存数据的一致性,接着再删除redis中该会员的缓存数据。如果此时有请求来查询数据,先获取分布式锁,发现该会员ID已经上锁了,说明ES刚刚更新的数据尚未生效,那么此时查询完数据后就不更新redis缓存了,直接返回,这样就避免了缓存数据的不一致问题。如下图所示:



    上述方案,乍一看似乎没什么问题了,但仔细分析,还是有可能导致缓存数据的不一致。例如,在更新请求加分布式锁之前,恰好有一个查询请求获取分布式锁,而此时是没有锁的,所以它可以继续更新缓存。但就在他更新缓存之前,线程block了,此时更新请求来了,加了分布式锁,并删除了缓存。当更新请求完成操作后,查询请求的线程活过来了,此时它再执行更新缓存,就把脏数据写到缓存中了。发现没有?主要的问题症结就在于“删除缓存”和“更新缓存”发生了并发冲突,只要将它们互斥,就能解决问题。如下图所示:



    实施了缓存方案后,经统计,缓存命中率90%+,极大缓解了ES的压力,会员系统整体性能得到了很大提升。


    2. Redis双中心多集群架构


    接下来,我们看一下如何保障Redis集群的高可用。如下图所示: 


    关于Redis集群的高可用,我们采用了双中心多集群的模式。在机房A和机房B各部署一套Redis集群。更新缓存数据时,双写,只有两个机房的redis集群都写成功了,才返回成功。查询缓存数据时,机房内就近查询,降低延时。这样,即使机房A整体故障,机房B还能提供完整的会员服务。


    四、高可用会员主库方案


    上述讲到,全平台会员的绑定关系数据存在ES,而会员的注册明细数据存在关系型数据库。最早,会员使用的数据库是SqlServer,直到有一天,DBA找到我们说,单台SqlServer数据库已经存储了十多亿的会员数据,服务器已达到物理极限,不能再扩展了。按照现在的增长趋势,过不了多久,整个SqlServer数据库就崩了。你想想,那是一种什么样的灾难场景:会员数据库崩了,会员系统就崩了;会员系统崩了,全公司所有业务线就崩了。想想就不寒而栗,酸爽无比,为此我们立刻开启了迁移DB的工作。


    1. MySql双中心Partition集群方案


    经过调研,我们选择了双中心分库分表的MySql集群方案,如下图所示:



    会员一共有十多亿的数据,我们把会员主库分了1000多个分片,平分到每个分片大概百万的量级,足够使用了。MySql集群采用1主3从的架构,主库放在机房A,从库放在机房B,两个机房之间通过专线同步数据,延迟在1毫秒内。会员系统通过DBRoute读写数据,写数据都路由到master节点所在的机房A,读数据都路由到本地机房,就近访问,减少网络延迟。这样,采用双中心的MySql集群架构,极大提高了可用性,即使机房A整体都崩了,还可以将机房B的Slave升级为Master,继续提供服务。


    双中心MySql集群搭建好后,我们进行了压测,测试下来,秒并发能达到2万多,平均耗时在10毫秒内,性能达标。


    2. 会员主库平滑迁移方案


    接下来的工作,就是把会员系统的底层存储从SqlServer切到MySql上,这是个风险极高的工作,主要有以下几个难点:


    • 会员系统是一刻都不能停机的,要在不停机的情况下完成SqlServer到MySql的切换,就像是在给高速行驶的汽车换轮子。


    • 会员系统是由很多个系统和接口组成的,毕竟发展了10多年,由于历史原因,遗留了大量老接口,逻辑错综复杂。这么多系统,必须一个不落的全部梳理清楚,DAL层代码必须重写,而且不能出任何问题,否则将是灾难性的。


    • 数据的迁移要做到无缝迁移,不仅是存量10多亿数据的迁移,实时产生的数据也要无缝同步到mysql。另外,除了要保障数据同步的实时性,还要保证数据的正确性,以及SqlServer和MySql数据的一致性。


    基于以上痛点,我们设计了“全量同步、增量同步、实时流量灰度切换”的技术方案。


    首先,为了保证数据的无缝切换,采用实时双写的方案。因为业务逻辑的复杂,以及SqlServer和MySql的技术差异性,在双写mysql的过程中,不一定会写成功,而一旦写失败,就会导致SqlServer和MySql的数据不一致,这是绝不允许的。所以,我们采取的策略是,在试运行期间,主写SqlServer,然后通过线程池异步写MySql,如果写失败了,重试三次,如果依然失败,则记日志,然后人工排查原因,解决后,继续双写,直到运行一段时间,没有双写失败的情况。通过上述策略,可以确保在绝大部分情况下,双写操作的正确性和稳定性,即使在试运行期间出现了SqlServer和MySql的数据不一致的情况,也可以基于SqlServer再次全量构建出MySql的数据,因为我们在设计双写策略时,会确保SqlServer一定能写成功,也就是说,SqlServer中的数据是全量最完整、最正确的。如下图所示:


     讲完了双写,接下来我们看一下“读数据”如何灰度。整体思路是,通过A/B平台逐步灰度流量,刚开始100%的流量读取SqlServer数据库,然后逐步切流量读取MySql数据库,先1%,如果没有问题,再逐步放流量,最终100%的流量都走MySql数据库。在逐步灰度流量的过程中,需要有验证机制,只有验证没问题了,才能进一步放大流量。那么这个验证机制如何实施呢?方案是,在一次查询请求里,通过异步线程,比较SqlServer和 MySql的查询结果是否一致,如果不一致,记日志,再人工检查不一致的原因,直到彻底解决不一致的问题后,再逐步灰度流量。如下图所示:



    所以,整体的实施流程如下:



    首先,在一个夜黑风高的深夜,流量最小的时候,完成SqlServer到MySql数据库的全量数据同步。接着,开启双写,此时,如果有用户注册,就会实时双写到两个数据库。那么,在全量同步和实时双写开启之间,两个数据库还相差这段时间的数据,所以需要再次增量同步,把数据补充完整,以防数据的不一致。剩下的时间,就是各种日志监控,看双写是否有问题,看数据比对是否一致等等。这段时间是耗时最长的,也是最容易发生问题的,如果有的问题比较严重,导致数据不一致了,就需要从头再来,再次基于SqlServer全量构建MySql数据库,然后重新灰度流量,直到最后,100%的流量全部灰度到MySql,此时就大功告成了,下线灰度逻辑,所有读写都切到MySql集群。


    3. MySql和ES主备集群方案


    做到这一步,感觉会员主库应该没问题了,可dal组件的一次严重故障改变了我们的想法。那次故障很恐怖,公司很多应用连接不上数据库了,创单量直线往下掉,这让我们意识到,即使数据库是好的,但dal组件异常,依然能让会员系统挂掉。所以,我们再次异构了会员主库的数据源,双写数据到ES,如下所示:



    如果dal组件故障或MySql数据库挂了,可以把读写切到ES,等MySql恢复了,再把数据同步到MySql,最后把读写再切回到MySql数据库。如下图所示:



    五、异常会员关系治理


    会员系统不仅仅要保证系统的稳定和高可用,数据的精准和正确也同样重要。举个例子,一个分布式并发故障,导致一名用户的APP账户绑定了别人的微信小程序账户,这将会带来非常恶劣的影响。首先,一旦这两个账号绑定了,那么这两个用户下的酒店、机票、火车票订单是互相可以看到的。你想想,别人能看到你订的酒店订单,你火不火,会不会投诉?除了能看到别人的订单,你还能操作订单。例如,一个用户在APP的订单中心,看到了别人订的机票订单,他觉得不是自己的订单,就把订单取消了。这将会带来非常严重的客诉,大家知道,机票退订费用是挺高的,这不仅影响了该用户的正常出行,还导致了比较大的经济损失,非常糟糕。


    针对这些异常会员账号,我们进行了详细的梳理,通过非常复杂烧脑的逻辑识别出这些账号,并对会员接口进行了深度优化治理,在代码逻辑层堵住了相关漏洞,完成了异常会员的治理工作。如下图所示:



    六、展望:更精细化的流控和降级策略


    任何一个系统,都不能保证百分之一百不出问题,所以我们要有面向失败的设计,那就是更精细化的流控和降级策略。


    1. 更精细化的流控策略


    热点控制。针对黑产刷单的场景,同一个会员id会有大量重复的请求,形成热点账号,当这些账号的访问超过设定阈值时,实施限流策略。


    基于调用账号的流控规则。这个策略主要是防止调用方的代码bug导致的大流量。例如,调用方在一次用户请求中,循环很多次来调用会员接口,导致会员系统流量暴增很多倍。所以,要针对每个调用账号设置流控规则,当超过阈值时,实施限流策略。


    全局流控规则。我们会员系统能抗下tps 3万多的秒并发请求量,如果此时,有个很恐怖的流量打过来,tps高达10万,与其让这波流量把会员数据库、es全部打死,还不如把超过会员系统承受范围之外的流量快速失败,至少tps 3万内的会员请求能正常响应,不会让整个会员系统全部崩溃。


    2. 更精细化的降级策略


    基于平均响应时间的降级。会员接口也有依赖其他接口,当调用其他接口的平均响应时间超过阈值,进入准降级状态。如果接下来 1s 内进入的请求,它们的平均响应时间都持续超过阈值,那么在接下的时间窗口内,自动地熔断。


    基于异常数和异常比例的降级。当会员接口依赖的其他接口发生异常,如果1分钟内的异常数超过阈值,或者每秒异常总数占通过量的比值超过阈值,进入降级状态,在接下的时间窗口之内,自动熔断。


    目前,我们最大的痛点是会员调用账号的治理。公司内,想要调用会员接口,必须申请一个调用账号,我们会记录该账号的使用场景,并设置流控、降级策略的规则。但在实际使用的过程中,申请了该账号的同事,可能异动到其他部门了,此时他可能也会调用会员系统,为了省事,他不会再次申请会员账号,而是直接沿用以前的账号过来调用,这导致我们无法判断一个会员账号的具体使用场景是什么,也就无法实施更精细的流控和降级策略。所以,接下来,我们将会对所有调用账号进行一个个的梳理,这是个非常庞大且繁琐的工作,但无路如何,硬着头皮也要做好。

  • ES+Redis+MySQL,这个高可用架构设计太顶了!

    快乐分享,Java干货及时送达👇

    来源:dbaplus
    • 一、背景
    • 二、ES高可用方案
    • 三、会员Redis缓存方案
    • 四、高可用会员主库方案
    • 五、异常会员关系治理
    • 六、展望:更精细化的流控和降级策略

    一、背景

    会员系统是一种基础系统,跟公司所有业务线的下单主流程密切相关。如果会员系统出故障,会导致用户无法下单,影响范围是全公司所有业务线。所以,会员系统必须保证高性能、高可用,提供稳定、高效的基础服务。

    随着同程和艺龙两家公司的合并,越来越多的系统需要打通同程APP、艺龙APP、同程微信小程序、艺龙微信小程序等多平台会员体系。例如微信小程序的交叉营销,用户买了一张火车票,此时想给他发酒店红包,这就需要查询该用户的统一会员关系。因为火车票用的是同程会员体系,酒店用的是艺龙会员体系,只有查到对应的艺龙会员卡号后,才能将红包挂载到该会员账号。除了上述讲的交叉营销,还有许多场景需要查询统一会员关系,例如订单中心、会员等级、里程、红包、常旅、实名,以及各类营销活动等等。所以,会员系统的请求量越来越大,并发量越来越高,今年五一小长假的秒并发tps甚至超过2万多。在如此大流量的冲击下,会员系统是如何做到高性能和高可用的呢?这就是本文着重要讲述的内容。

    二、ES高可用方案

    1. ES双中心主备集群架构

    同程和艺龙两家公司融合后,全平台所有体系的会员总量是十多亿。在这么大的数据体量下,业务线的查询维度也比较复杂。有的业务线基于手机号,有的基于微信unionid,也有的基于艺龙卡号等查询会员信息。这么大的数据量,又有这么多的查询维度,基于此,我们选择ES用来存储统一会员关系。ES集群在整个会员系统架构中非常重要,那么如何保证ES的高可用呢?

    首先我们知道,ES集群本身就是保证高可用的,如下图所示:

    图片

    当ES集群有一个节点宕机了,会将其他节点对应的Replica Shard升级为Primary Shard,继续提供服务。但即使是这样,还远远不够。例如ES集群都部署在机房A,现在机房A突然断电了,怎么办?例如服务器硬件故障,ES集群大部分机器宕机了,怎么办?或者突然有个非常热门的抢购秒杀活动,带来了一波非常大的流量,直接把ES集群打死了,怎么办?面对这些情况,让运维兄弟冲到机房去解决?这个非常不现实,因为会员系统直接影响全公司所有业务线的下单主流程,故障恢复的时间必须非常短,如果需要运维兄弟人工介入,那这个时间就太长了,是绝对不能容忍的。那ES的高可用如何做呢?我们的方案是ES双中心主备集群架构。

    我们有两个机房,分别是机房A和机房B。我们把ES主集群部署在机房A,把ES备集群部署在机房B。会员系统的读写都在ES主集群,通过MQ将数据同步到ES备集群。此时,如果ES主集群崩了,通过统一配置,将会员系统的读写切到机房B的ES备集群上,这样即使ES主集群挂了,也能在很短的时间内实现故障转移,确保会员系统的稳定运行。最后,等ES主集群故障恢复后,打开开关,将故障期间的数据同步到ES主集群,等数据同步一致后,再将会员系统的读写切到ES主集群。

    2. ES流量隔离三集群架构

    双中心ES主备集群做到这一步,感觉应该没啥大问题了,但去年的一次恐怖流量冲击让我们改变了想法。那是一个节假日,某个业务上线了一个营销活动,在用户的一次请求中,循环10多次调用了会员系统,导致会员系统的tps暴涨,差点把ES集群打爆。这件事让我们后怕不已,它让我们意识到,一定要对调用方进行优先级分类,实施更精细的隔离、熔断、降级、限流策略。首先,我们梳理了所有调用方,分出两大类请求类型。第一类是跟用户的下单主流程密切相关的请求,这类请求非常重要,应该高优先级保障。第二类是营销活动相关的,这类请求有个特点,他们的请求量很大,tps很高,但不影响下单主流程。基于此,我们又构建了一个ES集群,专门用来应对高tps的营销秒杀类请求,这样就跟ES主集群隔离开来,不会因为某个营销活动的流量冲击而影响用户的下单主流程。如下图所示:

    图片

    3. ES集群深度优化提升

    讲完了ES的双中心主备集群高可用架构,接下来我们深入讲解一下ES主集群的优化工作。有一段时间,我们特别痛苦,就是每到饭点,ES集群就开始报警,搞得每次吃饭都心慌慌的,生怕ES集群一个扛不住,就全公司炸锅了。那为什么一到饭点就报警呢?因为流量比较大, 导致ES线程数飙高,cpu直往上窜,查询耗时增加,并传导给所有调用方,导致更大范围的延时。那么如何解决这个问题呢?通过深入ES集群,我们发现了以下几个问题:

    • ES负载不合理,热点问题严重。ES主集群一共有几十个节点,有的节点上部署的shard数偏多,有的节点部署的shard数很少,导致某些服务器的负载很高,每到流量高峰期,就经常预警。
    • ES线程池的大小设置得太高,导致cpu飙高。我们知道,设置ES的threadpool,一般将线程数设置为服务器的cpu核数,即使ES的查询压力很大,需要增加线程数,那最好也不要超过“cpu core * 3 / 2 + 1”。如果设置的线程数过多,会导致cpu在多个线程上下文之间频繁来回切换,浪费大量cpu资源。
    • shard分配的内存太大,100g,导致查询变慢。我们知道,ES的索引要合理分配shard数,要控制一个shard的内存大小在50g以内。如果一个shard分配的内存过大,会导致查询变慢,耗时增加,严重拖累性能。
    • string类型的字段设置了双字段,既是text,又是keyword,导致存储容量增大了一倍。会员信息的查询不需要关联度打分,直接根据keyword查询就行,所以完全可以将text字段去掉,这样就能节省很大一部分存储空间,提升性能。
    • ES查询,使用filter,不使用query。因为query会对搜索结果进行相关度算分,比较耗cpu,而会员信息的查询是不需要算分的,这部分的性能损耗完全可以避免。
    • 节约ES算力,将ES的搜索结果排序放在会员系统的jvm内存中进行。
    • 增加routing key。我们知道,一次ES查询,会将请求分发给所有shard,等所有shard返回结果后再聚合数据,最后将结果返回给调用方。如果我们事先已经知道数据分布在哪些shard上,那么就可以减少大量不必要的请求,提升查询性能。

    经过以上优化,成果非常显著,ES集群的cpu大幅下降,查询性能大幅提升。ES集群的cpu使用率:

    会员系统的接口耗时:

    图片

    三、会员Redis缓存方案

    一直以来,会员系统是不做缓存的,原因主要有两个:第一个,前面讲的ES集群性能很好,秒并发3万多,99线耗时5毫秒左右,已经足够应付各种棘手的场景。第二个,有的业务对会员的绑定关系要求实时一致,而会员是一个发展了10多年的老系统,是一个由好多接口、好多系统组成的分布式系统。所以,只要有一个接口没有考虑到位,没有及时去更新缓存,就会导致脏数据,进而引发一系列的问题,例如:用户在APP上看不到微信订单、APP和微信的会员等级、里程等没合并、微信和APP无法交叉营销等等。那后来为什么又要做缓存呢?是因为今年机票的盲盒活动,它带来的瞬时并发太高了。虽然会员系统安然无恙,但还是有点心有余悸,稳妥起见,最终还是决定实施缓存方案。

    1. ES近一秒延时导致的Redis缓存数据不一致问题的解决方案

    在做会员缓存方案的过程中,遇到一个ES引发的问题,该问题会导致缓存数据的不一致。我们知道,ES操作数据是近实时的,往ES新增一个Document,此时立即去查,是查不到的,需要等待1秒后才能查询到。如下图所示:

    ES的近实时机制为什么会导致redis缓存数据不一致呢?具体来讲,假设一个用户注销了自己的APP账号,此时需要更新ES,删除APP账号和微信账号的绑定关系。而ES的数据更新是近实时的,也就是说,1秒后你才能查询到更新后的数据。而就在这1秒内,有个请求来查询该用户的会员绑定关系,它先到redis缓存中查,发现没有,然后到ES查,查到了,但查到的是更新前的旧数据。最后,该请求把查询到的旧数据更新到redis缓存并返回。就这样,1秒后,ES中该用户的会员数据更新了,但redis缓存的数据还是旧数据,导致了redis缓存跟ES的数据不一致。如下图所示:

    面对该问题,如何解决呢?我们的思路是,在更新ES数据时,加一个2秒的redis分布式并发锁,为了保证缓存数据的一致性,接着再删除redis中该会员的缓存数据。如果此时有请求来查询数据,先获取分布式锁,发现该会员ID已经上锁了,说明ES刚刚更新的数据尚未生效,那么此时查询完数据后就不更新redis缓存了,直接返回,这样就避免了缓存数据的不一致问题。如下图所示:

    图片

    上述方案,乍一看似乎没什么问题了,但仔细分析,还是有可能导致缓存数据的不一致。例如,在更新请求加分布式锁之前,恰好有一个查询请求获取分布式锁,而此时是没有锁的,所以它可以继续更新缓存。但就在他更新缓存之前,线程block了,此时更新请求来了,加了分布式锁,并删除了缓存。当更新请求完成操作后,查询请求的线程活过来了,此时它再执行更新缓存,就把脏数据写到缓存中了。发现没有?主要的问题症结就在于“删除缓存”和“更新缓存”发生了并发冲突,只要将它们互斥,就能解决问题。如下图所示:

    图片

    实施了缓存方案后,经统计,缓存命中率90%+,极大缓解了ES的压力,会员系统整体性能得到了很大提升。

    2. Redis双中心多集群架构

    接下来,我们看一下如何保障Redis集群的高可用。如下图所示:

    关于Redis集群的高可用,我们采用了双中心多集群的模式。在机房A和机房B各部署一套Redis集群。更新缓存数据时,双写,只有两个机房的redis集群都写成功了,才返回成功。查询缓存数据时,机房内就近查询,降低延时。这样,即使机房A整体故障,机房B还能提供完整的会员服务。

    四、高可用会员主库方案

    上述讲到,全平台会员的绑定关系数据存在ES,而会员的注册明细数据存在关系型数据库。最早,会员使用的数据库是SqlServer,直到有一天,DBA找到我们说,单台SqlServer数据库已经存储了十多亿的会员数据,服务器已达到物理极限,不能再扩展了。按照现在的增长趋势,过不了多久,整个SqlServer数据库就崩了。你想想,那是一种什么样的灾难场景:会员数据库崩了,会员系统就崩了;会员系统崩了,全公司所有业务线就崩了。想想就不寒而栗,酸爽无比,为此我们立刻开启了迁移DB的工作。

    1. MySql双中心Partition集群方案

    经过调研,我们选择了双中心分库分表的MySql集群方案,如下图所示:

    会员一共有十多亿的数据,我们把会员主库分了1000多个分片,平分到每个分片大概百万的量级,足够使用了。MySql集群采用1主3从的架构,主库放在机房A,从库放在机房B,两个机房之间通过专线同步数据,延迟在1毫秒内。会员系统通过DBRoute读写数据,写数据都路由到master节点所在的机房A,读数据都路由到本地机房,就近访问,减少网络延迟。这样,采用双中心的MySql集群架构,极大提高了可用性,即使机房A整体都崩了,还可以将机房B的Slave升级为Master,继续提供服务。

    双中心MySql集群搭建好后,我们进行了压测,测试下来,秒并发能达到2万多,平均耗时在10毫秒内,性能达标。

    2. 会员主库平滑迁移方案

    接下来的工作,就是把会员系统的底层存储从SqlServer切到MySql上,这是个风险极高的工作,主要有以下几个难点:

    • 会员系统是一刻都不能停机的,要在不停机的情况下完成SqlServer到MySql的切换,就像是在给高速行驶的汽车换轮子。
    • 会员系统是由很多个系统和接口组成的,毕竟发展了10多年,由于历史原因,遗留了大量老接口,逻辑错综复杂。这么多系统,必须一个不落的全部梳理清楚,DAL层代码必须重写,而且不能出任何问题,否则将是灾难性的。
    • 数据的迁移要做到无缝迁移,不仅是存量10多亿数据的迁移,实时产生的数据也要无缝同步到mysql。另外,除了要保障数据同步的实时性,还要保证数据的正确性,以及SqlServer和MySql数据的一致性。

    基于以上痛点,我们设计了“全量同步、增量同步、实时流量灰度切换”的技术方案。

    首先,为了保证数据的无缝切换,采用实时双写的方案。因为业务逻辑的复杂,以及SqlServer和MySql的技术差异性,在双写mysql的过程中,不一定会写成功,而一旦写失败,就会导致SqlServer和MySql的数据不一致,这是绝不允许的。所以,我们采取的策略是,在试运行期间,主写SqlServer,然后通过线程池异步写MySql,如果写失败了,重试三次,如果依然失败,则记日志,然后人工排查原因,解决后,继续双写,直到运行一段时间,没有双写失败的情况。通过上述策略,可以确保在绝大部分情况下,双写操作的正确性和稳定性,即使在试运行期间出现了SqlServer和MySql的数据不一致的情况,也可以基于SqlServer再次全量构建出MySql的数据,因为我们在设计双写策略时,会确保SqlServer一定能写成功,也就是说,SqlServer中的数据是全量最完整、最正确的。如下图所示:

    讲完了双写,接下来我们看一下“读数据”如何灰度。整体思路是,通过A/B平台逐步灰度流量,刚开始100%的流量读取SqlServer数据库,然后逐步切流量读取MySql数据库,先1%,如果没有问题,再逐步放流量,最终100%的流量都走MySql数据库。在逐步灰度流量的过程中,需要有验证机制,只有验证没问题了,才能进一步放大流量。那么这个验证机制如何实施呢?方案是,在一次查询请求里,通过异步线程,比较SqlServer和 MySql的查询结果是否一致,如果不一致,记日志,再人工检查不一致的原因,直到彻底解决不一致的问题后,再逐步灰度流量。如下图所示:

    图片

    所以,整体的实施流程如下:

    图片

    首先,在一个夜黑风高的深夜,流量最小的时候,完成SqlServer到MySql数据库的全量数据同步。接着,开启双写,此时,如果有用户注册,就会实时双写到两个数据库。那么,在全量同步和实时双写开启之间,两个数据库还相差这段时间的数据,所以需要再次增量同步,把数据补充完整,以防数据的不一致。剩下的时间,就是各种日志监控,看双写是否有问题,看数据比对是否一致等等。这段时间是耗时最长的,也是最容易发生问题的,如果有的问题比较严重,导致数据不一致了,就需要从头再来,再次基于SqlServer全量构建MySql数据库,然后重新灰度流量,直到最后,100%的流量全部灰度到MySql,此时就大功告成了,下线灰度逻辑,所有读写都切到MySql集群。

    3. MySql和ES主备集群方案

    做到这一步,感觉会员主库应该没问题了,可dal组件的一次严重故障改变了我们的想法。那次故障很恐怖,公司很多应用连接不上数据库了,创单量直线往下掉,这让我们意识到,即使数据库是好的,但dal组件异常,依然能让会员系统挂掉。所以,我们再次异构了会员主库的数据源,双写数据到ES,如下所示:

    如果dal组件故障或MySql数据库挂了,可以把读写切到ES,等MySql恢复了,再把数据同步到MySql,最后把读写再切回到MySql数据库。如下图所示:

    图片

    五、异常会员关系治理

    会员系统不仅仅要保证系统的稳定和高可用,数据的精准和正确也同样重要。举个例子,一个分布式并发故障,导致一名用户的APP账户绑定了别人的微信小程序账户,这将会带来非常恶劣的影响。首先,一旦这两个账号绑定了,那么这两个用户下的酒店、机票、火车票订单是互相可以看到的。你想想,别人能看到你订的酒店订单,你火不火,会不会投诉?除了能看到别人的订单,你还能操作订单。例如,一个用户在APP的订单中心,看到了别人订的机票订单,他觉得不是自己的订单,就把订单取消了。这将会带来非常严重的客诉,大家知道,机票退订费用是挺高的,这不仅影响了该用户的正常出行,还导致了比较大的经济损失,非常糟糕。

    针对这些异常会员账号,我们进行了详细的梳理,通过非常复杂烧脑的逻辑识别出这些账号,并对会员接口进行了深度优化治理,在代码逻辑层堵住了相关漏洞,完成了异常会员的治理工作。如下图所示:

    图片

    六、展望:更精细化的流控和降级策略

    任何一个系统,都不能保证百分之一百不出问题,所以我们要有面向失败的设计,那就是更精细化的流控和降级策略。

    1. 更精细化的流控策略

    热点控制。针对黑产刷单的场景,同一个会员id会有大量重复的请求,形成热点账号,当这些账号的访问超过设定阈值时,实施限流策略。

    基于调用账号的流控规则。这个策略主要是防止调用方的代码bug导致的大流量。例如,调用方在一次用户请求中,循环很多次来调用会员接口,导致会员系统流量暴增很多倍。所以,要针对每个调用账号设置流控规则,当超过阈值时,实施限流策略。

    全局流控规则。我们会员系统能抗下tps 3万多的秒并发请求量,如果此时,有个很恐怖的流量打过来,tps高达10万,与其让这波流量把会员数据库、es全部打死,还不如把超过会员系统承受范围之外的流量快速失败,至少tps 3万内的会员请求能正常响应,不会让整个会员系统全部崩溃。

    图片

    2. 更精细化的降级策略

    基于平均响应时间的降级。会员接口也有依赖其他接口,当调用其他接口的平均响应时间超过阈值,进入准降级状态。如果接下来 1s 内进入的请求,它们的平均响应时间都持续超过阈值,那么在接下的时间窗口内,自动地熔断。

    基于异常数和异常比例的降级。当会员接口依赖的其他接口发生异常,如果1分钟内的异常数超过阈值,或者每秒异常总数占通过量的比值超过阈值,进入降级状态,在接下的时间窗口之内,自动熔断。

    目前,我们最大的痛点是会员调用账号的治理。公司内,想要调用会员接口,必须申请一个调用账号,我们会记录该账号的使用场景,并设置流控、降级策略的规则。但在实际使用的过程中,申请了该账号的同事,可能异动到其他部门了,此时他可能也会调用会员系统,为了省事,他不会再次申请会员账号,而是直接沿用以前的账号过来调用,这导致我们无法判断一个会员账号的具体使用场景是什么,也就无法实施更精细的流控和降级策略。所以,接下来,我们将会对所有调用账号进行一个个的梳理,这是个非常庞大且繁琐的工作,但无路如何,硬着头皮也要做好。

    
    

  • 面试官:一千万的数据,你是怎么查询的?

    快乐分享,Java干货及时送达👇

    来源:juejin.cn/post/6863668253898735629
    • 前言

    • 准备数据

      • 创建表

      • 创建数据脚本

    • 开始测试

      • 普通分页查询

    • 如何优化

      • 优化偏移量大问题

      • 优化数据量大问题

    • SELECT * 它不香吗?

    • 结束


    前言

    • 面试官:来说说,一千万的数据,你是怎么查询的?
    • B哥:直接分页查询,使用limit分页。
    • 面试官:有实操过吗?
    • B哥:肯定有呀

    此刻献上一首《凉凉》

    也许有些人没遇过上千万数据量的表,也不清楚查询上千万数据量的时候会发生什么。

    今天就来带大家实操一下,这次是基于MySQL 5.7.26做测试

    准备数据

    没有一千万的数据怎么办?

    创建呗

    代码创建一千万?那是不可能的,太慢了,可能真的要跑一天。可以采用数据库脚本执行速度快很多。

    创建表

    CREATE TABLE `user_operation_log`  (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `user_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      `ip` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      `op_data` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      `attr1` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      `attr2` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      `attr3` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      `attr4` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      `attr5` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      `attr6` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      `attr7` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      `attr8` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      `attr9` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      `attr10` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      `attr11` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      `attr12` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

    创建数据脚本

    采用批量插入,效率会快很多,而且每1000条数就commit,数据量太大,也会导致批量插入效率慢

    DELIMITER ;;
    CREATE PROCEDURE batch_insert_log()
    BEGIN
      DECLARE i INT DEFAULT 1;
      DECLARE userId INT DEFAULT 10000000;
     set @execSql = 'INSERT INTO `test`.`user_operation_log`(`user_id`, `ip`, `op_data`, `attr1`, `attr2`, `attr3`, `attr4`, `attr5`, `attr6`, `attr7`, `attr8`, `attr9`, `attr10`, `attr11`, `attr12`) VALUES';
     set @execData = '';
      WHILE i   set @attr = "'测试很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长的属性'";
      set @execData = concat(@execData, "(", userId + i, ", '10.0.69.175', '用户登录操作'"",", @attr, ",", @attr, ",", @attr, ",", @attr, ",", @attr, ",", @attr, ",", @attr, ",", @attr, ",", @attr, ",", @attr, ",", @attr, ",", @attr, ")");
      if i % 1000 = 0
      then
         set @stmtSql = concat(@execSql, @execData,";");
        prepare stmt from @stmtSql;
        execute stmt;
        DEALLOCATE prepare stmt;
        commit;
        set @execData = "";
       else
         set @execData = concat(@execData, ",");
       end if;
      SET i=i+1;
      END WHILE;

    END;;
    DELIMITER ;

    开始测试

    哥的电脑配置比较低:win10 标压渣渣i5 读写约500MB的SSD

    由于配置低,本次测试只准备了3148000条数据,占用了磁盘5G(还没建索引的情况下),跑了38min,电脑配置好的同学,可以插入多点数据测试

    SELECT count(1) FROM `user_operation_log`

    返回结果:3148000

    三次查询时间分别为:

    • 14060 ms
    • 13755 ms
    • 13447 ms

    普通分页查询

    MySQL 支持 LIMIT 语句来选取指定的条数数据, Oracle 可以使用 ROWNUM 来选取。

    MySQL分页查询语法如下:

    SELECT * FROM table LIMIT [offset,] rows | rows OFFSET offset
    • 第一个参数指定第一个返回记录行的偏移量
    • 第二个参数指定返回记录行的最大数目

    下面我们开始测试查询结果:

    SELECT * FROM `user_operation_log` LIMIT 10000, 10

    查询3次时间分别为:

    • 59 ms
    • 49 ms
    • 50 ms

    这样看起来速度还行,不过是本地数据库,速度自然快点。

    换个角度来测试

    相同偏移量,不同数据量

    SELECT * FROM `user_operation_log` LIMIT 10000, 10
    SELECT * FROM `user_operation_log` LIMIT 10000, 100
    SELECT * FROM `user_operation_log` LIMIT 10000, 1000
    SELECT * FROM `user_operation_log` LIMIT 10000, 10000
    SELECT * FROM `user_operation_log` LIMIT 10000, 100000
    SELECT * FROM `user_operation_log` LIMIT 10000, 1000000

    查询时间如下:

    图片

    从上面结果可以得出结束:数据量越大,花费时间越长

    相同数据量,不同偏移量

    SELECT * FROM `user_operation_log` LIMIT 100, 100
    SELECT * FROM `user_operation_log` LIMIT 1000, 100
    SELECT * FROM `user_operation_log` LIMIT 10000, 100
    SELECT * FROM `user_operation_log` LIMIT 100000, 100
    SELECT * FROM `user_operation_log` LIMIT 1000000, 100

    图片

    从上面结果可以得出结束:偏移量越大,花费时间越长

    SELECT * FROM `user_operation_log` LIMIT 100, 100
    SELECT id, attr FROM `user_operation_log` LIMIT 100, 100

    如何优化

    既然我们经过上面一番的折腾,也得出了结论,针对上面两个问题:偏移大、数据量大,我们分别着手优化

    优化偏移量大问题

    采用子查询方式

    我们可以先定位偏移位置的 id,然后再查询数据

    SELECT * FROM `user_operation_log` LIMIT 1000000, 10SELECT id FROM `user_operation_log` LIMIT 1000000, 1SELECT * FROM `user_operation_log` WHERE id >= (SELECT id FROM `user_operation_log` LIMIT 1000000, 1) LIMIT 10

    查询结果如下:

    从上面结果得出结论:

    • 第一条花费的时间最大,第三条比第一条稍微好点
    • 子查询使用索引速度更快

    缺点:只适用于id递增的情况

    id非递增的情况可以使用以下写法,但这种缺点是分页查询只能放在子查询里面

    注意:某些 mysql 版本不支持在 in 子句中使用 limit,所以采用了多个嵌套select

    SELECT * FROM `user_operation_log` WHERE id IN (SELECT t.id FROM (SELECT id FROM `user_operation_log` LIMIT 1000000, 10) AS t)

    采用 id 限定方式

    这种方法要求更高些,id必须是连续递增,而且还得计算id的范围,然后使用 between,sql如下

    SELECT * FROM `user_operation_log` WHERE id between 1000000 AND 1000100 LIMIT 100

    SELECT * FROM `user_operation_log` WHERE id >= 1000000 LIMIT 100

    查询结果如下:

    图片

    从结果可以看出这种方式非常快

    注意:这里的 LIMIT 是限制了条数,没有采用偏移量

    优化数据量大问题

    返回结果的数据量也会直接影响速度

    SELECT * FROM `user_operation_log` LIMIT 1, 1000000

    SELECT id FROM `user_operation_log` LIMIT 1, 1000000

    SELECT id, user_id, ip, op_data, attr1, attr2, attr3, attr4, attr5, attr6, attr7, attr8, attr9, attr10, attr11, attr12 FROM `user_operation_log` LIMIT 1, 1000000

    查询结果如下:

    图片

    从结果可以看出减少不需要的列,查询效率也可以得到明显提升

    第一条和第三条查询速度差不多,这时候你肯定会吐槽,那我还写那么多字段干啥呢,直接 * 不就完事了

    注意本人的 MySQL 服务器和客户端是在_同一台机器_上,所以查询数据相差不多,有条件的同学可以测测客户端与MySQL分开

    SELECT * 它不香吗?

    在这里顺便补充一下为什么要禁止 SELECT *。难道简单无脑,它不香吗?

    主要两点:

    • 用 “SELECT * ” 数据库需要解析更多的对象、字段、权限、属性等相关内容,在 SQL 语句复杂,硬解析较多的情况下,会对数据库造成沉重的负担。
    • 增大网络开销,* 有时会误带上如log、IconMD5之类的无用且大文本字段,数据传输size会几何增涨。特别是MySQL和应用程序不在同一台机器,这种开销非常明显。

    结束

    最后还是希望大家自己去实操一下,肯定还可以收获更多,欢迎留言!!

    创建脚本我给你正好了,你还在等什么!!!

    
    

  • Spring Boot 优雅实现多租户架构,so easy!

    快乐分享,Java干货及时送达👇

    一、概述

    1.什么是多租户架构?

    多租户架构是指在一个应用中支持多个租户(Tenant)同时访问,每个租户拥有独立的资源和数据,并且彼此之间完全隔离。通俗来说,多租户就是把一个应用按照客户的需求“分割”成多个独立的实例,每个实例互不干扰。

    2. 多租户架构的优势

    • 更好地满足不同租户的个性化需求。
    • 可以降低运维成本,减少硬件、网络等基础设施的投入。
    • 节约开发成本,通过复用代码,快速上线新的租户实例。
    • 增强了系统的可扩展性和可伸缩性,支持水平扩展,每个租户的数据和资源均可管理和控制。

    3. 实现多租户架构的技术选择

    对于实现多租户架构技术不是最重要的最重要的是正确的架构思路。但是选择正确的技术可以更快地实现多租户架构。

    二、设计思路

    1. 架构选型

    基于Java开发多租户应用推荐使用Spring Boot和Spring Cloud。Spring Boot能快速搭建应用并提供许多成熟的插件。Spring Cloud则提供了许多实现微服务架构的工具和组件。

    1.1 Spring Boot

    使用Spring Boot可以简化项目的搭建过程自动配置许多常见的第三方库和组件,减少了开发人员的工作量。

    @RestController
    public class TenantController {

        @GetMapping("/hello")
        public String hello(@RequestHeader("tenant-id") String tenantId) {
            return "Hello, " + tenantId;
        }
    }

    1.2 Spring Cloud

    在架构多租户的系统时Spring Cloud会更加有用。Spring Cloud提供了一些成熟的解决方案,如Eureka、Zookeeper、Consul等,以实现服务发现、负载均衡等微服务功能。

    2. 数据库设计

    在多租户环境中数据库必须为每个租户分别存储数据并确保数据隔离。我们通常使用以下两种方式实现:

    • 多个租户共享相同的数据库,每个表中都包含tenant_id这一列,用于区分不同租户的数据。
    • 为每个租户创建单独的数据库,每个数据库内的表结构相同,但数据相互隔离。

    3. 应用多租户部署

    为了实现多租户在应用部署时我们需要考虑以下两个问题。

    3.1 应用隔离

    在多租户环境中不同租户需要访问不同的资源,因此需要进行应用隔离。可以通过构建独立的容器或虚拟机、使用命名空间等方式实现。Docker就是一种非常流行的隔离容器技术。

    3.2 应用配置

    由于每个租户都有自己的配置需求因此需要为每个租户分别设置应用配置信息,例如端口号、SSL证书等等。这些配置可以存储在数据库中,也可以存储在云配置中心中。

    4. 租户管理

    在多租户系统中需要能够管理不同租户的数据和资源,同时需要为每个租户分配相应的权限。解决方案通常包括以下两部分。

    4.1 租户信息维护

    租户信息的维护包括添加、修改、删除、查询等操作,要求能够根据租户名称或租户ID快速查找对应的租户信息。

    CREATE TABLE tenant (
        id BIGINT AUTO_INCREMENT PRIMARY KEY,
        name VARCHAR(50)
     NOT NULL UNIQUE,
        description VARCHAR(255),
        created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
        updated_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
    )
    ;

    4.2 租户权限控制

    在多租户应用中必须为每个租户分别设置对系统资源的访问权限。例如,A租户和B租户不能访问彼此的数据。

    @EnableGlobalMethodSecurity(prePostEnabled = true)
    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    .antMatchers("/api/tenant/**").hasRole("ADMIN")
                    .anyRequest().authenticated()
                    .and()
                    .formLogin();
        }

        @Autowired
        public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userDetailsService())
                    .passwordEncoder(new BCryptPasswordEncoder())
                    .and()
                    .inMemoryAuthentication()
                    .withUser("admin")
                    .password(new BCryptPasswordEncoder().encode("123456"))
                    .roles("ADMIN");
        }
    }

    三、技术实现

    1. Spring Boot中的多租户实现

    在Spring Boot中可以通过多数据源和动态路由来实现多租户机制。

    1.1 多数据源实现

    多数据源是指为不同的租户配置不同的数据源,使得每个租户都可以访问自己的独立数据。具体实现方法如下:

    @Configuration
    public class DataSourceConfig {
        @Bean(name = "dataSourceA")
        @ConfigurationProperties(prefix = "spring.datasource.a")
        public DataSource dataSourceA() {
            return DataSourceBuilder.create().build();
        }

        @Bean(name = "dataSourceB")
        @ConfigurationProperties(prefix = "spring.datasource.b")
        public DataSource dataSourceB() {
            return DataSourceBuilder.create().build();
        }

        @Bean(name = "dataSourceC")
        @ConfigurationProperties(prefix = "spring.datasource.c")
        public DataSource dataSourceC() {
            return DataSourceBuilder.create().build();
        }
    }

    以上代码是配置了三个数据源分别对应三个租户。然后在使用时,可以使用注解标记需要连接的数据源。

    @Service
    public class ProductService {
        @Autowired
        @Qualifier("dataSourceA")
        private DataSource dataSource;

        // ...
    }

    1.2 动态路由实现

    动态路由是指根据请求的URL或参数动态地切换到对应租户的数据源。具体实现如下:

    public class DynamicDataSource extends AbstractRoutingDataSource {
        @Override
        protected Object determineCurrentLookupKey() {
            return TenantContextHolder.getTenantId();
        }
    }

    @Configuration
    public class DataSourceConfig {
        @Bean(name = "dataSource")
        @ConfigurationProperties(prefix = "spring.datasource")
        public DataSource dataSource() {
            return DataSourceBuilder.create().type(DynamicDataSource.class).build();
        }
    }

    以上是动态路由的核心代码DynamicDataSource继承自AbstractRoutingDataSource,通过determineCurrentLookupKey()方法动态获得租户ID,然后切换到对应的数据源。关注公众号:码猿技术专栏,回复关键词:1111 获取阿里内部Java性能调优手册!

    2. Spring Cloud中的多租户实现

    在Spring Cloud中可以通过服务注册与发现、配置中心、负载均衡等方式实现多租户机制。

    2.1 服务注册与发现

    使用Spring Cloud中的Eureka实现服务注册与发现。每个租户的服务都在注册中心以不同的应用名称进行注册,客户端可以通过服务名称来访问对应租户的服务。

    2.2 配置中心

    使用Spring Cloud Config作为配置中心。配置文件以租户ID进行区分,客户端通过读取对应租户的配置文件来获取配置信息。

    2.3 负载均衡

    使用Spring Cloud Ribbon作为负载均衡器。根据请求的URL或参数选择对应租户的服务实例进行请求转发。

    2.4 API

    在API网关层面实现多租户机制根据请求的URL或参数判断所属租户,并转发到对应租户的服务实例。

    四、 应用场景

    1. 私有云环境

    私有云环境指的是由企业自行搭建的云环境,不对外提供服务,主要应用于企业内部的数据存储、管理、共享和安全控制。相较于公有云,私有云的优点在于可以更好地保护企业核心数据,同时也能够满足企业对于数据安全性和可控性的要求。

    2. 公有云环境

    公有云环境指的是由云服务商搭建并对外提供服务的云环境,用户可以根据需要购买相应的云服务,如云存储、云计算、云数据库等。相较于私有云,公有云的优点在于具有成本低廉、弹性伸缩、全球化部署等特点,能够更好地满足企业快速发展的需求。

    3. 企业级应用

    企业级应用是指面向企业客户的应用程序,主要包括ERP、CRM、OA等一系列应用系统。这类应用的特点在于功能强大、流程复杂、数据量大,需要满足企业的高效率、高可靠性、高安全性和易维护性等要求。在云计算环境下,企业可以将这些应用部署在私有云或公有云上,减少了硬件设备的投入和维护成本,提高了管理效率。

    五、实现步骤

    1. 搭建Spring Boot和Spring Cloud环境

    首先需要在Maven项目中引入以下依赖:


    dependency>
        groupId>org.springframework.bootgroupId>
        artifactId>spring-boot-starter-webartifactId>
    dependency>


    dependency>
        groupId>org.springframework.cloudgroupId>
        artifactId>spring-cloud-dependenciesartifactId>
        version>2020.0.3version>
        type>pomtype>
        scope>importscope>
    dependency>

    然后需要在application.yml中配置相应的参数,如下所示:

    spring:
      datasource:
        url: jdbc:mysql://localhost:3306/appdb?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
        username: root
        password: 123456

    mybatis:
      type-aliases-package: com.example.demo.model
      mapper-locations: classpath:mapper/*.xml

    server:
      port: 8080

    eureka:
      client:
        serviceUrl:
          defaultZone: http://localhost:8761/eureka/

    management:
      endpoints:
        web:
          exposure:
            include: "*"

    其中datasource.url为数据库连接的URL,username和password为数据库连接的账号和密码;server.port为Spring Boot应用启动的端口;eureka.client.serviceUrl.defaultZone为Eureka服务注册中心的URL。

    Java指南:java-family.cn

    2. 修改数据库设计

    接下来需要对数据库进行相应的修改,以支持多租户部署。具体来说,我们需要在数据库中添加一个与租户相关的字段,以便在应用中区分不同的租户。

    3. 实现应用多租户部署

    接着需要在代码中实现应用的多租户部署功能。具体来说,我们需要为每个租户实例化对应的Spring Bean,并根据租户ID将请求路由到相应的Bean中去处理。

    以下是一个简单的实现示例:

    @Configuration
    public class MultiTenantConfig {
     
        // 提供对应租户的数据源
        @Bean
        public DataSource dataSource(TenantRegistry tenantRegistry) {
            return new TenantAwareDataSource(tenantRegistry);
        }
     
        // 多租户Session工厂
        @Bean(name = "sqlSessionFactory")
        public SqlSessionFactory sqlSessionFactory(DataSource dataSource)
                throws Exception 
    {
            SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
            sessionFactory.setDataSource(dataSource);
            return sessionFactory.getObject();
        }
     
        // 动态切换租户
        @Bean
        public MultiTenantInterceptor multiTenantInterceptor(TenantResolver tenantResolver) {
            MultiTenantInterceptor interceptor = new MultiTenantInterceptor();
            interceptor.setTenantResolver(tenantResolver);
            return interceptor;
        }
     
        // 注册拦截器
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(multiTenantInterceptor());
        }
     
        // 注册租户信息
        @Bean
        public TenantRegistry tenantRegistry() {
            return new TenantRegistryImpl();
        }
         
        // 解析租户ID
        @Bean
        public TenantResolver tenantResolver() {
            return new HeaderTenantResolver();
        }
     
    }

    其中MultiTenantConfig是多租户部署的核心配置类,它提供了对应租户数据源、多租户Session工厂、动态切换租户等功能。

    4. 实现租户管理

    最后需要实现一个租户管理的功能,以便在系统中管理不同的租户。具体来说,我们可以使用Spring Cloud的服务注册与发现组件Eureka来注册每个租户的实例,并在管理界面中进行相应的操作。另外,我们还需要为每个租户提供一个独立的数据库,以保证数据隔离性。

    六、小结回顾

    本文详细介绍了如何使用Spring Boot和Spring Cloud实现一个支持多租户部署的应用。主要包括搭建Spring Boot和Spring Cloud环境、修改数据库设计、实现应用多租户部署、实现租户管理等方面。

    应用场景主要包括SaaS应用、多租户云服务等。优劣势主要体现在提升了应用的可扩展性和可维护性,但也增加了部署和管理的复杂度。未来的改进方向可以考虑进一步提升多租户管理的自动化程度,减少人工干预和错误率。

    来源:blog.csdn.net/u010349629/article

  • SpringBoot + 规则引擎 URule,真的很强!

    快乐分享,Java干货及时送达👇

    没有规则,不成方圆;

    一、背景

    前段时间,在做项目重构的时候,遇到很多地方需要做很多的条件判断。当然可以用很多的if-else判断去解决,但是当时也不清楚怎么回事,就想玩点别的。于是乎,就去调研了规则引擎。

    当然,市面上有很多成熟的规则引擎,功能很多,性能很好。但是,就是想玩点不一样的(大家做技术选型别这样,这个是反面教材)。最终一款URule的规则引擎吸引了我,主要还是采用浏览器可直接配置,不需要过多安装,可视化规则也做的不错。经过一系列调研,后面就把它接入了项目中,顺便记录下调研的结果。

    二、介绍

    规则引擎其实是一种组件,它可以嵌入到程序当中。将程序复杂的判断规则从业务代码中剥离出来,使得程序只需要关心自己的业务,而不需要去进行复杂的逻辑判断;简单的理解是规则接受一组输入的数据,通过预定好的规则配置,再输出一组结果。

    当然,市面上有很多成熟的规则引擎,如:Drools、Aviator、EasyRules等等。但是URule,它可以运行在Windows、Linux、Unix等各种类型的操作系统之上,采用纯浏览器的编辑模式,不需要安装工具,直接在浏览器上编辑规则和测试规则。

    当然这款规则引擎有开源和pro版本的区别,至于pro版是啥,懂的都懂,下面放个表格,了解下具体的区别

    特性 PRO版 开源版
    向导式决策集
    脚本式决策集
    决策树
    决策流
    决策表
    交叉决策表
    复杂评分卡
    文件名、项目名重构
    参数名、变量常量名重构
    Excel决策表导入
    规则集模版保存与加载
    中文项目名和文件名支持
    服务器推送知识包到客户端功能的支持
    知识包优化与压缩的支持
    客户端服务器模式下大知识包的推拉支持
    规则集中执行组的支持
    规则流中所有节点向导式条件与动作配置的支持
    循环规则多循环单元支持
    循环规则中无条件执行的支持
    导入项目自动重命名功能
    规则树构建优化
    对象查找索引支持
    规则树中短路计算的支持
    规则条件冗余计算缓存支持
    基于方案的批量场景测试功能
    知识包调用监控
    更为完善的文件读写权限控制
    知识包版本控制
    SpringBean及Java类的热部署
    技术支持

    三、安装使用

    实际使用时,有四种使用URule Pro的方式,分别是嵌入式模式、本地模式、分布式计算模式以及独立服务模式。

    但是我们这里不考虑URule Pro,咱自己整个开源版,在开源版集成springboot的基础上做一个二次开发,搜了一圈,其实就有解决方案。大致的项目模块如下:

    图片

    自己创建个空数据库,只需要在edas-rule-server服务中修改下数据库的配置,然后启动服务即可。第一次启动完成,数据库中会创建表。

    properties
    复制代码spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
    spring.datasource.driver-class-name=com.mysql.jdbc.Driver
    spring.datasource.url=jdbc:mysql://localhost:3306/urule-data?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false
    spring.datasource.username=root
    spring.datasource.password=mysql

    上面说过,它是纯用浏览器进行编辑,配置规则的,只需要打开浏览器,输入地址:http://localhost:8090/urule/frame,看到这个界面,就说明启动成功了。

    图片

    四、基础概念

    3.1整体介绍

    先说下URule它的构成部分,主要是两部分:1、设计器部分 2、规则执行引擎。设计器部分主要是库文件和规则文件构成。下面看下整体的结构图

    图片

    3.2库文件

    如上图介绍的,库文件有4种,包括变量库,参数库,常量库和动作库。其实类似于Java开发的系统中的实体对象,枚举,常量以及方法。

    上面说过,规则都是可视化配置的。在配置规则的过程中,就需要引入各种已经定义好的库文件,再结合业务需求,从而配置出符合业务场景的业务规则,所以哪里都有库文件的身影。

    3.2.1变量库文件

    在业务开发中,我们会创建很多Getter和Setter的Java类,比如PO、VO、BO、DTO、POJO等等,其实这些类new对象后主要起到的作用就是数据的载体,用来传输数据。

    在URule中,变量库就是用来映射这些对象,然后可以在规则中使用,最终完成业务和规则的互动。最后上一张图,用来创建变量库

    图片

    对了,上面废话了这么多可视化配置,这才是第一次展示配置界面,惭愧惭愧。

    上图一目了然,在“库”这个菜单底下右键,然后点击添加变量库即可,最后定义自己喜欢的变量库名,当然名字只支持中文或者英文,其他字符不可用。

    图片

    创建完变量库后,就可以对变量库进行编辑,可以认为就是给POJO添加属性

    图片

    也不弯弯绕绕讲什么术语,就个人理解。图左边是创建类,其中名称是它的别名,配置规则用它代替这个类。图右边是类的属性,我这里随便写了几个,估计看了懂得都懂。

    最后在业务系统中创建对应的类,注意全限定名和配置变量库的类路径一致。

    package com.cicada;

    import com.bstek.urule.model.Label;
    import lombok.Data;

    /**
     * @author 往事如风
     * @version 1.0
     * @date 2023/3/3 15:38
     * @description
     */
    @Data
    public class Stu {

        @Label("姓名")
        private String name;

        @Label("年龄")
        private int age;

        @Label("班级")
        private String classes;
    }

    最后说下这个@Label注解,这个是由URule提供的注解,主要是描述字段的属性,跟变量库的标题一栏一致就行。听官方介绍可以通过这个注解,实现POJO属性和变量库属性映射。就是POJO写好,然后对应规则的变量库就不需要重新写,可以直接生成。反正就有这个功能,这里就直接一笔带过了。

    3.2.2常量库文件

    说到常量库,这个就可以认为是我们Java系统中的常量,枚举。比如性别,要定义枚举吧;比如对接的机构,也可以定义一个枚举吧。

    当然,类似于变量库,常量库也可以实现和系统中的枚举相互映射,这样做的好处可以避免我们手动输入,防止输入错误。创建常量库也比较简单,直接在“库”这个菜单下右键,“添加常量库”。

    创建好常量库文件后,也会出现如下页面:

    图片

    3.2.3参数库文件

    参数库,就是URule规则中的临时变量,变量的类型和数量不固定。可以认为类似于Map,实际上存储参数库的也就是个Map。

    同样的套路,直接在“库”这个菜单下右键,“添加参数库”。

    可以看到,参数库已经少了左边分类这一项,直接添加参数,选择类型就是干,相对简单了很多。“名称”这列我这里用了英文,就是Map中的key,而“标题”这列就是在配置规则时候显示用的,中文看着比较直观。

    当然还需要注意的点是,定义的名称要保证唯一,因为Map中的key是唯一的,不然就会存在覆盖的情况。

    3.2.4动作库文件

    动作库可以对配置在spring中的bean方法进行映射,然后可以在规则中直接调用这批方法。惯用套路,还是在“库”菜单下右键,点击“添加动作库”。

    图片

    然后我在系统中添加了一个类Action,然后在类上标记@Component注解,将该类交给spring的bean容器管理。该类中添加一些方法,在方法上标记@ExposeAction注解,该注解是URule定义的,说明被标记的方法都会被动作库读取到。

    package com.bstek.urule.cicada;

    import com.bstek.urule.action.ActionId;
    import com.bstek.urule.model.ExposeAction;
    import org.springframework.stereotype.Component;

    import java.text.SimpleDateFormat;
    import java.util.Date;

    /**
     * @author 往事如风
     * @version 1.0
     * @date 2023/3/10 13:59
     * @description
     */
    @Component("action")
    public class Action {

        @ActionId("Hello")
        public String hello(){
            return "hello";
        }

        @ExposeAction(value="方法1")
        public boolean evalTest(String username){
            if(username==null){
                return false;
            }else if(username.equals("张三")){
                return true;
            }
            return false;
        }

        @ExposeAction(value="测试Int")
        public int testInt(int a,int b){
            return a+b;
        }

        @ExposeAction(value="打印内容")
        public void printContent(String username, Date birthday){
            SimpleDateFormat sd=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            if(birthday!=null){
                System.out.println(username+"今年已经"+sd.format(birthday)+"岁了!");
            }else{
                System.out.println("Hello "+username+"");
            }
        }
        
        @ExposeAction(value="打印Stu")
        public void printUser(Stu m){
            System.out.println("Hello "+m.getName()+", is age:"+m.getAge());
        }
    }

    最后在动作库页面上添加bean,“Bean Id”一列输入对应的spring bean的名称,这里输入action。然后点击操作列中的小手按钮,就会弹出刚在Action类中标记了ExposeAction注解的方法。选择一个指定的方法添加进来,最后看到方法对应的参数也会被自动加载进去。

    最后,变量库、参数库、动作库、常量库这些库文件定义好后,各种规则文件配置的时候就可以导入他们。但是一旦这些库文件被某个规则文件使用,就不要随意修改库文件了。

    我们创建了一个高质量的技术交流群,与优秀的人在一起,自己也会优秀起来,赶紧点击加群,享受一起成长的快乐。另外,如果你最近想跳槽的话,年前我花了2周时间收集了一波大厂面经,节后准备跳槽的可以点击这里领取!

    3.3规则集

    说到规则集,顾名思义,就是配置规则了。前面定义的库文件就需要导入到规则集中去配置使用。它是使用频率最高的一个业务规则实现方式。

    规则集说的是规则的集合,由三个部分规则组成:如果、那么、否则。

    在规则集的定义的方式上,URule由向导式和脚本式两种;

    • 向导式规则集:就是在页面上通过鼠标点点点,高度的可视化配置,不是开发都能懂,这也是这个规则引擎的亮点所在。
    • 脚本式规则集:听名字就知道了,这玩意要写脚本的。拉高配置门槛,需要懂点编码的人来编写。

    3.3.1向导式规则集

    还是一样,首先新建。这次是在“决策集”菜单上右键,点击“添加向导式决策集”,这样就创建好一个规则集了。

    图片

    在配置规则前,可以先导入前面定义好的库文件。我这里导入变量库文件,页面上点击“变量库”,然后选择指定的变量库文件即可。如图所示;

    图片

    最后,可以愉快的配置规则了,向导式没什么好讲的,都是可视化界面,点点点即可。下面是我配置的一个简单的规则集;

    图片

    可以看到由三部分组成:如果、那么、否则;

    1. 如果:配置规则的条件;
    2. 那么:配置满足条件后执行的动作,一般配置变量赋值比较多
    3. 否则:配置不满足条件执行的动作

    最后,附上添加完规则后,通过代码去执行规则;

    package com.cicada;

    import cn.hutool.core.bean.BeanUtil;
    import com.Result;
    import com.bstek.urule.Utils;
    import com.bstek.urule.runtime.KnowledgePackage;
    import com.bstek.urule.runtime.KnowledgeSession;
    import com.bstek.urule.runtime.KnowledgeSessionFactory;
    import com.bstek.urule.runtime.service.KnowledgeService;
    import com.cicada.req.StuReq;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;

    import java.io.IOException;

    /**
     * @author 往事如风
     * @version 1.0
     * @date 2023/3/10 16:47
     * @description
     */
    @RestController
    @RequestMapping("/rule")
    public class RuleDataController {

        @PostMapping("/stu")
        public Result rule(@RequestBody StuReq stuReq) throws IOException {
            KnowledgeService knowledgeService = (KnowledgeService) Utils.getApplicationContext().getBean(KnowledgeService.BEAN_ID);
            KnowledgePackage knowledgePackage = knowledgeService.getKnowledge("xxx/xxx");
            KnowledgeSession knowledgeSession = KnowledgeSessionFactory.newKnowledgeSession(knowledgePackage);
            Stu stu = BeanUtil.copyProperties(stuReq, Stu.class);
            knowledgeSession.insert(stu);
            knowledgeSession.fireRules();
            return Result.success(stu.getTeacher());
        }
    }

    请求接口,最终参数符合配置的条件,返回“那么”中配置的输出结果。

    3.3.2脚本式规则集

    脚本式的规则集,各种原理都是和向导式一模一样,无非就是拉高门槛,用写脚本的方式去实现配置的规则。这里不做过多的介绍了。

    3.4决策表

    再聊下决策表,其实它就是规则集的另一种展示形式,比较相对规则集,我更喜欢用决策表去配置规则,应为它呈现的更加直观,更便于理解。但是本质和规则集没啥区别。

    也不展开过多的赘述,这里我就放一张配置过的决策表;

    图片

    3.5其他

    当然,还有其他的概念和功能,这里也不一一介绍了,因为上面说的已经是最常用的了,想了解的可以自行去了解。其他功能包括:交叉决策表、评分卡、复杂评分卡、决策树、规则流;当然,其中有些是Pro版的功能。

    我们创建了一个高质量的技术交流群,与优秀的人在一起,自己也会优秀起来,赶紧点击加群,享受一起成长的快乐。另外,如果你最近想跳槽的话,年前我花了2周时间收集了一波大厂面经,节后准备跳槽的可以点击这里领取!

    四、运用场景

    最近在开发一期大版本的需求,其中就有个场景,具体如下;参与购买订单的用户都会有自己的一个职级,也可以说是角色。每个用户都会有三个职位:普通用户、会员、精英会员。

    然后,每个月初都会对用户进行一次晋升处理,普通用户达到要求,就会晋升为会员,会员达到要求就会晋升为精英会员。

    当然,普通用户晋升会员,会员晋升精英会员,都会有不同的规则;

    1. 普通用户->会员:3个月内帮注册人数达到3人;3个月内自己和底下团队的人,下单金额超过1万;个人的订单继续率超过80%。
    2. 会员->精英会员:3个月内帮注册人数达到6人;3个月内自己和底下团队的人,下单金额超过5万;个人的订单继续率超过90%。
    3. 不能跨级晋升,普通用户最多只能到会员,达到会员了才能晋升到精英会员。

    当然,这只是做过简化的一部分需求,我做过稍许的改动,真实的需求场景并没有这么简单。

    下面,我对这个需求做一个规则的配置,这里用一个决策表进行配置;在配置规则前,我添加一个变量库文件和常量库;

    图片

    图片

    最后,添加一个决策表,并进行规则配置;

    图片

    可以看到,表格一共五列,其中前四列是规则,最后一列是满足规则后输出的信息。这样看着就很清晰,即使并不是技术人员,也可以轻松看懂其中的规则。

    五、总结

    规则引擎对于我们的系统而言可用可不用,它可以锦上添花,帮助我们剥离出业务中需要进行大量判断的场景。但是,这种规则的剥离,需要我们开发人员对需求进行理解,在理解的基础上进行抽象概念的具化。这,也是整个编程的必经之路

    六、参考源码

    • https://gitee.com/cicadasmile/butte-java-note
    • https://gitee.com/cicadasmile/butte-flyer-parent

    来源:juejin.cn/post/7210194936276680759

    
    

  • 阿里一面:MySQL 单表数据最大不要超过多少行?为什么?

    点击关注公众号,Java干货及时送达👇

    来源:https://my.oschina.net/u/4090830/blog/5559454

    1 背景

    作为在后端圈开车的多年老司机,是不是经常听到过,“mysql 单表最好不要超过 2000w”,“单表超过 2000w 就要考虑数据迁移了”,“你这个表数据都马上要到 2000w 了,难怪查询速度慢

    这些名言民语就和 “群里只讨论技术,不开车,开车速度不要超过 120 码,否则自动踢群”,只听过,没试过,哈哈。

    下面我们就把车速踩到底,干到 180 码试试…….

    2 实验

    某一个值去控制增加的数据量即可。insert into person(id, person_id, person_name, gmt_create, gmt_modified)select @i:=@i+1,  left(rand()*10,10) as person_id,  concat(‘user_’,@i%2048),  date_add(gmt_create,interval + @i*cast(rand()*100 as signed) SECOND), date_add(date_add(gmt_modified,interval +@i*cast(rand()*100 as signed) SECOND), interval + cast(rand()*1000000 as signed) SECOND)from person;此处需要注意的是,也许你在执行到近 800w 或者 1000w 数据的时候,会报错:The total number of locks exceeds the lock table size,这是由于你的临时表内存设置的不够大,只需要扩大一下设置参数即可。SET GLOBAL tmp_table_size =512*1024*1024; (512M)SET global innodb_buffer_pool_size= 1*1024*1024*1024 (1G);先来看一组测试数据,这组数据是在 mysql8.0 的版本,并且是在我本机上,由于本机还跑着 idea , 浏览器等各种工具,所以并不是机器配置就是用于数据库配置,所以测试数据只限于参考。” linktype=”text” imgurl=”” imgdata=”null” data-itemshowtype=”0″ tab=”innerlink” style=”color: rgb(58, 58, 58);” data-linktype=”2″>实验一把看看…

    某一个值去控制增加的数据量即可。insert into person(id, person_id, person_name, gmt_create, gmt_modified)select @i:=@i+1,  left(rand()*10,10) as person_id,  concat(‘user_’,@i%2048),  date_add(gmt_create,interval + @i*cast(rand()*100 as signed) SECOND), date_add(date_add(gmt_modified,interval +@i*cast(rand()*100 as signed) SECOND), interval + cast(rand()*1000000 as signed) SECOND)from person;此处需要注意的是,也许你在执行到近 800w 或者 1000w 数据的时候,会报错:The total number of locks exceeds the lock table size,这是由于你的临时表内存设置的不够大,只需要扩大一下设置参数即可。SET GLOBAL tmp_table_size =512*1024*1024; (512M)SET global innodb_buffer_pool_size= 1*1024*1024*1024 (1G);先来看一组测试数据,这组数据是在 mysql8.0 的版本,并且是在我本机上,由于本机还跑着 idea , 浏览器等各种工具,所以并不是机器配置就是用于数据库配置,所以测试数据只限于参考。” linktype=”text” imgurl=”” imgdata=”null” data-itemshowtype=”0″ tab=”innerlink” data-linktype=”2″>建一张表:

     某一个值去控制增加的数据量即可。insert into person(id, person_id, person_name, gmt_create, gmt_modified)select @i:=@i+1,  left(rand()*10,10) as person_id,  concat('user_',@i%2048),  date_add(gmt_create,interval + @i*cast(rand()*100 as signed) SECOND), date_add(date_add(gmt_modified,interval +@i*cast(rand()*100 as signed) SECOND), interval + cast(rand()*1000000 as signed) SECOND)from person;此处需要注意的是,也许你在执行到近 800w 或者 1000w 数据的时候,会报错:The total number of locks exceeds the lock table size,这是由于你的临时表内存设置的不够大,只需要扩大一下设置参数即可。SET GLOBAL tmp_table_size =512*1024*1024; (512M)SET global innodb_buffer_pool_size= 1*1024*1024*1024 (1G);先来看一组测试数据,这组数据是在 mysql8.0 的版本,并且是在我本机上,由于本机还跑着 idea , 浏览器等各种工具,所以并不是机器配置就是用于数据库配置,所以测试数据只限于参考。" linktype="text" imgurl="" imgdata="null" data-itemshowtype="0" tab="innerlink" data-linktype="2">CREATE TABLE person(
      id int NOT NULL AUTO_INCREMENT PRIMARY KEY comment '主键',
      person_id tinyint not null comment '用户id',
      person_name VARCHAR(200) comment '用户名称',
      gmt_create datetime comment '创建时间',
      gmt_modified datetime comment '修改时间'
    ) comment '人员信息表';

    插入一条数据:

     某一个值去控制增加的数据量即可。insert into person(id, person_id, person_name, gmt_create, gmt_modified)select @i:=@i+1,  left(rand()*10,10) as person_id,  concat('user_',@i%2048),  date_add(gmt_create,interval + @i*cast(rand()*100 as signed) SECOND), date_add(date_add(gmt_modified,interval +@i*cast(rand()*100 as signed) SECOND), interval + cast(rand()*1000000 as signed) SECOND)from person;此处需要注意的是,也许你在执行到近 800w 或者 1000w 数据的时候,会报错:The total number of locks exceeds the lock table size,这是由于你的临时表内存设置的不够大,只需要扩大一下设置参数即可。SET GLOBAL tmp_table_size =512*1024*1024; (512M)SET global innodb_buffer_pool_size= 1*1024*1024*1024 (1G);先来看一组测试数据,这组数据是在 mysql8.0 的版本,并且是在我本机上,由于本机还跑着 idea , 浏览器等各种工具,所以并不是机器配置就是用于数据库配置,所以测试数据只限于参考。" linktype="text" imgurl="" imgdata="null" data-itemshowtype="0" tab="innerlink" data-linktype="2">insert into person values(1,1,'user_1', NOW(), now());

    利用 mysql 伪列 rownum 设置伪列起始点为 1

     某一个值去控制增加的数据量即可。insert into person(id, person_id, person_name, gmt_create, gmt_modified)select @i:=@i+1,  left(rand()*10,10) as person_id,  concat('user_',@i%2048),  date_add(gmt_create,interval + @i*cast(rand()*100 as signed) SECOND), date_add(date_add(gmt_modified,interval +@i*cast(rand()*100 as signed) SECOND), interval + cast(rand()*1000000 as signed) SECOND)from person;此处需要注意的是,也许你在执行到近 800w 或者 1000w 数据的时候,会报错:The total number of locks exceeds the lock table size,这是由于你的临时表内存设置的不够大,只需要扩大一下设置参数即可。SET GLOBAL tmp_table_size =512*1024*1024; (512M)SET global innodb_buffer_pool_size= 1*1024*1024*1024 (1G);先来看一组测试数据,这组数据是在 mysql8.0 的版本,并且是在我本机上,由于本机还跑着 idea , 浏览器等各种工具,所以并不是机器配置就是用于数据库配置,所以测试数据只限于参考。" linktype="text" imgurl="" imgdata="null" data-itemshowtype="0" tab="innerlink" data-linktype="2">select (@i:=@i+1) as rownum, person_name 
    from person, (select @i:=100) as init;

    set @i=1;

    某一个值去控制增加的数据量即可。insert into person(id, person_id, person_name, gmt_create, gmt_modified)select @i:=@i+1,  left(rand()*10,10) as person_id,  concat(‘user_’,@i%2048),  date_add(gmt_create,interval + @i*cast(rand()*100 as signed) SECOND), date_add(date_add(gmt_modified,interval +@i*cast(rand()*100 as signed) SECOND), interval + cast(rand()*1000000 as signed) SECOND)from person;此处需要注意的是,也许你在执行到近 800w 或者 1000w 数据的时候,会报错:The total number of locks exceeds the lock table size,这是由于你的临时表内存设置的不够大,只需要扩大一下设置参数即可。SET GLOBAL tmp_table_size =512*1024*1024; (512M)SET global innodb_buffer_pool_size= 1*1024*1024*1024 (1G);先来看一组测试数据,这组数据是在 mysql8.0 的版本,并且是在我本机上,由于本机还跑着 idea , 浏览器等各种工具,所以并不是机器配置就是用于数据库配置,所以测试数据只限于参考。” linktype=”text” imgurl=”” imgdata=”null” data-itemshowtype=”0″ tab=”innerlink” style=”color: rgb(58, 58, 58);” data-linktype=”2″>运行下面的 sql,连续执行 20 次,就是 2 的 20 次方约等于 100w 的数据;执行 23 次就是 2 的 23 次方约等于 800w , 如此下去即可实现千万测试数据的插入,如果不想翻倍翻倍的增加数据,而是想少量,少量的增加,有个技巧,就是在 SQL 的后面增加 where 条件,如 id > 某一个值去控制增加的数据量即可。

     某一个值去控制增加的数据量即可。insert into person(id, person_id, person_name, gmt_create, gmt_modified)select @i:=@i+1,  left(rand()*10,10) as person_id,  concat('user_',@i%2048),  date_add(gmt_create,interval + @i*cast(rand()*100 as signed) SECOND), date_add(date_add(gmt_modified,interval +@i*cast(rand()*100 as signed) SECOND), interval + cast(rand()*1000000 as signed) SECOND)from person;此处需要注意的是,也许你在执行到近 800w 或者 1000w 数据的时候,会报错:The total number of locks exceeds the lock table size,这是由于你的临时表内存设置的不够大,只需要扩大一下设置参数即可。SET GLOBAL tmp_table_size =512*1024*1024; (512M)SET global innodb_buffer_pool_size= 1*1024*1024*1024 (1G);先来看一组测试数据,这组数据是在 mysql8.0 的版本,并且是在我本机上,由于本机还跑着 idea , 浏览器等各种工具,所以并不是机器配置就是用于数据库配置,所以测试数据只限于参考。" linktype="text" imgurl="" imgdata="null" data-itemshowtype="0" tab="innerlink" data-linktype="2">insert into person(id, person_id, person_name, gmt_create, gmt_modified)
    select @i:=@i+1,
      left(rand()*10,10) as person_id,
      concat('user_',@i%2048),
      date_add(gmt_create,interval + @i*cast(rand()*100 as signed) SECOND),
     date_add(date_add(gmt_modified,interval +@i*cast(rand()*100 as signed) SECOND), interval + cast(rand()*1000000 as signed) SECOND)
    from person;

    某一个值去控制增加的数据量即可。insert into person(id, person_id, person_name, gmt_create, gmt_modified)select @i:=@i+1,  left(rand()*10,10) as person_id,  concat(‘user_’,@i%2048),  date_add(gmt_create,interval + @i*cast(rand()*100 as signed) SECOND), date_add(date_add(gmt_modified,interval +@i*cast(rand()*100 as signed) SECOND), interval + cast(rand()*1000000 as signed) SECOND)from person;此处需要注意的是,也许你在执行到近 800w 或者 1000w 数据的时候,会报错:The total number of locks exceeds the lock table size,这是由于你的临时表内存设置的不够大,只需要扩大一下设置参数即可。SET GLOBAL tmp_table_size =512*1024*1024; (512M)SET global innodb_buffer_pool_size= 1*1024*1024*1024 (1G);先来看一组测试数据,这组数据是在 mysql8.0 的版本,并且是在我本机上,由于本机还跑着 idea , 浏览器等各种工具,所以并不是机器配置就是用于数据库配置,所以测试数据只限于参考。” linktype=”text” imgurl=”” imgdata=”null” data-itemshowtype=”0″ tab=”innerlink” style=”color: rgb(58, 58, 58);” data-linktype=”2″>此处需要注意的是,也许你在执行到近 800w 或者 1000w 数据的时候,会报错:The total number of locks exceeds the lock table size,这是由于你的临时表内存设置的不够大,只需要扩大一下设置参数即可。

     某一个值去控制增加的数据量即可。insert into person(id, person_id, person_name, gmt_create, gmt_modified)select @i:=@i+1,  left(rand()*10,10) as person_id,  concat('user_',@i%2048),  date_add(gmt_create,interval + @i*cast(rand()*100 as signed) SECOND), date_add(date_add(gmt_modified,interval +@i*cast(rand()*100 as signed) SECOND), interval + cast(rand()*1000000 as signed) SECOND)from person;此处需要注意的是,也许你在执行到近 800w 或者 1000w 数据的时候,会报错:The total number of locks exceeds the lock table size,这是由于你的临时表内存设置的不够大,只需要扩大一下设置参数即可。SET GLOBAL tmp_table_size =512*1024*1024; (512M)SET global innodb_buffer_pool_size= 1*1024*1024*1024 (1G);先来看一组测试数据,这组数据是在 mysql8.0 的版本,并且是在我本机上,由于本机还跑着 idea , 浏览器等各种工具,所以并不是机器配置就是用于数据库配置,所以测试数据只限于参考。" linktype="text" imgurl="" imgdata="null" data-itemshowtype="0" tab="innerlink" data-linktype="2">SET GLOBAL tmp_table_size =512*1024*1024; (512M)
    SET global innodb_buffer_pool_size= 1*1024*1024*1024 (1G);

    某一个值去控制增加的数据量即可。insert into person(id, person_id, person_name, gmt_create, gmt_modified)select @i:=@i+1,  left(rand()*10,10) as person_id,  concat(‘user_’,@i%2048),  date_add(gmt_create,interval + @i*cast(rand()*100 as signed) SECOND), date_add(date_add(gmt_modified,interval +@i*cast(rand()*100 as signed) SECOND), interval + cast(rand()*1000000 as signed) SECOND)from person;此处需要注意的是,也许你在执行到近 800w 或者 1000w 数据的时候,会报错:The total number of locks exceeds the lock table size,这是由于你的临时表内存设置的不够大,只需要扩大一下设置参数即可。SET GLOBAL tmp_table_size =512*1024*1024; (512M)SET global innodb_buffer_pool_size= 1*1024*1024*1024 (1G);先来看一组测试数据,这组数据是在 mysql8.0 的版本,并且是在我本机上,由于本机还跑着 idea , 浏览器等各种工具,所以并不是机器配置就是用于数据库配置,所以测试数据只限于参考。” linktype=”text” imgurl=”” imgdata=”null” data-itemshowtype=”0″ tab=”innerlink” style=”color: rgb(58, 58, 58);” data-linktype=”2″>先来看一组测试数据,这组数据是在 mysql8.0 的版本,并且是在我本机上,由于本机还跑着 idea , 浏览器等各种工具,所以并不是机器配置就是用于数据库配置,所以测试数据只限于参考。

    看到这组数据似乎好像真的和标题对应,当数据达到 2000w 以后,查询时长急剧上升;难道这就是铁律吗?那下面我们就来看看这个建议值 2kw 是怎么来的?

    3 单表数量限制

    首先我们先想想数据库单表行数最大多大?

    CREATE TABLE person(
      id int(10) NOT NULL AUTO_INCREMENT PRIMARY KEY comment '主键',
      person_id tinyint not null comment '用户id',
      person_name VARCHAR(200) comment '用户名称',
      gmt_create datetime comment '创建时间',
      gmt_modified datetime comment '修改时间'
    ) comment '人员信息表';

    看看上面的建表 sql,id 是主键,本身就是唯一的,也就是说主键的大小可以限制表的上限,如果主键声明 int 大小,也就是 32 位,那么支持 2^32-1 ~~21 亿;如果是 bigint,那就是 2^62-1 ?(36893488147419103232),难以想象这个的多大了,一般还没有到这个限制之前,可能数据库已经爆满了!!

    有人统计过,如果建表的时候,自增字段选择无符号的 bigint , 那么自增长最大值是 18446744073709551615,按照一秒新增一条记录的速度,大约什么时候能用完?

    4 表空间

    下面我们再来看看索引的结构,对了,我们下面讲内容都是基于 Innodb 引擎的,大家都知道 Innodb 的索引内部用的是 B+ 树

    这张表数据,在硬盘上存储也是类似如此的,它实际是放在一个叫 person.ibd (innodb data)的文件中,也叫做表空间;虽然数据表中,他们看起来是一条连着一条,但是实际上在文件中它被分成很多小份的数据页,而且每一份都是 16K。大概就像下面这样,当然这只是我们抽象出来的,在表空间中还有段、区、组等很多概念,但是我们需要跳出来看。

    5 页的数据结构

    因为每个页只有 16K 的大小,但是如果数据很多,那一页肯定就放不下这些数据,那数据肯定就会被分到其他的页中,所以为了把这些页关联起来,肯定就会有记录前后页地址,方便找到对应页;同时每页都是唯一的,那就会需要有一个唯一标志来标记页,就是页号;页中会记录数据所以会存在读写操作,读写操作会存在中断或者其他异常导致数据不全等,那就会需要有校验机制,所以里面还有会校验码,而读操作最重要的就是效率问题,如果按照记录一个个进行遍历,那肯定是很费劲的,所以这里面还会为数据生成对应的页目录(Page Directory); 所以实际页的内部结构像是下面这样的。

    从图中可以看出,一个 InnoDB 数据页的存储空间大致被划分成了 7 个部分,有的部分占用的字节数是确定的,有的部分占用的字节数是不确定的。

    在页的 7 个组成部分中,我们自己存储的记录会按照我们指定的行格式存储到 User Records 部分。

    但是在一开始生成页的时候,其实并没有 User Records 这个部分,每当我们插入一条记录,都会从 Free Space 部分,也就是尚未使用的存储空间中申请一个记录大小的空间划分到 User Records 部分,当 Free Space 部分的空间全部被 User Records 部分替代掉之后,也就意味着这个页使用完了,如果还有新的记录插入的话,就需要去申请新的页了。这个过程的图示如下。

    那下面就来说说,数据的查找过程,假如我们需要查找一条记录,我们可以把表空间中的每一页都加载到内存中,然后对记录挨个判断是不是我们想要的,在数据量小的时候,没啥问题,内存也可以撑;但是现实就是这么残酷,不会给你这个局面;为了解决这问题,mysql 中就有了索引的概念;大家都知道索引能够加快数据的查询,那到底是怎么个回事呢?下面我就来看看。

    6 索引的数据结构

    在 mysql 中索引的数据结构和刚刚描述的页几乎是一模一样的,而且大小也是 16K, 但是在索引页中记录的是页 (数据页,索引页) 的最小主键 id 和页号,以及在索引页中增加了层级的信息,从 0 开始往上算,所以页与页之间就有了上下层级的概念。

    看到这个图之后,是不是有点似曾相似的感觉,是不是像一棵二叉树啊,对,没错!它就是一棵树,只不过我们在这里只是简单画了三个节点,2 层结构的而已,如果数据多了,可能就会扩展到 3 层的树,这个就是我们常说的 B+ 树,最下面那一层的 page level =0, 也就是叶子节点,其余都是非叶子节点。

    看上图中,我们是单拿一个节点来看,首先它是一个非叶子节点(索引页),在它的内容区中有 id 和 页号地址两部分,这个 id 是对应页中记录的最小记录 id 值,页号地址是指向对应页的指针;而数据页与此几乎大同小异,区别在于数据页记录的是真实的行数据而不是页地址,而且 id 的也是顺序的。

    7 单表建议值

    下面我们就以 3 层,2 分叉(实际中是 M 分叉)的图例来说明一下查找一个行数据的过程。

    比如说我们需要查找一个 id=6 的行数据,因为在非叶子节点中存放的是页号和该页最小的 id,所以我们从顶层开始对比,首先看页号 10 中的目录,有 [id=1, 页号 = 20],[id=5, 页号 = 30], 说明左侧节点最小 id 为 1,右侧节点最小 id 是 5;6>5, 那按照二分法查找的规则,肯定就往右侧节点继续查找,找到页号 30 的节点后,发现这个节点还有子节点(非叶子节点),那就继续比对,同理,6>5&&6

    从上述的过程中发现,我们为了查找 id=6 的数据,总共查询了三个页,如果三个页都在磁盘中(未提前加载至内存),那么最多需要经历三次的磁盘 IO。需要注意的是,图中的页号只是个示例,实际情况下并不是连续的,在磁盘中存储也不一定是顺序的。

    至此,我们大概已经了解了表的数据是怎么个结构了,也大概知道查询数据是个怎么的过程了,这样我们也就能大概估算这样的结构能存放多少数据了。

    从上面的图解我们知道 B+ 数的叶子节点才是存在数据的,而非叶子节点是用来存放索引数据的。

    所以,同样一个 16K 的页,非叶子节点里的每条数据都指向新的页,而新的页有两种可能

    • 如果是叶子节点,那么里面就是一行行的数据
    • 如果是非叶子节点的话,那么就会继续指向新的页

    假设

    • 非叶子节点内指向其他页的数量为 x
    • 叶子节点内能容纳的数据行数为 y
    • B+ 数的层数为 z

    如下图中所示
    Total =x^(z-1) *y 也就是说总数会等于 x 的 z-1 次方 与 Y 的乘积。

    X =?

    在文章的开头已经介绍了页的结构,索引也也不例外,都会有 File Header (38 byte)、Page Header (56 Byte)、Infimum + Supermum(26 byte)、File Trailer(8byte), 再加上页目录,大概 1k 左右,我们就当做它就是 1K, 那整个页的大小是 16K, 剩下 15k 用于存数据,在索引页中主要记录的是主键与页号,主键我们假设是 Bigint (8 byte), 而页号也是固定的(4Byte), 那么索引页中的一条数据也就是 12byte; 所以 x=15*1024/12≈1280 行。

    Y=?

    叶子节点和非叶子节点的结构是一样的,同理,能放数据的空间也是 15k;但是叶子节点中存放的是真正的行数据,这个影响的因素就会多很多,比如,字段的类型,字段的数量;每行数据占用空间越大,页中所放的行数量就会越少;这边我们暂时按一条行数据 1k 来算,那一页就能存下 15 条,Y≈15。

    算到这边了,是不是心里已经有谱了啊
    根据上述的公式,Total =x^(z-1) y,已知 x=1280,y=15
    假设 B+ 树是两层,那就是 Z =2, Total = (1280 ^1 )
    15 = 19200
    假设 B+ 树是三层,那就是 Z =3, Total = (1280 ^2) *15 = 24576000 (约 2.45kw)

    哎呀,妈呀!这不是正好就是文章开头说的最大行数建议值 2000w 嘛!对的,一般 B+ 数的层级最多也就是 3 层,你试想一下,如果是 4 层,除了查询的时候磁盘 IO 次数会增加,而且这个 Total 值会是多少,大概应该是 3 百多亿吧,也不太合理,所以,3 层应该是比较合理的一个值。

    到这里难道就完了?


    我们刚刚在说 Y 的值时候假设的是 1K ,那比如我实际当行的数据占用空间不是 1K , 而是 5K, 那么单个数据页最多只能放下 3 条数据
    同样,还是按照 Z=3 的值来计算,那 Total = (1280 ^2) *3 = 4915200 (近 500w)

    所以,在保持相同的层级(相似查询性能)的情况下,在行数据大小不同的情况下,其实这个最大建议值也是不同的,而且影响查询性能的还有很多其他因素,比如,数据库版本,服务器配置,sql 的编写等等,MySQL 为了提高性能,会将表的索引装载到内存中。在 InnoDB buffer size 足够的情况下,其能完成全加载进内存,查询不会有问题。但是,当单表数据库到达某个量级的上限时,导致内存无法存储其索引,使得之后的 SQL 查询会产生磁盘 IO,从而导致性能下降,所以增加硬件配置(比如把内存当磁盘使),可能会带来立竿见影的性能提升哈。

    8 总结

    • Mysql 的表数据是以页的形式存放的,页在磁盘中不一定是连续的。
    • 页的空间是 16K, 并不是所有的空间都是用来存放数据的,会有一些固定的信息,如,页头,页尾,页码,校验码等等。
    • 在 B+ 树中,叶子节点和非叶子节点的数据结构是一样的,区别在于,叶子节点存放的是实际的行数据,而非叶子节点存放的是主键和页号。
    • 索引结构不会影响单表最大行数,2kw 也只是推荐值,超过了这个值可能会导致 B + 树层级更高,影响查询性能。

    参考资料:

    • https://www.jianshu.com/p/cf5d381ef637
    • https://www.modb.pro/db/139052
    • 《MYSQL 内核:INNODB 存储引擎 卷 1》

    
    

    文章来源于互联网:阿里一面:MySQL 单表数据最大不要超过多少行?为什么?

  • 被问懵了:MySQL 自增主键一定是连续的吗?

    测试环境:

    MySQL版本:8.0

    数据库表:T (主键id,唯一索引c,普通字段d)

    如果你的业务设计依赖于自增主键的连续性,这个设计假设自增主键是连续的。但实际上,这样的假设是错的,因为自增主键不能保证连续递增。

    一、自增值的属性特征:

    1. 自增主键值是存储在哪的?

    MySQL5.7版本

    在 MySQL 5.7 及之前的版本,自增值保存在内存里,并没有持久化。每次重启后,第一次打开表的时候,都会去找自增值的最大值 max(id),然后将 max(id)+1 作为这个表当前的自增值。

    MySQL8.0之后版本

    在 MySQL 8.0 版本,将自增值的变更记录在了 redo log 中,重启的时候依靠 redo log 恢复重启之前的值。

    可以通过看表详情查看当前自增值,以及查看表参数详情AUTO_INCREMENT值(AUTO_INCREMENT就是当前数据表的自增值)

    2. 自增主键值的修改机制?

    在表t中,我定义了主键id为自增值,在插入一行数据的时候,自增值的行为如下:

    1. 如果插入数据时 id 字段指定为 0、null 或未指定值,那么就把这个表当前的 AUTO_INCREMENT 值填到自增字段;
    2. 如果插入数据时 id 字段指定了具体的值,就直接使用语句里指定的值。

    根据要插入的值和当前自增值的大小关系,自增值的变更结果也会有所不同。假设,某次要插入的值是 X,当前的自增值是 Y。

    1. 如果 X
    2. 如果 X≥Y,就需要把当前自增值修改为新的自增值。

    二、新增语句自增主键是如何变化的:

    我们执行以下SQL语句,来观察自增主键是如何进行变化的

    insert into t values(null11); 
    

    流程图如下所示

    流程步骤:

    • AUTO_INCREMENT=1 (表示下一次插入数据时,如果需要自动生成自增值,会生成 id=1。)
    • insert into t values(null, 1, 1) (执行器调用 InnoDB 引擎接口写入一行,传入的这一行的值是 (0,1,1))
    • get AUTO_INCREMENT=1 (InnoDB 发现用户没有指定自增 id 的值,获取表 t 当前的自增值 1 )
    • AUTO_INCREMENT=2 insert into t values(1, 1, 1) (将传入的行的值改成 (1,1,1),并把自增值改为2)
    • insert (1,1,1) 执行插入操作,至此流程结束

    大家可以发现,在这个流程当中是先进行自增值的+1,在进行新增语句的执行的。大家可以发现这个操作并没有进行原子操作,如果SQL语句执行失败,那么自增是不是就不会连续了呢?

    三、自增主键值不连续情况:(唯一主键冲突)

    当我执行以下SQL语句时

    insert into t values(null11); 
    

    第一次我们可以进行新增成功,根据自增值的修改机制。如果插入数据时 id 字段指定为 0、null 或未指定值,那么就把这个表当前的 AUTO_INCREMENT 值填到自增字段;

    当我们第二次在执行以下SQL语句时,就会出现错误。因为我们表中c字段是唯一索引,会出现Duplicate key error错误导致新增失败。

    例如:

    • AUTO_INCREMENT=2 (表示下一次插入数据时,如果需要自动生成自增值,会生成 id=2。)
    • insert into t values(null, 1, 1) (执行器调用 InnoDB 引擎接口写入一行,传入的这一行的值是 (0,1,1))
    • get AUTO_INCREMENT=2 (InnoDB 发现用户没有指定自增 id 的值,获取表 t 当前的自增值 2 )
    • AUTO_INCREMENT=3 insert into t values(2, 1, 1) (将传入的行的值改成 (2,1,1),并把自增值改为3)
    • insert (2,1,1)  执行插入操作,由于已经存在 c=1 的记录,所以报 Duplicate key error,语句返回。

    可以看到,这个表的自增值改成 3,是在真正执行插入数据的操作之前。这个语句真正执行的时候,因为碰到唯一键 c 冲突,所以 id=2 这一行并没有插入成功,但也没有将自增值再改回去。所以,在这之后,再插入新的数据行时,拿到的自增 id 就是 3。也就是说,出现了自增主键不连续的情况。

    四、自增主键值不连续情况:(事务回滚)

    其实事务回滚原理也和上面一样,都是因为异常导致新增失败,但是自增值没有进行回退。

    五、自增主键值不连续情况:(批量插入)

    批量插入数据的语句,MySQL 有一个批量申请自增 id 的策略:

    1. 语句执行过程中,第一次申请自增 id,会分配 1 个;
    2. 1 个用完以后,这个语句第二次申请自增 id,会分配 2 个;
    3. 2 个用完以后,还是这个语句, 第三次申请自增 id,会分配 4 个;
    4. 依此类推,同一个语句去申请自增 id,每次申请到的自增 id 个数都是上一次的两倍。

    执行以下SQL语句(在表t中先新增了4条数据,在创建表tt把表t数据进行批量新增)

    insert into t values(null1,1);
    insert into t values(null2,2);
    insert into t values(null3,3);
    insert into t values(null4,4);
    create table tt like t;
    insert into tt(c,d) select c,d from t;
    
    insert into tt values(null5,5);
    

    第一次申请到了 id=1,第二次被分配了 id=2 和 id=3, 第三次被分配到 id=4 到 id=7。当我们再执行 insert into t2 values(null, 5,5),实际上插入的数据就是(8,5,5),出现了自增主键不连续的情况。

    六、自增主键值的优化

    1.什么是自增锁

    自增锁是一种比拟非凡的表级锁。并且在事务向蕴含了 AUTO_INCREMENT 列的表中新增数据时就会去持有自增锁,假如事务 A 正在做这个操作,如果另一个事务 B 尝试执行 INSERT语句,事务 B 会被阻塞住,直到事务 A 开释自增锁。

    2.自增锁有哪些优化

    在 MySQL 5.0 版本的时候,自增锁的范围是语句级别。也就是说,如果一个语句申请了一个表自增锁,这个锁会等语句执行结束以后才释放。显然,这样设计会影响并发度。在MySQL 5.1.22 版本引入了一个新策略,新增参数 innodb_autoinc_lock_mode,默认值是 1。

    传统模式(Traditional)

    这个参数的值被设置为 0 时,表示采用之前 MySQL 5.0 版本的策略,即语句执行结束后才释放锁;

    传统模式他可以保证数据一致性,但是如果有多个事务并发的执行 INSERT 操作,AUTO-INC的存在会使得 MySQL 的性能略有降落,因为同时只能执行一条 INSERT 语句。

    间断模式(Consecutive)

    这个参数的值被设置为 1 时:普通 insert 语句,自增锁在申请之后就马上释放;类似 insert … select 这样的批量插入数据的语句,自增锁还是要等语句结束后才被释放;

    间断模式他可以保证数据一致性,但是如果有多个事务并发的执行 INSERT 批量操作时,就会进行锁等待状态。如果我们业务插入数据量很大时,这个时候MySQL的性能就会大大下降。

    穿插模式(Interleaved)

    这个参数的值被设置为 2 时,所有的申请自增主键的动作都是申请后就释放锁。

    穿插模式他没有进行任何的上锁设置。在一定情况下是保证了MySQL的性能,但是他无法保证数据的一致性。如果我们在穿插模式下进行主从复制时,如果你的binlog格式不是row格式,主从复制就会出现不一致。

    七、MySQL8.0做了哪些优化

    在MySQL8.0之后版本,已经默认设置为 innodb_autoinc_lock_mode=2binlog_format=row.。这样更有利与我们在 insert … select 这种批量插入数据的场景时,既能提升并发性,又不会出现数据一致性问题。

    作者:又欠

    来源:blog.csdn.net/qq_48157004/article/

    details/128356734

  • MySQL 批量操作,一次插入多少行数据效率最高?

    MySQL 批量操作,一次插入多少行数据效率最高?来源:blog.csdn.net/LJFPHP/article/details/99708888

    • 一、前言
    • 二、批量插入前准备
      • 1、插入到数据表的字段
      • 2、计算一行字段占用的空间
      • 3、在数据里做插入操作的时候,整体时间的分配
    • 三、批量插入数据测试
      • 1、SQL语句的大小限制
      • 2、查看服务器上的参数:
      • 3、计算一次能插入的最大行记录
      • 4、测试插入数据比对
      • 5、如果插入的值就是sql语句限制的最大值,那么性能真的好吗?
    • 四、其他影响插入性能的因素
      • 1、首先是插入的时候,要注意缓冲区的大小使用情况
      • 2、插入缓存
      • 3、使用事务提升效率
      • 4、通过配置提升读写性能
      • 5、索引影响插入性能
    • 五、总结

    一、前言

    我们在操作大型数据表或者日志文件的时候经常会需要写入数据到数据库,那么最合适的方案就是数据库的批量插入。只是我们在执行批量操作的时候,一次插入多少数据才合适呢?

    假如需要插入的数据有百万条,那么一次批量插入多少条的时候,效率会高一些呢?这里博主和大家一起探讨下这个问题,应用环境为批量插入数据到临时表。

    二、批量插入前准备

    博主本地原本是循环查出来的数据,然后每1000条插入一次,直至完成插入操作。但是为什么要设置1000条呢,实不相瞒,这是因为项目里的其他批量插入都是一次插1000条。。汗,博主不服,所以想要测试下。

    首先是查看当前数据库的版本,毕竟各个版本之间存在差异,脱离版本讲数据库就是耍流氓(以前没少耍啊):

    mysql> select version();
    +------------+
    | version()  |
    +------------+
    | 5.6.34-log |
    +------------+
    1 row in set (0.00 sec)
    

    1、插入到数据表的字段

    对于手动创建的临时表来说,字段当然是越少越好,而且字段占用的空间要尽量小一些,这样临时表不至于太大,影响表操作的性能。这里需要插入的字段是:

    字段1 int(10)
    字段2 int(10)
    字段3 int(10)
    字段4 varchar(10)
    

    我们一共插入四个字段,分别是3个int类型的,一个varchar类型的,整体来说这些字段都比较小,占用的内存空间会小一些。

    2、计算一行字段占用的空间

    对于innodb引擎来说,int类型可以存储4个字节,里面的Int(M)并不会影响存储字节的大小,这个M只是数据的展示位数,和mysql的ZEROFILL属性有关,即在数字长度不够的数据前面填充0,以达到设定的长度。此处不多说,想要了解的朋友可以百度一下,还是很有意思的。

    varchar(10)代表可以存储10个字符,不管是英文还是中文,最多都是10个,这部分假设存储的是中文,在utf-8mb4下,10个中文占用10*4 = 40个字节那么一行数据最多占用:4+4+4+40 = 52字节

    3、在数据里做插入操作的时候,整体时间的分配

    链接耗时 (30%)
    发送query到服务器 (20%)
    解析query (20%)
    插入操作 (10% * 词条数目)
    插入index (10% * Index的数目)
    关闭链接 (10%)
    

    从这里可以看出来,真正耗时的不是操作,而是链接,解析的过程。单条sql的话,会在链接,解析部分耗费大量的时间,因此速度会很慢,所以我们一般都是采用批量插入的操作,争取在一次链接里面写入尽可能多的数据,以此来提升插入的速度。但是这个尽可能多的数据是多少呢?一次到底插入多少才合适呢?

    三、批量插入数据测试

    开始测试,但是一开始插入多少是合适的呢,是否有上限?查询mysql手册,我们知道sql语句是有大小限制的。

    1、SQL语句的大小限制

    my.ini 里有 max_allowed_packet 这个参数控制通信的 packet 大小。mysql默认的sql语句的最大限制是1M(mysql5.7的客户端默认是16M,服务端默认是4M),可以根据设置查看。官方解释是适当增大 max_allowed_packet 参数可以使client端到server端传递大数据时,系统能够分配更多的扩展内存来处理。

    官方手册:https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html

    2、查看服务器上的参数:

    mysql> show variables like '%max_allowed_packet%';
    +--------------------------+------------+
    | Variable_name            | Value      |
    +--------------------------+------------+
    | max_allowed_packet       | 33554432   |
    | slave_max_allowed_packet | 1073741824 |
    +--------------------------+------------+
    2 rows in set (0.00 sec)
    

    33554432字节 = 32M ,也就是规定大小不能超过32M。

    3、计算一次能插入的最大行记录

    1M计算的话,(1024*1024)/52 ≈ 20165 ,为了防止溢出,最大可一次性插入20000条(根据自己的配置和sql语句大小计算)。那么32M的话就是:20000 *32 = 640000 也就是64W条。

    4、测试插入数据比对

    (1)插入11W条数据,按照每次10,600,1000,20000,80000来测试:
    +---------------+
    | count(c1.uin) |
    +---------------+
    |         110000 |
    +---------------+
    

    有个博客说一次插入10条最快,,我觉得一次插的有点少,咱们试试

    参考:https://www.cnblogs.com/aicro/p/3851434.html

    这个博主测试后,认为一次插10条是性能最快的,他的每条记录是3kb,相当于我的59行数据,取个整数60,那么对于这个博主是插入10条,对我来说插入:600,这几个值都试试。

    耗时:

    11W的数据,每次插入10条。耗时:2.361s
    11W的数据,每次插入600条。耗时:0.523s
    11W的数据,每次插入1000条。耗时:0.429s
    11W的数据,每次插入20000条。耗时:0.426s
    11W的数据,每次插入80000条。耗时:0.352s
    

    从这部分看,随着批量插入的增加,速度略有提升,最起码一次插10条应该不是最佳的。插入数据量多,减少了循环的次数,也就是在数据库链接部分的耗时有所减少,只是这个8W并不是极限数据,具体一次插入多少条,还有待参考。

    (2)加大数据量到24w
    +---------------+
    | count(c1.uin) |
    +---------------+
    |        241397 |
    +---------------+
    

    耗时:

    24W的数据,每次插入10条。耗时:4.445s
    24W的数据,每次插入600条。耗时:1.187s
    24W的数据,每次插入1000条。耗时:1.13s
    24W的数据,每次插入20000条。耗时:0.933s
    24W的数据,每次插入80000条。耗时:0.753s
    

    一次插入24W反而性能最佳,这么代表我们的测试数据量依然不够。

    (3)加大测试量到42W
    +---------------+
    | count(c1.uin) |
    +---------------+
    |        418859 |
    

    耗时:

    42W的数据,每次插入1000条。耗时:2.216s
    42W的数据,每次插入80000条。耗时:1.777s
    42W的数据,每次插入16W条。耗时:1.523s
    42W的数据,每次插入20W条。耗时:1.432s
    42W的数据,每次插入30W条。耗时:1.362s
    42W的数据,每次插入40W条。耗时:1.764s
    

    随着插入量的增加,批量插入条数多了之后,性能是有所提升的。但是在达到30W以上之后,效率反而有所下降。这部分我的理解是mysql是要分配一定的内存给传过来的数据包使用,当批量插入的数据量到达一定程度之后,一次插入操作的开销就很耗费内存了。

    个人感觉,最佳大小是max_allowed_packet的一半,也就是极限能插入64W,选用32W也许性能会更好一些,同时也不会对mysql的其他操作产生太大的影响。

    5、如果插入的值就是sql语句限制的最大值,那么性能真的好吗?

    博主疯狂谷歌百度,都没有找到有人来具体的说一下这个问题,不过在高性能mysql里面发现一句话:

    客户端用一个单独的数据包将查询请求发送给服务器,所以当查询语句很长的时候,需要设置max_allowed_packet参数。但是需要注意的是,如果查询实在是太大,服务端会拒绝接收更多数据并抛出异常。与之相反的是,服务器响应给用户的数据通常会很多,由多个数据包组成。但是当服务器响应客户端请求时,客户端必须完整的接收整个返回结果,而不能简单的只取前面几条结果,然后让服务器停止发送。因而在实际开发中,尽量保持查询简单且只返回必需的数据,减小通信间数据包的大小和数量是一个非常好的习惯,这也是查询中尽量避免使用SELECT *以及加上LIMIT限制的原因之一。

    后面通过各种百度,博主觉得最大只是代表传输数据包的最大长度,但性能是不是最佳就要从各个方面来分析了。比如下面列出的插入缓冲,以及插入索引时对于缓冲区的剩余空间需求,以及事务占有的内存等,都会影响批量插入的性能。

    四、其他影响插入性能的因素

    1、首先是插入的时候,要注意缓冲区的大小使用情况

    在分析源码的过程中,有一句话:如果buffer pool余量不足25%,插入失败,返回DB_LOCK_TABLE_FULL。这个错误并不是直接报错:max_allowed_packet 不够大之类的,这个错误是因为对于innodb引擎来说,一次插入是涉及到事务和锁的,在插入索引的时候,要判断缓冲区的剩余情况,所以插入并不能仅仅只考虑max_allowed_packet的问题,也要考虑到缓冲区的大小。

    参考淘宝的数据库日报:http://mysql.taobao.org/monthly/2017/09/10/

    2、插入缓存

    另外对于innodb引擎来说,因为存在插入缓存(Insert Buffer)这个概念,所以在插入的时候也是要耗费一定的缓冲池内存的。当写密集的情况下,插入缓冲会占用过多的缓冲池内存,默认最大可以占用到1/2的缓冲池内存,当插入缓冲占用太多缓冲池内存的情况下,会影响到其他的操作。

    也就是说,插入缓冲受到缓冲池大小的影响,缓冲池大小为:

    mysql> show variables like 'innodb_buffer_pool_size';
    +-------------------------+-----------+
    | Variable_name           | Value     |
    +-------------------------+-----------+
    | innodb_buffer_pool_size | 134217728 |
    +-------------------------+-----------+
    

    换算后的结果为:128M,也就是说,插入缓存最多可以占用64M的缓冲区大小。这个大小要超过咱们设置的sql语句大小,所以可以忽略不计。

    详细解释:

    我们都知道,在InnoDB引擎上进行插入操作时,一般需要按照主键顺序进行插入,这样才能获得较高的插入性能。当一张表中存在非聚簇的且不唯一的索引时,在插入时,数据页的存放还是按照主键进行顺序存放,但是对于非聚簇索引叶节点的插入不再是顺序的了,这时就需要离散的访问非聚簇索引页,由于随机读取的存在导致插入操作性能下降。

    InnoDB为此设计了Insert Buffer来进行插入优化。对于非聚簇索引的插入或者更新操作,不是每一次都直接插入到索引页中,而是先判断插入的非聚集索引是否在缓冲池中,若在,则直接插入;若不在,则先放入到一个Insert Buffer中。

    看似数据库这个非聚集的索引已经查到叶节点,而实际没有,这时存放在另外一个位置。然后再以一定的频率和情况进行Insert Buffer和非聚簇索引页子节点的合并操作。这时通常能够将多个插入合并到一个操作中,这样就大大提高了对于非聚簇索引的插入性能。

    参考:https://cloud.tencent.com/developer/article/1200824

    参考:mysql技术内幕 Innodb篇

    3、使用事务提升效率

    还有一种说法,使用事务可以提高数据的插入效率,这是因为进行一个INSERT操作时,MySQL内部会建立一个事务,在事务内才进行真正插入处理操作。通过使用事务可以减少创建事务的消耗,所有插入都在执行后才进行提交操作。大概如下:

    START TRANSACTION;
    INSERT INTO `insert_table` (`datetime`, `uid`, `content`, `type`) 
        VALUES ('0''userid_0''content_0', 0);
    INSERT INTO `insert_table` (`datetime`, `uid`, `content`, `type`) 
        VALUES ('1''userid_1''content_1', 1);
    ...
    COMMIT;
    

    参考:https://my.oschina.net/songhongxu/blog/163063

    事务需要控制大小,事务太大可能会影响执行的效率。MySQL有innodb_log_buffer_size配置项,超过这个值会把innodb的数据刷到磁盘中,这时,效率会有所下降。所以比较好的做法是,在数据达到这个这个值前进行事务提交。

    查看:show variables like '%innodb_log_buffer_size%';

    +------------------------+----------+
            | Variable_name          | Value    |
            +------------------------+----------+
            | innodb_log_buffer_size | 67108864 |
            +------------------------+----------+
    

    大概是:64M

    这种写法和批量写入的效果差不多,只不过sql语句还是单句的,然后统一提交。一个瓶颈是SQL语句的大小,一个瓶颈是事务的大小。当我们在提交sql的时候,首先是受到sql大小的限制,其次是受到事务大小的限制。在开启事务的情况下使用批量插入,会节省不少事务的开销,如果要追求极致的速度的话,建议是开着事务插入的。

    不过需要注意一下,内存是有限且共享的,如果批量插入占用太多的事务内存,那么势必会对其他的业务操作等有一定的影响。

    4、通过配置提升读写性能

    也可以通过增大innodb_buffer_pool_size 缓冲区来提升读写性能,只是缓冲区是要占用内存空间的,内存很珍贵,所以这个方案在内存富裕,而性能瓶颈的时候,可以考虑下。

    参考:https://my.oschina.net/anuodog/blog/3002941

    5、索引影响插入性能

    如果表中存在多个字段索引,当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护。这样就降低了数据的插入速度。对于普通的数据表,主键索引是肯定要有的,想要加快性能的话,就是要有序插入,每次插入记录都在索引的最后面,索引的定位效率很高,并且对索引调整较小。如果插入的记录在索引中间,需要B+tree进行分裂合并等处理,会消耗比较多计算资源,并且插入记录的索引定位效率会下降,数据量较大时会有频繁的磁盘操作。

    五、总结

    博主经过测试+谷歌,最终是选用的一次批量插入数据量为max_allowed_packet大小的一半。只是在不断的搜索中,发现影响插入性能的地方挺多的,如果仅仅是拿max_allowed_packet这个参数作为分析,其实是没有意义的,这个参数只是设置最大值,但并不是最佳性能。

    不过需要注意,由于sql语句比较大,所以才执行完插入操作之后,一定要释放变量,不要造成无谓的内存损耗,影响程序性能。

    对于我们的mysql来说也是一样的,mysql的最佳性能是建立在各个参数的合理设置上,这样协同干活儿的效果最佳。如果其他设置不到位的话,就像是木桶原理一样,哪怕内存缓冲区设置的很大,但是性能取决的反而是设置最差的那个配置。关于mysql的配置调优,我们都在路上,加油!