现在分布式越来越普遍了,怎么在分布式服务设计一套id生成策略呢?这里提供一套基于数据配置的分段自增id生成工具。基本满足同数据库多服务共用的id生成需求。另外可根据需要灵活配置,操作方便。
首先需求保证单应用内的线程安全,使用传统的synchronized来解决。采用数据库的隔离级别和对比老值得方式,实现多服务之间的数据安全,避免出现id重复现象。此外,每次取一段值保存在服务中,能够减少对数据库的访问,提升性能。
CREATE TABLE `table_id` (
`key` varchar(50) NOT NULL,
`value` bigint(20) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
key为表的名称,value为表的id值。服务每次从数据库取值时都会修改value为新的值。
id工具类
package com.david.concurrentdemo2;
import com.david.concurrentdemo2.service.UserServiceImpl;
import com.david.concurrentdemo2.util.SpringContextHolder;
import java.util.HashMap;
import java.util.Map;
/**
* @author David
* @descritpion id生成工具
* 思路: 首先在单机内线程安全,然后利用mysql的隔离级别,保证每次只能有一个机器获取id成功,每次获取2000个id缓存起来,
* 使用完再次获取
* @date 2019/10/22
*/
public class IdUtil {
/**
* 定义一个变量作为id基数
*/
private static long idNum = 0;
/**
* 每次的id缓存
*/
private static int idcache = 0;
/**
* 每次的增长数
*/
private final static int stepNum = 2000;
private static UserServiceImpl userService ;
/**
* 首次调用从数据库获取当前id的值
*/
static {
userService = SpringContextHolder.getBean(UserServiceImpl.class);
idNum = userService.getTableIdByKey("test");
}
public synchronized static long getId() {
//每次获取的id的值为基础值加上缓存的数值,如果缓存中用完了,则刷新id基础值和缓存
if (idcache > 0) {
return idNum + (stepNum - --idcache);
} else {
if (refreshId()) {
return idNum + (stepNum - --idcache);
}
return -1;
}
}
/**
* 每次刷新的时候,拿老的id和数据对比,一样则修改成功。不一样修改失败。
* 修改失败后,从数据库获取当前的id作为基础id,再次进行修改。此过程共可执行三次。
* 修改成功则从新设置基础的id和缓存id
* @return
*/
private static boolean refreshId() {
for (int i = 0; i < 3; i++) {
try {
if (updateTableId( idNum + stepNum) ){
idNum = idNum + (i+1) * stepNum;
idcache = stepNum;
return true;
} else {
//重新从数据库获取id位置
idNum = userService.getTableIdByKey("test");
}
} catch (Exception e) {
e.printStackTrace();
}
}
return false;
}
/**
* 修改成功返回true,修改失败返回false。修改过程中出现异常,会进行再次修改。最多执行三次。
* 利用数据库的隔离级别和对比旧值来实现不同服务间的数据安全。
* @param num
* @return
*/
public static boolean updateTableId(long num) {
Map map = new HashMap<>();
map.put("key", "test");
map.put("oldValue", num - stepNum);
map.put("value", num);
for (int i = 0; i < 3; i++) {
try{
if(userService.updateTableId(map) == 1) {
return true;
}
return false;
} catch (Exception e) {
e.printStackTrace();
}
}
return false;
}
}
测试代码
public void test1() {
List<Long> longL = Collections.synchronizedList(new ArrayList<>());
Set<Long> longS = Collections.synchronizedSet(new HashSet<>());
CountDownLatch count = new CountDownLatch(4);
long start = System.currentTimeMillis();
for (int i = 0; i < 4; i++) {
new Thread(() ->{
for (int j = 0; j < 20000; j++) {
long id = IdUtil.getId();
longL.add(id);
longS.add(id);
//System.out.println(id);
}
count.countDown();
}).start();
}
try {
count.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("user time:" + (end - start));
System.out.println(longL.size());
System.out.println(longS.size());
longL.forEach(ele -> {
userService.insertResult(ele);
});
}
多线程下id生成没有问题,最后把id插入验证表(主键唯一),插入无异常。
/**
* 耗时1.626
*/
@Test
public void test2() {
long start = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
IdUtil.getId();
}
long end = System.currentTimeMillis();
System.out.println("use time:" + (end - start));
}
两个应用单台机器上同时跑,单个应用100万个id的获取时间在1.5s左右。
可根据实际的需求设置缓存数据的大小。使用数据库记录id值速度上比不上redis,安全性会好一些。请大神指教!