前言
当需要用到分布式id时候,必然需要不同的id,自增已经不合用了。
下面将整合雪花算法,已经对应的配置中心,简要介绍id的生成策略。
参考:
Twitter的分布式自增ID算法snowflake (Java版)
当然,请注意,这次插件要用到mac地址的,请看看下面的文章。
【代码打假】java如何获取mac物理地址?
整合思路说明
前人已经写了很多雪花算法的实现了,算法实现非难点。雪花算法强依赖于时间还有本机的编号,最多有1024个编号组合。
也就是说,每一个雪花算法的实例,只要编号不同那么生成的id就不会重复—这就引出了一个问题,同一个编号的多个不同的雪花算法实例,那么生成的id肯定会重复。
经过考虑之后,得到基本思路:每一台部署雪花id生成服务的机器有一个唯一的mac地址,该地址对应着一个编号,一台机器只能有一个雪花id生成服务,杜绝并发而产生冲突id的可能性。
必须的算法、工具支持
在整合前,基本上起码也得要有雪花算法的实现啊。。
下面是必要的代码工具:
package net.w2p.Shared.common;
/**
*
* 来源:https://www.cnblogs.com/relucent/p/4955340.html
* 作者:永夜微光
* Twitter_Snowflake<br>
* SnowFlake的结构如下(每部分用-分开):<br>
* 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 <br>
* 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0<br>
* 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截)
* 得到的值),这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下下面程序IdWorker类的startTime属性)。41位的时间截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69<br>
* 10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId<br>
* 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号<br>
* 加起来刚好64位,为一个Long型。<br>
* SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。
*/
public class SnowflakeIdWorker {
// ==============================Fields===========================================
/** 开始时间截 (2019-02-01) */
private final long twepoch = 1548950400000L;
/** 机器id所占的位数 */
private final long workerIdBits = 5L;
/** 数据标识id所占的位数 */
private final long datacenterIdBits = 5L;
/** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
/** 支持的最大数据标识id,结果是31 */
private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
/** 序列在id中占的位数 */
private final long sequenceBits = 12L;
/** 机器ID向左移12位 */
private final long workerIdShift = sequenceBits;
/** 数据标识id向左移17位(12+5) */
private final long datacenterIdShift = sequenceBits + workerIdBits;
/** 时间截向左移22位(5+5+12) */
private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
/** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
/** 工作机器ID(0~31) */
private long workerId;
/** 数据中心ID(0~31) */
private long datacenterId;
/** 毫秒内序列(0~4095) */
private long sequence = 0L;
/** 上次生成ID的时间截 */
private long lastTimestamp = -1L;
//==============================Constructors=====================================
/**
* 构造函数
* @param workerId 工作ID (0~31)
* @param datacenterId 数据中心ID (0~31)
*/
public SnowflakeIdWorker(long workerId, long datacenterId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
// ==============================Methods==========================================
/**
* 获得下一个ID (该方法是线程安全的)
* @return SnowflakeId
*/
public synchronized long nextId() {
long timestamp = timeGen();
//如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
if (timestamp < lastTimestamp) {
throw new RuntimeException(
String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
//如果是同一时间生成的,则进行毫秒内序列
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
//毫秒内序列溢出
if (sequence == 0) {
//阻塞到下一个毫秒,获得新的时间戳
timestamp = tilNextMillis(lastTimestamp);
}
}
//时间戳改变,毫秒内序列重置
else {
sequence = 0L;
}
//上次生成ID的时间截
lastTimestamp = timestamp;
//移位并通过或运算拼到一起组成64位的ID
return ((timestamp - twepoch) << timestampLeftShift) //
| (datacenterId << datacenterIdShift) //
| (workerId << workerIdShift) //
| sequence;
}
/**
* 阻塞到下一个毫秒,直到获得新的时间戳
* @param lastTimestamp 上次生成ID的时间截
* @return 当前时间戳
*/
protected long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
/**
* 返回以毫秒为单位的当前时间
* @return 当前时间(毫秒)
*/
protected long timeGen() {
return System.currentTimeMillis();
}
//==============================Test=============================================
/** 测试 */
public static void main(String[] args) {
SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0, 0);
for (int i = 0; i < 1000; i++) {
long id = idWorker.nextId();
System.out.println("二进制形式:"+Long.toBinaryString(id));
System.out.println("Long整数形式:"+id);
}
}
}
还有,需要有获取本机mac地址的能力:
package net.w2p.WebExt.Utils;
import java.net.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.stream.Collectors;
public class MacTools {
/***因为一台机器不一定只有一个网卡呀,所以返回的是数组是很合理的***/
public static List<String> getLocalMacList() throws Exception {
java.util.Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces();
StringBuilder sb = new StringBuilder();
ArrayList<String> tmpMacList=new ArrayList<>();
while(en.hasMoreElements()){
NetworkInterface iface = en.nextElement();
List<InterfaceAddress> addrs = iface.getInterfaceAddresses();
for(InterfaceAddress addr : addrs) {
InetAddress ip = addr.getAddress();
NetworkInterface network = NetworkInterface.getByInetAddress(ip);
if(network==null){continue;}
byte[] mac = network.getHardwareAddress();
if(mac==null){continue;}
sb.delete( 0, sb.length() );
for (int i = 0; i < mac.length; i++) {sb.append(String.format("%02X%s", mac[i], (i < mac.length - 1) ? "-" : ""));}
tmpMacList.add(sb.toString());
} }
if(tmpMacList.size()<=0){return tmpMacList;}
/***去重,别忘了同一个网卡的ipv4,ipv6得到的mac都是一样的,肯定有重复,下面这段代码是。。流式处理***/
List<String> unique = tmpMacList.stream().distinct().collect(Collectors.toList());
return unique;
}
}
项目准备
请根据以下文章准备好一个微服务模块:
https://blog.csdn.net/cdnight/article/details/86732474
配置完成是这样的:
模块内整合算法id生成器
配置中心添加参数
执行:
create or replace function "initBaseConfig"(
in envName varchar
)
returns varchar
as $BODY$
declare _defaultValues varchar;
declare _envName varchar;
declare _appname varchar;
declare _prefix varchar;
declare strArrays varchar[];
declare arrItemLv1 varchar;
declare tempArrSubItem varchar;
declare valArrs varchar[];
declare item_attr varchar;
declare item_title varchar;
declare item_val varchar;
begin
if envName <> 'test' and envName<> 'ppe' and envName<> 'product' then
raise notice '环境变量异常,只能为test、ppe以及product其中一个。';
return '环境变量异常,只能为test、ppe以及product其中一个。';
end if;
_appname:='base';
_prefix:=concat(_appname,'.','');
_defaultValues:=
'snowflake_roster->雪花算法id生成器的服务器名单,用的是json格式字符串,格式如下:{"服务器mac地址":"工作id,数据中心id"}->{"68-EC-C5-C4-D1-96":"0,0"}$$' ||
''
;
strArrays:=string_to_array(_defaultValues,'$$');
_envName:=envName;
-- fastdfs.connect_timeout_in_seconds = 5
-- fastdfs.network_timeout_in_seconds = 30
-- fastdfs.charset = UTF-8
-- fastdfs.http_anti_steal_token = false
-- fastdfs.http_secret_key = FastDFS1234567890
-- fastdfs.http_tracker_http_port = 80
-- #fastdfs.tracker_servers = tw-server:22122,10.0.11.202:22122,10.0.11.203:22122
-- fastdfs.tracker_servers = localhost:22122
-- fastdfs.visit_url = http://localhost/
-- env varchar(100) not null,
-- key varchar(200) not null,
-- appname varchar(100) not null,
-- title varchar(100) not null,
-- value varchar(2000) default NULL::character varying,
insert into xxl_conf_project (appname, title) values (_appname,'基础设施') on conflict ("appname") do nothing;
<<loop4BigArray>>
foreach arrItemLv1 in array strArrays
loop
if char_length(arrItemLv1) < 1 then
raise notice '空字符串无须处理';
continue ;
end if;
valArrs:=string_to_array(arrItemLv1,'->');
item_attr:=valArrs[1];
item_title:=valArrs[2];
item_val:=valArrs[3];
raise notice '属性名称:%,描述:%,当前值:%',item_attr,item_title,item_val;
raise notice '开始添加记录';
insert into xxl_conf_node("env","key","appname","title","value")
values (_envName,concat(_prefix,item_attr),_appname,item_title,item_val)
on conflict ("env","key") do nothing ;
end loop loop4BigArray;
return envName||'环境下的'||_appName||'配置成功';
end;
$BODY$ language plpgsql volatile ;
-- 记住执行下面方法分别添加三个环境下的默认数据。
-- select "initBaseConfig"('test');
-- select "initBaseConfig"('ppe');
-- select "initBaseConfig"('product');
然后再执行:
select "initBaseConfig"('test');
select "initBaseConfig"('ppe');
select "initBaseConfig"('product');
配置插件
package net.w2p.local.plugins.BeanConfiguration;
import com.alibaba.fastjson.JSONObject;
import com.xxl.conf.core.XxlConfClient;
import net.w2p.Shared.common.SnowflakeIdWorker;
import net.w2p.Shared.common.ValidateUtils;
import net.w2p.WebExt.Utils.MacTools;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/***
*
* 雪花id生成器配置
* ***/
@Configuration
public class IdWorkerConfiguration {
Logger logger= LogManager.getLogger();
@Bean(name="idWorker")
@Primary
public SnowflakeIdWorker idWorker() throws Exception{
final String VarPrefix="base.";
String json_roster=XxlConfClient.get(VarPrefix+"snowflake_roster");
if(ValidateUtils.isEmpty(json_roster)){
Exception error=new Exception("必须在配置中心设置雪花id生成的各服务器编号名单!");
logger.error(error);
throw error;
}
List<String> localMacs= MacTools.getLocalMacList();
if(localMacs.size()<=0){
Exception error=new Exception("系统异常,无法获取当前服务器mac地址,请检查是服务器的网络和网卡配置!");
logger.error(error);
throw error;
}
JSONObject jsonObj=JSONObject.parseObject(json_roster);
Long workerId=null;
Long datacenterId=null;
Map<String,String> map4Roster=new HashMap<>();
for(String key:jsonObj.keySet()){
map4Roster.put(key.trim().toLowerCase(),jsonObj.getString(key));
}
for(String macId:localMacs){
String ref_mac=macId.toLowerCase().trim();
if(map4Roster.containsKey(ref_mac)){
String combineStr=map4Roster.get(ref_mac);
if(ValidateUtils.isEmpty(combineStr)){
continue;
}
String[] numbers=combineStr.split(",");
if(numbers.length<2){
continue;
}
workerId=Long.parseLong(numbers[0]);
datacenterId=Long.parseLong(numbers[1]);
if(workerId>31||datacenterId>31||workerId<0||datacenterId<0){
Exception error=new Exception("工作id或数据中心id异常,只能在0到31之间!");
logger.error(error);
throw error;
}
}
else{
continue;
}
}
if(workerId==null||datacenterId==null){
Exception error=new Exception("抱歉,当前服务器mac地址尚未注册,请在配置中心进行注册。");
logger.error(error);
throw error;
}
SnowflakeIdWorker idWorker=new SnowflakeIdWorker(workerId,datacenterId);
return idWorker;
}
}
写测试代码
@Autowired
SnowflakeIdWorker idWorker;
@Test
public void testIdWorker(){
Long nowId=idWorker.nextId();
System.out.println("生成了id:"+nowId);
}
测试结果:
成功获得下一个id。
对外发布服务
添加api项目
如下图:
代码有:
package net.w2p.BaseServiceApp.service;
public interface IdWorkerBiz {
public Long nextId();
}
app项目中实现接口配置服务发布
引入api项目依赖
接口实现
代码为:
package net.w2p.BaseServiceApp.service;
import net.w2p.Shared.common.SnowflakeIdWorker;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
@Service
@Qualifier("idGeneratorImpl")
public class IdWorkerBizImpl implements IdWorkerBiz {
@Autowired
SnowflakeIdWorker idWorker;
@Override
public Long nextId() {
return idWorker.nextId();
}
}
同时,记得在applicationContext.xml中注册要扫描这个包:
配置zk服务器信息
package net.w2p.local.plugins.BeanConfiguration;
import com.xxl.conf.core.XxlConfClient;
import net.w2p.WebExt.config.ZkConf;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ZkConfiguration {
@Bean(name="zkConf")
public ZkConf zkConf(){
ZkConf conf=new ZkConf();
return conf;
}
}
在配置中心配置rpc provider的端口
-- 执行以下脚本,自动生成base-serverice的provider配置项
select "initRpcProviderConfig"('test','file-server');
select "initRpcProviderConfig"('ppe','file-server');
select "initRpcProviderConfig"('product','file-server');
注意,手动修改端口,因为这个配置已经在FileServerWebApp中做过了,同一台机器上面同时开两个服务肯定端口冲突的,譬如,换成3:
配置rpc服务提供者信息并发布该服务
package net.w2p.local.plugins.BeanConfiguration;
import com.xxl.conf.core.XxlConfClient;
import net.w2p.BaseServiceApp.service.IdWorkerBiz;
import net.w2p.BaseServiceApp.service.IdWorkerBizImpl;
import net.w2p.WebExt.Plugins.SofaRpc.ZookeeperBoltServer;
import net.w2p.WebExt.config.SofaProviderConf;
import net.w2p.WebExt.config.ZkConf;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/***
* sofa rpc的服务提供者配置
*
* ***/
@Configuration
public class RpcProviderConfiguration {
@Bean(name="providerConf")
public SofaProviderConf providerConf(){
final String VarPrefix ="base-service.rpc_provider.";
SofaProviderConf conf=new SofaProviderConf();
conf.port= XxlConfClient.getInt(VarPrefix+"port");
return conf;
}
@Bean(name="rpcProvider")
@Autowired
public ZookeeperBoltServer rpcProvider(@Qualifier("zkConf")ZkConf zkConf, @Qualifier("providerConf")SofaProviderConf providerConf, @Qualifier("idGeneratorImpl") IdWorkerBizImpl idGenerator){
ZookeeperBoltServer provider=new ZookeeperBoltServer(zkConf.address,providerConf);
//--注意,配置要用export来导出远程接口。
//provider.export()
//--发布第一个demo性质的服务接口
// provider.export(IHelloService.class,new HelloServiceImpl());
provider.export(IdWorkerBiz.class,idGenerator);
return provider;
}
}
在MasterWebApp中配置引用该服务
build.gradle上面添加引用:
添加zk配置以及rpc的客户端
package net.w2p.local.plugins.BeanConfiguration;
import com.xxl.conf.core.XxlConfClient;
import net.w2p.WebExt.config.ZkConf;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ZkConfiguration {
@Bean(name="zkConf")
public ZkConf zkConf(){
ZkConf conf=new ZkConf();
return conf;
}
}
客户端配置:
package net.w2p.local.plugins.BeanConfiguration;
import net.w2p.BaseServiceApp.service.IdWorkerBiz;
import net.w2p.FileServerWebApp.Rpc.IHelloService;
import net.w2p.WebExt.Plugins.SofaRpc.ZookeeperBoltClient;
import net.w2p.WebExt.config.SofaCostumerConf;
import net.w2p.WebExt.config.ZkConf;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/****这是sofarpc的客户端的配置***/
@Configuration
public class RpcConsumerConfiguration {
@Bean(name="rpcConsumer")
@Autowired
public ZookeeperBoltClient rpcConsumer(@Qualifier("zkConf") ZkConf zkConf){
ZookeeperBoltClient consumer=new ZookeeperBoltClient(zkConf.address,new SofaCostumerConf());
//--注意,配置要用import来导入接口
//provider.export()
consumer.importInterface(IHelloService.class);
consumer.importInterface(IdWorkerBiz.class);
return consumer;
}
@Bean("idGenerator")
@Autowired
public IdWorkerBiz idGenerator(@Qualifier("rpcConsumer")ZookeeperBoltClient consumer){
return consumer.refer(IdWorkerBiz.class);
}
}
测试
首先,启动BaseServiceApp,毕竟服务提供者不启动客户端怎么连接:
添加测试代码:
@Autowired
IdWorkerBiz idGenerator;
@Test
public void testNextId(){
System.out.println("生成的id是:"+idGenerator.nextId());
}
到这里已经很明显了。。将IdWorkerBiz这个sofarpc的对象都托管到spring了,一个autowired就直接引用,方便程度是真的高。
测试结果:
id生成成功。。
结语
雪花算法生成id这个服务是基础啊。。
是分布式系统还有,分布式数据库,分表分库的基础啊。。必须能够整合出来的。