设置MySQL数据库唯一性ID的方法
1.使用uuid来作为ID
使用方法如下:
System.out.println(UUID.randomUUID());
System.out.println(UUID.randomUUID().toString().length());
由此可知,uuid获取的值是一串长度为36的字符
此种方法的优缺点如下:
优点
1.使用起来很简单
2.不影响数据库的拓展,比如分表分库
缺点
1.表结构中ID类型是字符串且长度在36,空间占用大
2.mysql数据库在索引上使用的是B+树算法,有序的查找效率更高,且整型的查找效率比字符串类型要高,所以使用无序字符串作为ID类型,索引效率低。
3.当服务器集群的时候,并发量大时可能会出现ID重复的情况
2.使用数据库ID的自增方法
使用方法如下:
在设置表结构的时候,将ID设置为整型且自动增长。
因为这种方式在数据库集群的时候,会出现大量ID重复的情况,所以在集群环境下,可以对不同的库设置不同的起始值和自增量
MySQL下的修改配置如下:
1.show VARIABLES like ‘auto_inc%’ 先查看当前配置
2.auto_increment_offset 参数代表起始值,
修改该参数配置的语法为 set @@auto_increment_offset =n(根据情况自行设置)
3.auto_increment_increment 参数代表自增量,
修改该参数配置的语法为 set @@auto_increment_increment = n(根据情况自行设置)
例如:当有100以内的数据库集群的时候,可以将自增量设置为100,而给每个数据库设置从1-99的起始值,这样自增的ID就不会出现重复的情况。
优缺点如下:
优点:
1.使用起来也很简单,只需在建表的时候修改几个参数配置
2.ID为有序的整型,索引效率高
3.性能也比较好
缺点:
1.不适合水平分表,因为在不同表下新增ID时会出现重复的ID
2.前期建表的时候就要先规划好配置的值,不方便后期拓展
3.依赖数据库的自增锁,在高并发情况下会影响性能
3.使用雪花算法
由图可知该算法分为4个部分
1.第一位
占用1bit,其值始终是0,没有实际作用。
2.时间戳
占用41bit,精确到毫秒,总共可以容纳约69 年的时间。
3.工作机器id
占用10bit,其中高位5bit是数据中心ID(datacenterId),低位5bit是工作节点ID(workerId),最多可以容纳1024个节点。
4.序列号
占用12bit,这个值在同一毫秒同一节点上从0开始不断累加,最多可以累加到4095。
所以这种算法在同一毫秒内可以产生的ID数为1024*4096=4194304个,对于绝大多数场景下的高并发已经足够使用了
该算法的实现代码如下:
public class SnowFlake {
//2019-01-01的毫秒数
private static final long Initial_time_stamp =1548950400000L;
//工作节点id所占的位数
private static final long Worker_ID_bits = 5L;
//数据中心id所占的位数
private static final long Data_center_ID_bits = 5L;
//序列号所占的位数
private static final long Serial_number_bits=12L;
//工作节点的最大数(根据二进制数算出最多十进制数)
private static final long Max_Worker_ID = -1L ^ (-1L<<Worker_ID_bits);
//数据中心的最大数
private static final long Max_Data_center_ID = -1L ^ (-1L<<Data_center_ID_bits);
//序列号的最大数
private static final long Max_Serial_number = -1L ^ (-1L<<Serial_number_bits);
//数据中心ID
private long datacenterId;
//工作节点ID
private long workerId;
//上次时间戳毫秒值
private static long lastTimestamp;
//序列号
private static long serialNumber=0L;
//时间戳偏移位数
private static final long timeStamp_Offset=Data_center_ID_bits+Worker_ID_bits+Serial_number_bits;
//数据中心偏移位数
private static final long datacenter_Offset=Worker_ID_bits+Serial_number_bits;
//工作节点偏移位数
private static final long worker_Offset=Serial_number_bits;
/**
* 无参构造函数默认取数据中心ID和工作节点ID为最大值
*/
public SnowFlake() {
this.datacenterId = Max_Data_center_ID;
this.workerId=Max_Worker_ID;
}
/**
* @param datacenterId 数据中心ID
* @param workerId 工作节点ID
*/
public SnowFlake(long datacenterId, long workerId) {
if(datacenterId<0||datacenterId>Max_Data_center_ID){
throw new IllegalArgumentException(String.format("数据中心ID不能小于0或大于%d",Max_Data_center_ID));
}
if(workerId<0||workerId>Max_Worker_ID){
throw new IllegalArgumentException(String.format("工作节点ID不能小于0或大于%d",Max_Worker_ID));
}
this.datacenterId = datacenterId;
this.workerId = workerId;
}
public synchronized long getNextID(){
long timeStamp = System.currentTimeMillis();
// 如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
if(timeStamp<lastTimestamp){
//服务器时钟被调整了,ID生成器停止服务.
throw new RuntimeException(String.format("当前时间戳小于上次添加的时间.在小于之前时间%d毫秒的情况下禁止生成ID", lastTimestamp - timeStamp));
}
//当毫秒值相同时,序列号自增
if(timeStamp==lastTimestamp){
serialNumber=(serialNumber+1)&Max_Serial_number;
//当前毫秒内序列已经满了,需要等待下一毫秒
if(serialNumber==0){
timeStamp = getNextMills(lastTimestamp);
}
}else {
serialNumber=0L;
}
//将当前时间戳赋值给上次时间戳
lastTimestamp=timeStamp;
return ((timeStamp-Initial_time_stamp)<<timeStamp_Offset)|(datacenterId<<datacenter_Offset)|(workerId<<worker_Offset)|serialNumber;
}
/**
* 再次获取一次时间戳,与上次进行比较
* @param lastTimestamp
* @return
*/
public long getNextMills(long lastTimestamp){
long currentTimeMillis = System.currentTimeMillis();
while (currentTimeMillis<=lastTimestamp){
currentTimeMillis=System.currentTimeMillis();
}
return currentTimeMillis;
}
}
该种方法的优缺点
优点:
1.性能较好,速度快
2.可以根据情况拓展该算法,比较灵活
3.无需其他依赖,使用也比较简单
缺点:
1.依赖机器时间,当数据库服务器时间回调时,可能会出现ID重复的情况
4.使用Redis的自增方法
使用redis的incr自增的方法,这种方法利用了Redis的特性:单线程原子操作、自增计数API、数据有效期机制。
可以自行设定规则,例如 时间+自增数值 来制成一个唯一性ID
实现代码如下:
public String getID(){
long id = redisTemplate.opsForValue().increment("Market_Order_ID");
SimpleDateFormat format=new SimpleDateFormat("yyyyMMddHHmmss");
String prefix="";
//获取当前时间的年月日作为ID的前缀
try {
prefix = format.format(new Date());
} catch (Exception e) {
e.printStackTrace();
}
//自动补0的语法,当整型长度没有超过5时自动在前面补0
String format_id = String.format("%1$05d", id);
//根据时间和ID拼接
return prefix+format_id;
}
这种方法的优缺点如下:
优点:
1.拓展性强,可以结合业务场景进行处理
2.利用Redis原子性的特性,在高并发情况下ID不会重复
3.性能好
缺点
1.依赖Redis的使用
2.增加了网络开销
3.需要对Redis实现高可用
5.高并发场景测试
这几种方法均可用CountDownLatch类在高并发场景下测试
以下是使用Redis自增的方法进行高并发测试
public class ServiceApplicationTest {
@Autowired
private StringRedisTemplate redisTemplate;
private final static int count=500;//线程并发数
private static CountDownLatch countDownLatch =new CountDownLatch(count);
public void testRedisId(){
System.out.println(getID(););
}
@Test
public void testMain(){
System.out.println("-------------start-------------");
for (int i=0;i<count;i++){
new Thread(new ThreadDemo()).start();
//当countDownLatch中的数量减至0时,线程全部被唤醒
countDownLatch.countDown();
}
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("-------------end-------------");
}
public String getID(){
long id = redisTemplate.opsForValue().increment("Market_Order_ID");
SimpleDateFormat format=new SimpleDateFormat("yyyyMMddHHmmss");
String prefix="";
//获取当前时间的年月日作为ID的前缀
try {
prefix = format.format(new Date());
} catch (Exception e) {
e.printStackTrace();
}
//自动补0的语法,当整型长度没有超过5时自动在前面补0
String format_id = String.format("%1$05d", id);
//根据时间和ID拼接
return prefix+format_id;
}
class ThreadDemo implements Runnable {
@Override
public void run() {
try {
//线程在这里进行等待
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
testRedisId();
}
}
}
总结
方法 | 优点 | 缺点 |
UUID | 实现简单、不占用网络 | 无序、占空间、索引不友好 |
数据库自增 | 无代码调整、递增 | 数据库单点故障、扩展性有瓶颈 |
雪花算法 | 性能优、不占用网络、趋势递增 | 依赖服务器时间 |
Redis自增 | 无单点故障、性能优于DB、递增、拓展性好 | 占用网络连接、需要维护Redis集群 |