高并发分布式无碰撞ID生成机制
转载请注明出处:奇思漫想
由于用户管理(APP后端的订单号生成机制是采用 机构编号+时间戳+3位随机数+3位随机数)在高并发情况下碰撞几率很高。
正好诊疗卡支付的订单号需要完善,于是写了一套id生成机制。主要思路 机器(mac地址/IP)+时间戳+同一毫秒内的自增序列号(达到最大值等待等到下一毫秒重新从0开始)。底层主要依靠原子对象AtomicLong的CAS操作compareAndSet(expect,update),如果了解虚拟机的锁机制对这个应该会了解。
经测试自己的笔记本平均可支持一秒内生成30000条唯一ID。
拓展说明
-
本类会默认生成一个长度为19位的唯一ID,可支持同一局域网内多机超高并发不唯一,最长支持到2286年(请在此日期前更换id生成机制)
-
多局域网共享同一数据库的话,高并发情况下可能会产生碰撞(解决办法是依靠19位之外的数据位,假如你的订单是32位的你可以取其中两位作为机房编号,通过配置项的形式进行配置)
-
你可以在此订单号的基础上加入一些其他的标志位,用以拓展订单信息(日期等)或适应更复杂的编号生成环境,如多局域网(多机房)。
-
目前作者遇到的最复杂的 高并发分布式自增ID机制 也可以由此拓展而来。
代码如下
package com.yuantu.gateway.utils;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.text.NumberFormat;
import java.util.Date;
import java.util.concurrent.atomic.AtomicLong;
/**
* 唯一编号,经测试作者本人的笔记本平均每毫秒能生成30条左右唯一ID,单机平均每秒30000个唯一ID
* 本类会默认生成一个长度为19位的唯一ID,可支持同一局域网内多机超高并发不唯一,最长支持到2286年(请在此日期前更换id生成机制)
* 多局域网共享同一数据库的话,高并发情况下可能会产生碰撞(解决办法是依靠19位之外的数据位,假如你的订单是32位的你可以取其中两位作为机房编号,通过配置项的形式进行配置)
* 你可以在此订单号的基础上加入一些其他的标志位,用以拓展订单信息(日期等)或适应更复杂的编号生成环境,如多局域网(多机房)。
* IP后缀(192.168.20.12 -> 012)+ 时间戳 + 序列号
* Created by zilue on 2017/4/22.
*/
public class UID {
private static AtomicLong atomicLong = new AtomicLong(0L);
private static AtomicLong timestamp = new AtomicLong(System.currentTimeMillis());
private static final Long DEFAULT_MAX = 999L;
private static String ip_postfix;
private static NumberFormat numberFormat;
static {
try {
InetAddress ia = InetAddress.getLocalHost();
String[] split = ia.getHostAddress().split("\\.");
ip_postfix = split[split.length - 1];
NumberFormat numberFormat = NumberFormat.getNumberInstance();
numberFormat.setGroupingUsed(false);
numberFormat.setMinimumIntegerDigits(3);
ip_postfix = numberFormat.format(Long.parseLong(ip_postfix));
} catch (UnknownHostException e) {
e.printStackTrace();
}
numberFormat = NumberFormat.getNumberInstance();
int seuquenceLen = String.valueOf(DEFAULT_MAX).length();
numberFormat.setMinimumIntegerDigits(seuquenceLen);
numberFormat.setGroupingUsed(false);
}
public static void main(String[] args) {
System.out.println(getUniqueId());
Long maxtimstamp=9999999999999L;
Date date=new Date(maxtimstamp);
System.out.println(date.toString());
}
public static String getUniqueId() {
String formatSequence = numberFormat.format(getSequence(DEFAULT_MAX));
return ip_postfix +System.currentTimeMillis()+ formatSequence;
}
private static String getUniqueId(Long maxPerMillisecond) {
if (maxPerMillisecond == null)
maxPerMillisecond = DEFAULT_MAX;
NumberFormat numberFormat = NumberFormat.getNumberInstance();
numberFormat.setMinimumIntegerDigits(String.valueOf(DEFAULT_MAX).length());
Long sequence = getSequence(maxPerMillisecond);
String formatSequence = numberFormat.format(sequence);
return ip_postfix +System.currentTimeMillis()+ formatSequence;
}
/**
* get a sequence(number) in a millisecond,if sequenceNumber equels maxPerMillisecond,
* it will return at next millisecond with a sequenceNumber which will increase from zero again
*
* @param maxPerMillisecond
* @return
*/
public static Long getSequence(Long maxPerMillisecond) {
long nextSequence = atomicLong.get();
while (nextSequence >= maxPerMillisecond || !atomicLong.compareAndSet(nextSequence, ++nextSequence)) {
if (nextSequence >= maxPerMillisecond && timestamp.longValue() >= System.currentTimeMillis()) {
nextSequence = atomicLong.get();
} else if (nextSequence >= maxPerMillisecond) {
if (atomicLong.compareAndSet(maxPerMillisecond, 0)) {
timestamp.set(System.currentTimeMillis());
nextSequence = 0L;
}
}
}
return nextSequence;
}
}
2017-05-07追加:
5月初恰好用户管理与his传的业务参数发生了id碰撞。
晓飞哥准备让用户管理用32位ID,便采用了12位全IP(高位补领)。过了几天又准备用64位ID,于是向师兄提出将Mac地址的10进制值、进程ID、全IP全部记录下来便可以支持非常高的高并发ID 。组成了固定值+其他值+mac地址+IP全路径+进程ID+时间戳+毫秒自增序列值。
下面说一下各个值的作用。
- 固定值:可以用来拓展
- 其他值:一些业务需要的值也可以看作拓展值
- mac地址:10进制的mac地址,区分一台电脑
- IP全路径:12位的ip全路径,区分同一个网络中不同的机器。
- 进程ID:假如一台机器上有两个tomcat可以通过进程ID区分
- 时间戳:这个不必说了吧(这里有个时间回拨的问题,如果你对机器时间做了调整,一定要注意 ,如何避免这又是一个问题)
- 毫秒自增序列:一台机器毫秒内最大ID数量
如果出现以上全一样的情况那么祝贺你,你该买彩票了
其实主要的理念就是尽量区分开每台机器,每个tomcat程序,时间戳,再保证单机器原子循环自增。