性能优化之数据库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查询

相关文章

摘要: 原创出处 https://www.bysocket.com 「公众号:泥瓦匠...
摘要: 原创出处 https://www.bysocket.com 「公众号:泥瓦匠...
今天犯了个错:“接口变动,伤筋动骨,除非你确定只有你一个...
Writer :BYSocket(泥沙砖瓦浆木匠)微 博:BYSocket豆 瓣:...
本文目录 线程与多线程 线程的运行与创建 线程的状态 1 线程...