专题一:主键ID生成
- 数据库自增
- 数据库多主模式
- 号段模式
- UUID
- 时间戳+随机数
- Redis里生成
- 雪花算法(SnowFlake)
- 滴滴出品(TinyID)
- 百度(Uidgenertor)
- 美团(Leaf)
- 数据库自增
基于数据库的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`)
)
- UUID
- 时间戳加随机数
- 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());
}
}
}
- 百度(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 业务折衷法-禁止跳页查询
- 用正常的方法取得第一页数据,并得到第一页记录的最大id
- 每次翻页,将order by id limit X,Y 改为order by id where id>max_id limit Y
2.1.3 业务折衷法-允许数据不精准
- 将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除以分库的数量,除不尽的向下取整避免丢数据
查询结果如下:
- 上次查询拿到的结果集中,找到所有库里最小的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查询