一个简单的数据库分库分表主键ID生成策略
Posted MachineLN
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一个简单的数据库分库分表主键ID生成策略相关的知识,希望对你有一定的参考价值。
一些数据量比较大的项目中,如果后台使用单机单表存储数据,则很容易遇到诸如操作缓慢、容量瓶颈和扩展困难等问题。尤其是对响应时间要求比较高的项目,数据库执行的速度直接决定了项目的成败,所以,大部公司对于性能要求比较高的项目都采用了分库分表的策略。虽然分库分表极大的提高了数据库的操作性能,但是进行拆分之后,数据库的操作便不如单表方便,比如如何让多表多库能够最均匀的分担流量、如何保证主键id的唯一性、如何进行事务管理和跨节点的集合及聚合运算等,这些问题现如今都有比较成熟的解决方案,本文即是针对分库和分表后如何生成主键id进行讨论。
提到唯一性,我想大多数人都会想到UUID,也有很多的项目确实是使用UUID作为主键id,但是UUID生成的效率是非常低的,java中UUID的生成依赖系统提供随机数,而系统提供的随机数是源阻塞的,直接造成UUID生成效率低下。另外,当mysql数据库使用innoDB引擎时,在主键是有序的情况下,会使数据库的操作效率显著提高,这是因为innoDB采用b+树的存储结构,而UUID显然是无序的。还有一点也应当引起我们的注意,UUID本生的存储成本很高。
另外一种使用比较普遍的方法就是构建一个Sequence表,该表中只有一个数据,这种方法正好也是我目前的正在开发的一个任务系统中使用的。该项目中当用户领取一个任务的时候,系统就会生成一个含有id(主键id)、userName(用户名)和taskId(任务id)等的记录,在生成该记录的时候会先查询sequence表中的SEQUENCE值,将该值加1之后作为用户任务记录的id插入到数据库中。该表的结构如下图:
为了保证获取ID数值的唯一性,必须要对代码加上同步锁,这里为了减少与数据库的读写次数,建议先从数据库中获取到数值,一个块内的id获取可以放在内存中进行,块读取结束之后再回写到数据库中,这个块不宜设置的太大。
public synchronized long get(String sequenceName) {
//Step是一个静态内部类,该类中维护这一个块的id
Step step = stepMap.get(sequenceName);
//判断当前内存中是否存在该id值
if(step ==null) {
step = new Step(startValue,startValue+blockSize);
stepMap.put(sequenceName, step);
} else {
//如果已经存在内存中,则直接返回当前id+1的值
if (step.currentValue < step.endValue) {
return step.incrementAndGet();
}
}
//如果当前的数据块使用完成,则将sequenceValue表中的SEQUENCE值更新为最新的id值
//并返回下一个数据块,其中blockSize是数据块的大小
for (int i = 0; i < blockSize; i++) {
if (getNextBlock(sequenceName,step)) {
return step.incrementAndGet();
}
}
}
这个方案应对一般的数据量访问没有什么问题,但是它有个很明显的缺陷,即sequence表的访问速度是性能瓶颈,每一次领取任务记录的插入都要同步的获取主键id。那么如何优化这个主键获取的策略呢,下一次将会在此基础上结合flickr团队发表的一种主键生成策略优化当前开发的系统。flicker团队的方案是针对每个表都设置一个初始值,该初始值是连续的,例如我们有4个任务表,那四个表的初始值可以分别设置为1,2,3,4,每个表递增的步长设置为4的倍数,例如递增步长为4,那对于第表1生成的主键id就是1,5,9,…,表2为2,6,10,…表3为3,7,11,…,表4为4,8,12,…,这样就能够保证主键id不重复。
总结:本文目前介绍的方案简单且基础,适合初学者粗略的了解一下,应对一般的分库分表的项目是完全足够的,大家可以自己动手写一个模拟代码。
MachineLN 交流群请扫码加machinelp为好友:
以上是关于一个简单的数据库分库分表主键ID生成策略的主要内容,如果未能解决你的问题,请参考以下文章