• 性能优化之数据库3-主键生成与分页查询


    专题一:主键ID生成

    • 数据库自增
    • 数据库多主模式
    • 号段模式
    • UUID
    • 时间戳+随机数
    • Redis里生成
    • 雪花算法(SnowFlake)
    • 滴滴出品(TinyID)
    • 百度(Uidgenertor)
    • 美团(Leaf)
    1. 数据库自增

    基于数据库的auto_increment自增ID完全可以充当分布式ID。需要单独有个Mysql实例用来生成ID。

    缺点:数据量大的时候,数据库本身就是一个瓶颈,而且单点的DB容易存在单点故障
    2. 数据库多主模式

    也就是数据库做成主从模式集群。为防止一个主节点挂掉,就做双主模式集群。但是这样两个Mysql实例的自增ID都从1开始,重复了怎么办?

    解决方案:设置起始值和步长。 例如db1:初始值1,步长2. db2:初始值2,步长2.
    3. 号段模式

    号段模式可以理解为从数据库批量的获取自增ID,比如每次获取一千个ID给具体的服务使用。

    CREATE TABLE id_generator (
      id int(10) NOT NULL,
      max_id bigint(20) NOT NULL COMMENT '当前最大id',
      step int(20) NOT NULL COMMENT '号段的布长',
      biz_type    int(20) NOT NULL COMMENT '业务类型',
      version int(20) NOT NULL COMMENT '版本号',
      PRIMARY KEY (`id`)
    ) 
    
    1. UUID
    2. 时间戳加随机数
    3. Redis里生成

    通过redis的incr命令实现
    7. 雪花算法(Snowflake)

    SnowFlake ID组成结构:正数位(1bit)+时间戳(41bit)+机器ID(5bit)+数据中心(5bit)+自增值(12bit)

    • 第一位bit:Java中long的最高位是符号位,正数是0,负数是1,一般生成的id都是正数,所以默认为0
    • 时间戳部分(41bit):毫秒级别的时间,不建议存当前时间戳,而是用(当前时间戳-固定开始时间戳)的差值,可以使产生的ID从更小的值开始。
    • 工作机器id(10bit):可以灵活配置,机房或机器号组合都可以
    • 自增值(12bit):支持同一毫秒内同一个节点生成4096个ID
    public class SnowFlakeShortUrl {
    
        /**
         * 起始的时间戳
         */
        private final static long START_TIMESTAMP = 1480166465631L;
    
        /**
         * 每一部分占用的位数
         */
        private final static long SEQUENCE_BIT = 12;   //序列号占用的位数
        private final static long MACHINE_BIT = 5;     //机器标识占用的位数
        private final static long DATA_CENTER_BIT = 5; //数据中心占用的位数
    
        /**
         * 每一部分的最大值
         */
        private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);
        private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
        private final static long MAX_DATA_CENTER_NUM = -1L ^ (-1L << DATA_CENTER_BIT);
    
        /**
         * 每一部分向左的位移
         */
        private final static long MACHINE_LEFT = SEQUENCE_BIT;
        private final static long DATA_CENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
        private final static long TIMESTAMP_LEFT = DATA_CENTER_LEFT + DATA_CENTER_BIT;
    
        private long dataCenterId;  //数据中心
        private long machineId;     //机器标识
        private long sequence = 0L; //序列号
        private long lastTimeStamp = -1L;  //上一次时间戳
    
        private long getNextMill() {
            long mill = getNewTimeStamp();
            while (mill <= lastTimeStamp) {
                mill = getNewTimeStamp();
            }
            return mill;
        }
    
        private long getNewTimeStamp() {
            return System.currentTimeMillis();
        }
    
        /**
         * 根据指定的数据中心ID和机器标志ID生成指定的序列号
         *
         * @param dataCenterId 数据中心ID
         * @param machineId    机器标志ID
         */
        public SnowFlakeShortUrl(long dataCenterId, long machineId) {
            if (dataCenterId > MAX_DATA_CENTER_NUM || dataCenterId < 0) {
                throw new IllegalArgumentException("DtaCenterId can't be greater than MAX_DATA_CENTER_NUM or less than 0!");
            }
            if (machineId > MAX_MACHINE_NUM || machineId < 0) {
                throw new IllegalArgumentException("MachineId can't be greater than MAX_MACHINE_NUM or less than 0!");
            }
            this.dataCenterId = dataCenterId;
            this.machineId = machineId;
        }
    
        /**
         * 产生下一个ID
         *
         * @return
         */
        public synchronized long nextId() {
            long currTimeStamp = getNewTimeStamp();
            if (currTimeStamp < lastTimeStamp) {
                throw new RuntimeException("Clock moved backwards.  Refusing to generate id");
            }
    
            if (currTimeStamp == lastTimeStamp) {
                //相同毫秒内,序列号自增
                sequence = (sequence + 1) & MAX_SEQUENCE;
                //同一毫秒的序列数已经达到最大
                if (sequence == 0L) {
                    currTimeStamp = getNextMill();
                }
            } else {
                //不同毫秒内,序列号置为0
                sequence = 0L;
            }
    
            lastTimeStamp = currTimeStamp;
    
            return (currTimeStamp - START_TIMESTAMP) << TIMESTAMP_LEFT //时间戳部分
                    | dataCenterId << DATA_CENTER_LEFT       //数据中心部分
                    | machineId << MACHINE_LEFT             //机器标识部分
                    | sequence;                             //序列号部分
        }
    
        public static void main(String[] args) {
            SnowFlakeShortUrl snowFlake = new SnowFlakeShortUrl(2, 3);
    
            for (int i = 0; i < (1 << 4); i++) {
                //10进制
                System.out.println(snowFlake.nextId());
            }
        }
    }
    
    1. 百度(uid-generator)

    uid-generator是由百度技术部开发,项目GitHub地址 https://github.com/baidu/uid-generator
    9. 美团(Leaf)

    Leaf由美团开发,github地址:https://github.com/Meituan-Dianping/Leaf
    10. 滴滴(Tinyid)

    Tinyid由滴滴开发,Github地址:https://github.com/didi/tinyid。

    专题二:分页查询

    在分页查询中,容易遇到哪些问题呢?

    问题1:使用分页插件

    分页插件的原理是把你的sql加一个括号,然后用count(*)来计算行数。

    select count(*) from ("你的sql")

    解决:如果你的sql是简单sql那到还好,如果是复杂的连接查询,那就会很慢。可以根据业务情况改写,弃用分页插件。

    问题2:大数量级别的分页问题,如limit 100000,20

    • 方案1:简单优化
    SELECT * FROM table WHERE id >= (SELECT id FROM table LIMIT 1000000, 1) LIMIT 10;
    
    • 方案2:如果分页数量到了最后几页,可以通过反序取前几页的方法。
    • 方案3:如果业务允许精度丢失,我们可以大概估算出id的话,将id带到查询条件。如:where id>100000 limit 20(适用于id连续的情况),如果有数据删除就会有一些不准确。
    • 方案4:禁止跨页查询,如果业务允许只能一页页翻的话,可以保存每次查询的结果的最大id,带到下一页查询里面去。

    问题3:各种查询条件任意组合的进行查询?

    解决:这种情况,涉及到的字段很多,不能都加索引,可以采用搜索引擎来解决。

    2.1 跨库分页查询

    2.1.1 全局视野法

    将原有的sql:select * from t_order order by id limit 20,10 改成 select * from t_order order by id limit 0,30

    即将limit X,Y 改为 limit 0,X+Y

    缺点:这种方法随着翻页的进行,性能越来越低。其实改不改写sql,数据库端的查询压力是差不多的,因为limit 0,5000 和 limit 4900,100,数据库都是差不多要扫描4900行才能取到数据,limit 0,5000最大的影响是客户端内存消耗和网络消耗变大了。

    sharding-jdbc进行分页查询的时候就是采用这个方法,但是它利用流式处理和优先级队列解和的方式,消除了客户端内存消耗的压力,但是网络消耗还是无法消除。

    2.1.2 业务折衷法-禁止跳页查询

    1. 用正常的方法取得第一页数据,并得到第一页记录的最大id
    2. 每次翻页,将order by id limit X,Y 改为order by id where id>max_id limit Y

    2.1.3 业务折衷法-允许数据不精准

    1. 将order by id limit X,Y 改写为 order by id limit X/N,Y/N ,N代表分库数量。

    如limit 600,100,一共分了两个库,我们假设数据是均匀的,那么每个库取50条,并且都从300那里取,然后把数据合并起来就得到了我们想取到的100条数据。改为limit 300,50。

    2.1.4 二次查询法

    前提:多个库的数据是均摊的并且是取余进行分片的,不能是分段的(某一时间段的数据都在一个库),不能某个库数据有某块的缺失

    示例数据:

    假如我们要查询order by id limit 5,4(结果应该为6,7,8,9)

    1. 第一次sql改写

    order by id limit 2,4

    注意:这里的2是5除以分库的数量,除不尽的向下取整避免丢数据

    查询结果如下:

    1. 上次查询拿到的结果集中,找到所有库里最小的id,记为all_min_id,自己的库里面最大的id,记为max_id

    3. 第二次sql改写

    where id bewteen all_min_id and max_id

    库1:where id bewteen 5,14

    库2:where id bewteen 5,11

    查询结果如下:

    然后在结果集中排序,取出最小的4个值。所以结果为5,6,7,8

    优点:

    • 返回的数据不多
    • 满足业务精确需求
    • 性能高

    缺点:

    • 有使用条件限制,需要数据均摊
    • 需要二次sql查询
    书山有路勤为径,学海无涯苦作舟
  • 相关阅读:
    第08组 Alpha冲刺 (2/6)
    第08组 Alpha冲刺 (1/6)
    结对编程作业
    第01组 Alpha冲刺总结
    第01组 Alpha冲刺(6/6轮)
    第01组 Alpha冲刺(6/6)
    第01组 Alpha冲刺(5/6)
    第01组 Alpha冲刺(5/6轮)
    第01组 Alpha冲刺(4/6)
    第01组 Alpha冲刺(3/6)
  • 原文地址:https://www.cnblogs.com/javammc/p/15017893.html
Copyright © 2020-2023  润新知