├── .gitignore ├── src └── main │ ├── java │ └── cn │ │ └── aoho │ │ └── generator │ │ ├── service │ │ ├── SnowflakeId.java │ │ ├── IdConverter.java │ │ ├── IdService.java │ │ └── impl │ │ │ ├── IdConverterImpl.java │ │ │ ├── SnowflakeIdWorker.java │ │ │ └── IdServiceImpl.java │ │ ├── entity │ │ ├── IdEntity.java │ │ ├── AssembleID.java │ │ └── IdMeta.java │ │ ├── config │ │ ├── JerseyConfig.java │ │ └── ServiceConfig.java │ │ ├── GeneratorApplication.java │ │ └── rest │ │ └── IdResource.java │ └── resources │ ├── application.yml │ └── bootstrap.yml ├── Dockerfile ├── pom.xml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /*.iml 3 | /target 4 | /logs 5 | .DS_Store 6 | /yunpian_error.* 7 | /yunpian_send.* 8 | -------------------------------------------------------------------------------- /src/main/java/cn/aoho/generator/service/SnowflakeId.java: -------------------------------------------------------------------------------- 1 | package cn.aoho.generator.service; 2 | 3 | /** 4 | * Created by keets on 2017/9/9. 5 | */ 6 | public interface SnowflakeId { 7 | long nextId(); 8 | } 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM 192.168.1.202/library/basejava 2 | VOLUME /tmp 3 | ADD ./target/snowflake-id-generate-1.0-SNAPSHOT.jar app.jar 4 | RUN bash -c 'touch /app.jar' 5 | ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"] -------------------------------------------------------------------------------- /src/main/java/cn/aoho/generator/service/IdConverter.java: -------------------------------------------------------------------------------- 1 | package cn.aoho.generator.service; 2 | 3 | 4 | import cn.aoho.generator.entity.IdEntity; 5 | 6 | /** 7 | * Created by keets on 2017/9/9. 8 | */ 9 | public interface IdConverter { 10 | long convert(IdEntity id); 11 | 12 | IdEntity convert(long id); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/cn/aoho/generator/service/IdService.java: -------------------------------------------------------------------------------- 1 | package cn.aoho.generator.service; 2 | 3 | 4 | import java.util.Date; 5 | 6 | /** 7 | * Created by keets on 2017/9/9. 8 | */ 9 | public interface IdService { 10 | long genId(); 11 | 12 | Date transTime(long time); 13 | 14 | long makeId(long time, long seq); 15 | 16 | long makeId(long time, long seq, long machine); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/cn/aoho/generator/entity/IdEntity.java: -------------------------------------------------------------------------------- 1 | package cn.aoho.generator.entity; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.io.Serializable; 8 | 9 | /** 10 | * Created by keets on 2017/9/9. 11 | */ 12 | 13 | @Data 14 | @NoArgsConstructor 15 | @AllArgsConstructor 16 | public class IdEntity implements Serializable { 17 | private long timeStamp; 18 | private long worker; 19 | private long sequence; 20 | } 21 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | ####################### swagger starter info ########################### 2 | swagger: 3 | enabled: true 4 | title: spring-boot-id-generate 5 | config-id: id-generate 6 | version: v2 7 | license: Apache License, Version 2.0 8 | licenseUrl: https://www.apache.org/licenses/LICENSE-2.0.html 9 | termsOfServiceUrl: https://git.oschina.net/keets/snowflake-id-generator 10 | contact: aoho002@gmail.com 11 | base-path: /** 12 | resource-package: cn.aoho.generator.rest 13 | 14 | 15 | ####################### generate info ########################### 16 | generate: 17 | origin: false #是否启用原生的snowflake 18 | worker: 1001 -------------------------------------------------------------------------------- /src/main/java/cn/aoho/generator/config/JerseyConfig.java: -------------------------------------------------------------------------------- 1 | package cn.aoho.generator.config; 2 | 3 | import cn.aoho.generator.rest.IdResource; 4 | import io.swagger.jaxrs.listing.ApiListingResource; 5 | import io.swagger.jaxrs.listing.SwaggerSerializers; 6 | import org.glassfish.jersey.server.ResourceConfig; 7 | import org.springframework.stereotype.Component; 8 | 9 | import javax.annotation.PostConstruct; 10 | 11 | /** 12 | * Created by keets on 2017/9/9. 13 | */ 14 | @Component 15 | public class JerseyConfig extends ResourceConfig { 16 | 17 | public JerseyConfig() { 18 | register(IdResource.class); 19 | // External 20 | } 21 | 22 | @PostConstruct 23 | public void init() { 24 | this.register(ApiListingResource.class, SwaggerSerializers.class); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/cn/aoho/generator/GeneratorApplication.java: -------------------------------------------------------------------------------- 1 | package cn.aoho.generator; 2 | 3 | import cn.keets.swagger.EnableSwagger2Doc; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient; 8 | import org.springframework.cloud.netflix.feign.EnableFeignClients; 9 | 10 | @SpringBootApplication 11 | @Slf4j 12 | @EnableFeignClients 13 | @EnableDiscoveryClient 14 | @EnableSwagger2Doc 15 | public class GeneratorApplication { 16 | 17 | public static void main(String[] args) { 18 | log.info("start execute GeneratorApplication....\n"); 19 | SpringApplication.run(GeneratorApplication.class, args); 20 | log.info("end execute GeneratorApplication....\n"); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/cn/aoho/generator/entity/AssembleID.java: -------------------------------------------------------------------------------- 1 | package cn.aoho.generator.entity; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import io.swagger.annotations.ApiModel; 5 | import io.swagger.annotations.ApiModelProperty; 6 | import lombok.Data; 7 | 8 | import javax.validation.constraints.Max; 9 | import javax.validation.constraints.Min; 10 | 11 | /** 12 | * Created by keets on 2017/8/28. 13 | */ 14 | @ApiModel(value = "生成ID所需的参数") 15 | @Data 16 | public class AssembleID { 17 | @Max(1023) 18 | @Min(0) 19 | @ApiModelProperty(value = "机器ID") 20 | @JsonProperty("worker") 21 | private long machine = -1; 22 | 23 | @ApiModelProperty(value = "时间戳", required = true) 24 | @JsonProperty("timeStamp") 25 | private long time = -1; 26 | 27 | @Max(4095) 28 | @Min(0) 29 | @ApiModelProperty(value = "序列号", required = true) 30 | @JsonProperty("sequence") 31 | private long seq = -1; 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/cn/aoho/generator/service/impl/IdConverterImpl.java: -------------------------------------------------------------------------------- 1 | package cn.aoho.generator.service.impl; 2 | 3 | import cn.aoho.generator.entity.IdEntity; 4 | import cn.aoho.generator.entity.IdMeta; 5 | import cn.aoho.generator.service.IdConverter; 6 | 7 | /** 8 | * Created by keets on 2017/9/9. 9 | */ 10 | public class IdConverterImpl implements IdConverter { 11 | public long convert(IdEntity id) { 12 | long ret = 0; 13 | 14 | ret |= id.getSequence(); 15 | 16 | ret |= id.getWorker() << IdMeta.SEQUENCE_BITS; 17 | 18 | ret |= id.getTimeStamp() << IdMeta.TIMESTAMP_LEFT_SHIFT_BITS; 19 | 20 | return ret; 21 | } 22 | 23 | public IdEntity convert(long id) { 24 | IdEntity ret = new IdEntity(); 25 | 26 | ret.setSequence(id & IdMeta.SEQUENCE_MASK); 27 | 28 | ret.setWorker((id >>> IdMeta.SEQUENCE_BITS) & IdMeta.ID_MASK); 29 | 30 | ret.setTimeStamp((id >>> IdMeta.TIMESTAMP_LEFT_SHIFT_BITS) & IdMeta.TIMESTAMP_MASK); 31 | 32 | return ret; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/cn/aoho/generator/entity/IdMeta.java: -------------------------------------------------------------------------------- 1 | package cn.aoho.generator.entity; 2 | 3 | /** 4 | * Created by keets on 2017/8/28. 5 | */ 6 | public class IdMeta { 7 | //开始时间截 (从2015-01-01起) 8 | public static final long START_TIME = 1420041600000L; 9 | // 机器ID所占位数 10 | public static final long ID_BITS = 10L; 11 | // 机器ID最大值1023 (此移位算法可很快计算出n位二进制数所能表示的最大十进制数) 12 | public static final long MAX_ID = ~(-1L << ID_BITS); 13 | //Sequence所占位数 14 | public static final long SEQUENCE_BITS = 12L; 15 | //机器ID偏移量12 16 | public static final long ID_SHIFT_BITS = SEQUENCE_BITS; 17 | //时间戳的偏移量12+10=22 18 | public static final long TIMESTAMP_LEFT_SHIFT_BITS = SEQUENCE_BITS + ID_BITS; 19 | // Sequence掩码4095 20 | public static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS); 21 | // 机器ID掩码1023 22 | public static final long ID_MASK = ~(-1L << ID_BITS); 23 | // 时间戳掩码2的41次方减1 24 | public static final long TIMESTAMP_MASK = ~(-1L << 41L); 25 | 26 | /** 27 | * 构造方法 28 | */ 29 | private IdMeta() { 30 | //构造方法 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/resources/bootstrap.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: ${HOST_ADDRESS:aoho}-generator-${spring.profiles.active:default} 4 | 5 | --- 6 | server: 7 | port: 9090 8 | 9 | spring: 10 | profiles: default 11 | cloud: 12 | config: 13 | discovery: 14 | service-id: config-service 15 | enabled: true 16 | consul: 17 | discovery: 18 | preferIpAddress: true 19 | enabled: true 20 | register: true 21 | service-name: generator 22 | health-check-interval: 10s 23 | health-check-timeout: 20s 24 | heartbeat: 25 | enabled: true 26 | ip-address: ${HOST_ADDRESS:localhost} 27 | port: ${SERVER_PORT:${server.port}} 28 | lifecycle: 29 | enabled: true 30 | scheme: http 31 | prefer-agent-address: false 32 | register-health-check: true 33 | tags: master 34 | retry: 35 | initial-interval: 1000 36 | max-attempts: 4 37 | max-interval: 1000 38 | host: ${CONSUL_ADDRESS:192.168.1.200} 39 | port: ${CONSUL_PORT:8500} -------------------------------------------------------------------------------- /src/main/java/cn/aoho/generator/config/ServiceConfig.java: -------------------------------------------------------------------------------- 1 | package cn.aoho.generator.config; 2 | 3 | import cn.aoho.generator.service.IdConverter; 4 | import cn.aoho.generator.service.IdService; 5 | import cn.aoho.generator.service.SnowflakeId; 6 | import cn.aoho.generator.service.impl.IdConverterImpl; 7 | import cn.aoho.generator.service.impl.IdServiceImpl; 8 | import cn.aoho.generator.service.impl.SnowflakeIdWorker; 9 | import com.ecwid.consul.v1.ConsulClient; 10 | import org.springframework.beans.factory.annotation.Value; 11 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 12 | import org.springframework.context.annotation.Bean; 13 | import org.springframework.context.annotation.Configuration; 14 | import org.springframework.core.annotation.Order; 15 | 16 | /** 17 | * Created by keets on 2017/9/9. 18 | */ 19 | @Configuration 20 | public class ServiceConfig { 21 | 22 | @Value("${generate.worker:11}") 23 | private long workId; 24 | 25 | @Value("${generate.dateCenterId:10}") 26 | private long dateCenterId; 27 | 28 | @Value("${spring.cloud.consul.host:localhost}") 29 | private String host; 30 | 31 | @Value("${spring.cloud.consul.port:8500}") 32 | private int port; 33 | 34 | @Bean 35 | @Order(1) 36 | public ConsulClient consulClient() { 37 | return new ConsulClient(host, port); 38 | } 39 | 40 | @Bean 41 | public IdService idService() { 42 | return new IdServiceImpl(workId, consulClient()); 43 | } 44 | 45 | @Bean 46 | public IdConverter idConverter() { 47 | return new IdConverterImpl(); 48 | } 49 | 50 | @Bean 51 | @ConditionalOnProperty(value = "generate.origin") 52 | public SnowflakeId snowflakeId() { 53 | return new SnowflakeIdWorker(workId, dateCenterId); 54 | } 55 | 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/cn/aoho/generator/rest/IdResource.java: -------------------------------------------------------------------------------- 1 | package cn.aoho.generator.rest; 2 | 3 | import cn.aoho.generator.entity.AssembleID; 4 | import cn.aoho.generator.entity.IdEntity; 5 | import cn.aoho.generator.service.IdConverter; 6 | import cn.aoho.generator.service.IdService; 7 | import io.swagger.annotations.Api; 8 | import io.swagger.annotations.ApiOperation; 9 | import io.swagger.annotations.ApiParam; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.apache.commons.lang3.time.DateFormatUtils; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | 14 | import javax.ws.rs.Consumes; 15 | import javax.ws.rs.GET; 16 | import javax.ws.rs.POST; 17 | import javax.ws.rs.Path; 18 | import javax.ws.rs.PathParam; 19 | import javax.ws.rs.Produces; 20 | import javax.ws.rs.core.MediaType; 21 | import javax.ws.rs.core.Response; 22 | 23 | /** 24 | * Created by keets on 2017/9/9. 25 | */ 26 | 27 | @Api(value = "/") 28 | @Slf4j 29 | @Path(value = "/api") 30 | public class IdResource { 31 | 32 | @Autowired 33 | private IdService idService; 34 | 35 | @Autowired 36 | private IdConverter idConverter; 37 | 38 | 39 | @Path(value = "/id") 40 | @GET 41 | @ApiOperation(value = "生成ID", httpMethod = "GET", 42 | notes = "成功返回ID", 43 | response = long.class 44 | ) 45 | public Response genId() { 46 | return Response.ok().entity(idService.genId()).build(); 47 | } 48 | 49 | @Path("/id/{id:[0-9]*}") 50 | @GET 51 | @Produces(MediaType.APPLICATION_JSON) 52 | @ApiOperation(value = "对ID进行解析", httpMethod = "GET", 53 | notes = "成功返回解析后的ID(json格式)", 54 | response = IdEntity.class 55 | ) 56 | public Response explainId(@ApiParam(value = "要解析的ID", required = true) @PathParam("id") long id) { 57 | log.info("id is {}", id); 58 | return Response.ok().entity(idConverter.convert(id)).build(); 59 | } 60 | 61 | @Path("/time/{time:[0-9]*}") 62 | @GET 63 | @ApiOperation(value = "对时间戳进行解析", httpMethod = "GET", 64 | notes = "成功返回yyyy-MM-dd HH:mm:ss格式的日期时间", 65 | response = String.class 66 | ) 67 | public Response transTime(@ApiParam(value = "要解析的时间戳", required = true) @PathParam("time") long time) { 68 | log.info("time is {}", time); 69 | return Response.ok().entity(DateFormatUtils.format(idService.transTime(time), "yyyy-MM-dd HH:mm:ss")).build(); 70 | } 71 | 72 | @Path("/id") 73 | @POST 74 | @Consumes(MediaType.APPLICATION_JSON) 75 | @ApiOperation(value = "传入相应参数生成ID", httpMethod = "POST", 76 | notes = "成功返回ID", 77 | response = long.class 78 | ) 79 | public Response makeId(@ApiParam(value = "传入的相应参数(json格式)", required = true) AssembleID makeID) { 80 | long worker = makeID.getMachine(); 81 | long time = makeID.getTime(); 82 | long sequence = makeID.getSeq(); 83 | log.info("worker is {}", worker); 84 | log.info("time is {}", time); 85 | log.info("sequence is {}", sequence); 86 | 87 | if (time == -1 || sequence == -1) { 88 | throw new IllegalArgumentException("Both time and sequence are required."); 89 | } 90 | 91 | long ret = worker == -1 ? idService.makeId(time, sequence) : idService.makeId(time, worker, sequence); 92 | return Response.ok().entity(ret).build(); 93 | } 94 | } -------------------------------------------------------------------------------- /src/main/java/cn/aoho/generator/service/impl/SnowflakeIdWorker.java: -------------------------------------------------------------------------------- 1 | package cn.aoho.generator.service.impl; 2 | 3 | import cn.aoho.generator.service.SnowflakeId; 4 | import lombok.extern.slf4j.Slf4j; 5 | 6 | /** 7 | *

8 | * Snowflake算法是带有时间戳的全局唯一ID生成算法。它有一套固定的ID格式,如下: 9 | *

10 | * 41位的时间序列(精确到毫秒,41位的长度可以使用69年) 11 | * 10位的机器标识(10位的长度最多支持部署1024个节点) 12 | * 12位的Sequence序列号(12位的Sequence序列号支持每个节点每毫秒产生4096个ID序号) 13 | *

14 | * 结构如下(每部分用-分开):
15 | * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000
16 | * 优点是:整体上按照时间自增排序,且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分) 17 | */ 18 | @Slf4j 19 | public class SnowflakeIdWorker implements SnowflakeId { 20 | //开始时间截 (从2015-01-01起) 21 | private static final long START_TIME = 1420041600000L; 22 | // 机器ID所占位数 23 | private static final long ID_BITS = 5L; 24 | //数据中心ID所占位数 25 | private static final long DATA_CENTER_ID_BITS = 5L; 26 | // 机器ID最大值31 (此移位算法可很快计算出n位二进制数所能表示的最大十进制数) 27 | private static final long MAX_ID = ~(-1L << ID_BITS); 28 | // 数据中心ID最大值31 29 | private static final long MAX_DATA_CENTER_ID = ~(-1L << DATA_CENTER_ID_BITS); 30 | //Sequence所占位数 31 | private static final long SEQUENCE_BITS = 12L; 32 | //机器ID偏移量12 33 | private static final long ID_SHIFT_BITS = SEQUENCE_BITS; 34 | //数据中心ID偏移量12+5=17 35 | private static final long DATA_CENTER_ID_SHIFT_BITS = SEQUENCE_BITS + ID_BITS; 36 | //时间戳的偏移量12+5+5=22 37 | private static final long TIMESTAMP_LEFT_SHIFT_BITS = SEQUENCE_BITS + ID_BITS + DATA_CENTER_ID_BITS; 38 | // Sequence掩码4095 39 | private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS); 40 | // 上一毫秒数 41 | private static long lastTimestamp = -1L; 42 | //毫秒内Sequence(0~4095) 43 | private static long sequence = 0L; 44 | //机器ID(0-31) 45 | private final long workerId; 46 | //数据中心ID(0-31) 47 | private final long dataCenterId; 48 | 49 | /** 50 | * 构造 51 | * 52 | * @param workerId 机器ID(0-31) 53 | * @param dataCenterId 数据中心ID(0-31) 54 | */ 55 | public SnowflakeIdWorker(long workerId, long dataCenterId) { 56 | if (workerId > MAX_ID || workerId < 0) { 57 | throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", MAX_ID)); 58 | } 59 | if (dataCenterId > MAX_DATA_CENTER_ID || dataCenterId < 0) { 60 | throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", MAX_DATA_CENTER_ID)); 61 | } 62 | this.workerId = workerId; 63 | this.dataCenterId = dataCenterId; 64 | log.info(String.format("worker starting. timestamp left shift %d, datacenter id bits %d, worker id bits %d, sequence bits %d, workerid %d", TIMESTAMP_LEFT_SHIFT_BITS, DATA_CENTER_ID_BITS, ID_BITS, SEQUENCE_BITS, workerId)); 65 | } 66 | 67 | /** 68 | * 生成ID(线程安全) 69 | * 70 | * @return id 71 | */ 72 | @Override 73 | public synchronized long nextId() { 74 | long timestamp = timeGen(); 75 | 76 | //如果当前时间小于上一次ID生成的时间戳,说明系统时钟被修改过,回退在上一次ID生成时间之前应当抛出异常!!! 77 | if (timestamp < lastTimestamp) { 78 | log.error(String.format("clock is moving backwards. Rejecting requests until %d.", lastTimestamp)); 79 | throw new IllegalStateException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp)); 80 | } 81 | 82 | //如果是同一时间生成的,则进行毫秒内sequence生成 83 | if (lastTimestamp == timestamp) { 84 | sequence = (sequence + 1) & SEQUENCE_MASK; 85 | //溢出处理 86 | if (sequence == 0) {//阻塞到下一毫秒,获得新时间戳 87 | timestamp = tilNextMillis(lastTimestamp); 88 | } 89 | } else {//时间戳改变,毫秒内sequence重置 90 | sequence = 0L; 91 | } 92 | //上次生成ID时间截 93 | lastTimestamp = timestamp; 94 | 95 | //移位并通过或运算组成64位ID 96 | return ((timestamp - START_TIME) << TIMESTAMP_LEFT_SHIFT_BITS) | (dataCenterId << DATA_CENTER_ID_SHIFT_BITS) | (workerId << ID_SHIFT_BITS) | sequence; 97 | } 98 | 99 | /** 100 | * 阻塞到下一毫秒,获得新时间戳 101 | * 102 | * @param lastTimestamp 上次生成ID时间截 103 | * @return 当前时间戳 104 | */ 105 | private long tilNextMillis(long lastTimestamp) { 106 | long timestamp = timeGen(); 107 | while (timestamp <= lastTimestamp) { 108 | timestamp = timeGen(); 109 | } 110 | return timestamp; 111 | } 112 | 113 | /** 114 | * 获取以毫秒为单位的当前时间 115 | * 116 | * @return 当前时间(毫秒) 117 | */ 118 | private long timeGen() { 119 | return System.currentTimeMillis(); 120 | } 121 | } -------------------------------------------------------------------------------- /src/main/java/cn/aoho/generator/service/impl/IdServiceImpl.java: -------------------------------------------------------------------------------- 1 | package cn.aoho.generator.service.impl; 2 | 3 | import cn.aoho.generator.entity.IdEntity; 4 | import cn.aoho.generator.entity.IdMeta; 5 | import cn.aoho.generator.service.IdConverter; 6 | import cn.aoho.generator.service.IdService; 7 | import com.ecwid.consul.v1.ConsulClient; 8 | import com.ecwid.consul.v1.Response; 9 | import com.ecwid.consul.v1.kv.model.GetValue; 10 | import lombok.extern.slf4j.Slf4j; 11 | 12 | import java.util.Date; 13 | 14 | /** 15 | * Created by keets on 2017/9/9. 16 | */ 17 | 18 | @Slf4j 19 | public class IdServiceImpl implements IdService { 20 | // 上一毫秒数 21 | private static long lastTimestamp = -1L; 22 | //毫秒内Sequence(0~4095) 23 | private static long sequence = 0L; 24 | //机器ID(0-1023) 25 | private final long workerId; 26 | //各种元数据 27 | protected IdMeta idMeta; 28 | 29 | private ConsulClient consulClient; 30 | 31 | private enum Periods { 32 | START, RUNNING 33 | } 34 | 35 | /** 36 | * 构造 37 | * 38 | * @param workerId 机器ID((0-1023) 39 | */ 40 | public IdServiceImpl(long workerId, ConsulClient consulClient) { 41 | if (workerId > idMeta.MAX_ID || workerId < 0) { 42 | throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", idMeta.MAX_ID)); 43 | } 44 | this.workerId = workerId; 45 | this.consulClient = consulClient; 46 | validateStoredTimestamp(); 47 | log.info("worker starting. timestamp left shift {}, worker id bits {}, sequence bits {}, workerid {}", idMeta.TIMESTAMP_LEFT_SHIFT_BITS, idMeta.ID_BITS, idMeta.SEQUENCE_BITS, workerId); 48 | } 49 | 50 | /** 51 | * 生成ID(线程安全) 52 | * 53 | * @return id 54 | */ 55 | public synchronized long genId() { 56 | long timestamp = timeGen(); 57 | 58 | //如果当前时间小于上一次ID生成的时间戳,说明系统时钟被修改过,回退在上一次ID生成时间之前应当抛出异常!!! 59 | validateTimestamp(timestamp, lastTimestamp, Periods.RUNNING); 60 | 61 | //如果是同一时间生成的,则进行毫秒内sequence生成 62 | if (lastTimestamp == timestamp) { 63 | sequence = (sequence + 1) & IdMeta.SEQUENCE_MASK; 64 | //溢出处理 65 | if (sequence == 0) {//阻塞到下一毫秒,获得新时间戳 66 | timestamp = tilNextMillis(lastTimestamp); 67 | } 68 | } else {//时间戳改变,毫秒内sequence重置 69 | sequence = 0L; 70 | } 71 | //上次生成ID时间截 72 | lastTimestamp = timestamp; 73 | consulClient.setKVValue(String.valueOf(workerId), String.valueOf(lastTimestamp)); 74 | //移位并通过或运算组成64位ID 75 | return ((timestamp - idMeta.START_TIME) << idMeta.TIMESTAMP_LEFT_SHIFT_BITS) | (workerId << idMeta.ID_SHIFT_BITS) | sequence; 76 | } 77 | 78 | /** 79 | * 如果当前时间戳小于上一次ID生成的时间戳,说明系统时钟被修改过,回退在上一次ID生成时间之前应当抛出异常!!! 80 | * 81 | * @param lastTimestamp 上一次ID生成的时间戳 82 | * @param timestamp 当前时间戳 83 | */ 84 | private void validateTimestamp(long timestamp, long lastTimestamp, Periods period) { 85 | if (timestamp < lastTimestamp) { 86 | log.error(String.format("clock is moving backwards. Rejecting requests until %d.", lastTimestamp)); 87 | throw new IllegalStateException(String.format("Clock moved backwards in %s. Refusing to generate id for %d milliseconds", period, lastTimestamp - timestamp)); 88 | } 89 | } 90 | 91 | /** 92 | * checks for timestamp by workerId when server starts. 93 | * if server starts for the first time, just let it go and log warns. 94 | * if current timestamp is smaller than the value stored in consul server, throw exception. 95 | */ 96 | private void validateStoredTimestamp() { 97 | long current = timeGen(); 98 | Response keyValueResponse = consulClient.getKVValue(String.valueOf(workerId)); 99 | if (keyValueResponse.getValue() != null) { 100 | lastTimestamp = Long.parseLong(keyValueResponse.getValue().getDecodedValue()); 101 | validateTimestamp(current, lastTimestamp, Periods.START); 102 | } else { 103 | log.warn(String.format("clock in consul is null. Generator works as for the 1st time.")); 104 | } 105 | } 106 | 107 | /** 108 | * 阻塞到下一毫秒,获得新时间戳 109 | * 110 | * @param lastTimestamp 上次生成ID时间截 111 | * @return 当前时间戳 112 | */ 113 | private long tilNextMillis(long lastTimestamp) { 114 | long timestamp = timeGen(); 115 | while (timestamp <= lastTimestamp) { 116 | timestamp = timeGen(); 117 | } 118 | return timestamp; 119 | } 120 | 121 | /** 122 | * 获取以毫秒为单位的当前时间 123 | * 124 | * @return 当前时间(毫秒) 125 | */ 126 | private long timeGen() { 127 | return System.currentTimeMillis(); 128 | } 129 | 130 | /** 131 | * 对时间戳单独进行解析 132 | * 133 | * @param time 时间戳 134 | * @return 生成的Date时间 135 | */ 136 | public Date transTime(long time) { 137 | return new Date(time + idMeta.START_TIME); 138 | } 139 | 140 | /** 141 | * 根据时间戳和序列号生成ID 142 | * 143 | * @param timeStamp 时间戳 144 | * @param sequence 序列号 145 | * @return 生成的ID 146 | */ 147 | public long makeId(long timeStamp, long sequence) { 148 | return makeId(timeStamp, workerId, sequence); 149 | } 150 | 151 | /** 152 | * 根据时间戳、机器ID和序列号生成ID 153 | * 154 | * @param timeStamp 时间戳 155 | * @param worker 机器ID 156 | * @param sequence 序列号 157 | * @return 生成的ID 158 | */ 159 | public long makeId(long timeStamp, long worker, long sequence) { 160 | IdConverter idConverter = new IdConverterImpl(); 161 | return idConverter.convert(new IdEntity(timeStamp, worker, sequence)); 162 | } 163 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | org.springframework.boot 9 | spring-boot-starter-parent 10 | 1.5.2.RELEASE 11 | 12 | 13 | com.blueskykong 14 | snowflake-id-generate 15 | jar 16 | 1.0-SNAPSHOT 17 | id-generate 18 | 19 | 20 | 21 | The Apache Software License, Version 2.0 22 | http://www.apache.org/licenses/LICENSE-2.0.txt 23 | repo 24 | 25 | 26 | 27 | 28 | 29 | aoho 30 | aoho002@gmail.com 31 | http://blueskykong.com 32 | 33 | 34 | 35 | 36 | 0.4.14 37 | 192.168.1.202/library 38 | 39 | true 40 | UTF-8 41 | 1.8 42 | 1.0.0-SNAPSHOT 43 | 44 | 45 | 46 | 47 | 48 | 49 | org.springframework.boot 50 | spring-boot-starter-actuator 51 | 52 | 53 | org.springframework.cloud 54 | spring-cloud-starter 55 | 56 | 57 | org.springframework.cloud 58 | spring-cloud-starter-feign 59 | 60 | 61 | jsr311-api 62 | javax.ws.rs 63 | 64 | 65 | 66 | 67 | 68 | org.springframework.boot 69 | spring-boot-starter-web 70 | 71 | 72 | 73 | org.springframework.cloud 74 | spring-cloud-starter-consul-discovery 75 | 76 | 77 | jsr311-api 78 | javax.ws.rs 79 | 80 | 81 | 82 | 83 | org.springframework.boot 84 | spring-boot-starter-jersey 85 | 86 | 87 | org.springframework.boot 88 | spring-boot-starter-jetty 89 | provided 90 | 91 | 92 | 93 | 94 | com.ecwid.consul 95 | consul-api 96 | 97 | 98 | 99 | 100 | org.projectlombok 101 | lombok 102 | 103 | 104 | 105 | 106 | club.hacloud 107 | jersey-starter-swagger 108 | ${swageger.jsersey.version} 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | org.springframework.cloud 117 | spring-cloud-dependencies 118 | Dalston.RELEASE 119 | pom 120 | import 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | org.springframework.boot 129 | spring-boot-maven-plugin 130 | 131 | 132 | com.spotify 133 | docker-maven-plugin 134 | ${maven.docker.version} 135 | 136 | 137 | 138 | install 139 | 140 | build 141 | 142 | 143 | 144 | 145 | 146 | ${docker.skip.build} 147 | ${docker.image.prefix}/${project.artifactId} 148 | 149 | 150 | ${project.version} 151 | latest 152 | 153 | true 154 | 155 | 156 | 157 | Asia/Shanghai 158 | 159 | 160 | ln -snf /usr/share/zoneinfo/$TZ /etc/localtime 161 | echo $TZ > /etc/timezone 162 | wget https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh 163 | chmod 777 wait-for-it.sh 164 | 165 | 166 | ${project.basedir} 167 | 168 | 169 | / 170 | ${project.build.directory} 171 | ${project.build.finalName}.jar 172 | 173 | 174 | 175 | docker-registry 176 | 177 | 178 | 179 | 180 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # snowflake升级版全局id生成 2 | 3 | ** 对 docker-maven-plugin的支持见文章 http://blueskykong.com/2017/11/02/dockermaven/ ** 4 | 5 | ## 1. 背景 6 | 分布式系统或者微服务架构基本都采用了分库分表的设计,全局唯一id生成的需求变得很迫切。 7 | 传统的单体应用,使用单库,数据库中自增id可以很方便实现。分库之后,首先需要分库键,分库键必然不能重复,所以传统的做法并不能满足需求。概括下来,那业务系统对ID号的要求有哪些呢? 8 | 9 | >1.全局唯一性:不能出现重复的ID号,既然是唯一标识,这是最基本的要求。 10 | >2.趋势递增:在MySQL InnoDB引擎中使用的是聚集索引,由于多数RDBMS使用B-tree的数据结构来存储索引数据,在主键的选择上面我们应该尽量使用有序的主键保证写入性能。 11 | 3.单调递增:保证下一个ID一定大于上一个ID,例如事务版本号、IM增量消息、排序等特殊需求。 12 | 4.信息安全:如果ID是连续的,恶意用户的扒取工作就非常容易做了,直接按照顺序下载指定URL即可;如果是订单号就更危险了,竞对可以直接知道我们一天的单量。所以在一些应用场景下,会需要ID无规则、不规则。 13 | 14 | 其中第3和第4点是互斥的。除了功能性需求,还有性能和可靠性的需求: 15 | 16 | > - 平均延迟和TP999延迟都要尽可能低; 17 | > - 可用性5个9; 18 | > - 高QPS。 19 | 20 | ## 2. 进阶历程 21 | 自从项目从单体应用拆分成微服务架构后,对全局id部分做了些摸索。 22 | ### 2.1 uuid 23 | 刚开始拆分业务,id主键都是使用uuid字符串。 24 | UUID(Universally Unique Identifier)的标准型式包含32个16进制数字,以连字号分为五段,形式为8-4-4-4-12的36个字符。类似这样的字符串:`dc5adf0a-d531-11e5-95aa-3c15c2d22392`。128位,根本不用担心不够用。生成的方法也很简单: 25 | 26 | ```java 27 | UUID userId = UUID.randomUUID(); 28 | ``` 29 | uuid全球唯一,本地生成,没有网络消耗,产生的性能绝对可以满足。 30 | 其缺点也是显而易见的,比较占地方,和INT类型相比,存储一个UUID要花费更多的空间。 31 | 使用UUID后,URL显得冗长,不够友好。ID作为主键时在特定的环境会存在一些问题,比如做DB主键的场景下,UUID就非常不适用: 32 | 33 | - MySQL官方有明确的建议主键要尽量越短越好,36个字符长度的UUID不符合要求。 34 | - 对MySQL索引不利:如果作为数据库主键,在InnoDB引擎下,UUID的无序性可能会引起数据位置频繁变动,严重影响性能。 35 | 36 | ### 2.2 数据库生成 37 | 以MySQL举例,利用给字段设置`auto_increment_increment`和`auto_increment_offset`来保证ID自增,每次业务使用下列SQL读写MySQL得到ID号。 38 | 参考了[Leaf](https://tech.meituan.com/MT_Leaf.html)的实现思想: 39 | 40 | - id server每次批量从数据库取号段,本地缓存这个号段,并且设置阈值,当达到0.8(已用与号段容量的比值),自动去获取一个新的号段,更新本地缓存的号段。 41 | - id client,即具体的调用服务实例,在本地也做一个缓存,实现和id server的缓存差不多,这样做的目的是为了减轻id服务端的压力,同时减少了rpc调用的网络消耗。 42 | 43 | 以上方案,其缺点是: 44 | 45 | 1. 号段存在浪费,无论哪个客户端还是服务端重启都会浪费号段。 46 | 2. 号段是直接自增,不够随机,对外暴露信息过多。 47 | 3. DB宕机会造成整个系统不可用。虽然在DB宕机之后,利用缓存还能进行短暂供号,但是数据库的依赖还是很重。Leaf采用的一般做法是高可用容灾: 48 | 49 | >采用一主两从的方式,同时分机房部署,Master和Slave之间采用半同步方式同步数据。同时使用DBProxy做主从切换。当然这种方案在一些情况会退化成异步模式,甚至在非常极端情况下仍然会造成数据不一致的情况,但是出现的概率非常小。 50 | 51 | ![主从](http://ovci9bs39.bkt.clouddn.com/master-slave.png "主从方式") 52 | 53 | ## 3. snowflake方案 54 | ### 3.1 介绍 55 | 考虑到上述方案的缺陷,笔者调查了其他的生成方案,snowflake就是其中一种方案。 56 | 趋势递增和不够随机的问题,在snowflake完全可以解决,Snowflake ID有64bits长,由以下三部分组成: 57 | 58 | ![snowflake](http://ovci9bs39.bkt.clouddn.com/snowflake-64bit.jpg "snowflake") 59 | 60 | 1. 第一位为0,不用。 61 | 2. timestamp—41bits,精确到ms,那就意味着其可以表示长达(2^41-1)/(1000360024*365)=139.5年,另外使用者可以自己定义一个开始纪元(epoch),然后用(当前时间-开始纪元)算出time,这表示在time这个部分在140年的时间里是不会重复的,官方文档在这里写成了41bits,应该是写错了。另外,这里用time还有一个很重要的原因,就是可以直接更具time进行排序,对于twitter这种更新频繁的应用,时间排序就显得尤为重要了。 62 | 63 | 3. machine id—10bits,该部分其实由datacenterId和workerId两部分组成,这两部分是在配置文件中指明的。 64 | 65 | - datacenterId,方便搭建多个生成uid的service,并保证uid不重复,比如在datacenter0将机器0,1,2组成了一个生成uid的service,而datacenter1此时也需要一个生成uid的service,从本中心获取uid显然是最快最方便的,那么它可以在自己中心搭建,只要保证datacenterId唯一。如果没有datacenterId,即用10bits,那么在搭建一个新的service前必须知道目前已经在用的id,否则不能保证生成的id唯一,比如搭建的两个uid service中都有machine id为100的机器,如果其server时间相同,那么产生相同id的情况不可避免。 66 | - workerId是实际server机器的代号,最大到32,同一个datacenter下的workerId是不能重复的。它会被注册到consul上,确保workerId未被其他机器占用,并将host:port值存入,注册成功后就可以对外提供服务了。 67 | 68 | 4. sequence id —12bits,该id可以表示4096个数字,它是在time相同的情况下,递增该值直到为0,即一个循环结束,此时便只能等到下一个ms到来,一般情况下4096/ms的请求是不太可能出现的,所以足够使用了。 69 | 70 | ### 3.2 实现思路 71 | snowflake方案,id服务端生成,不依赖DB,既能保证性能,且生成的id足够随机。每一毫秒,一台worker可以生成4096个id,如果超过,会阻塞到下一毫秒生成。 72 | 对于那些并发量很大的系统来说,显然是不够的, 那么这个时候就是通过datacenterId和workerId来做区分,这两个ID,分别是5bit,共10bit,最大值是1024(0-1023)个, 在这种情况下,snowflake一毫秒理论上最大能够生成的ID数量是约42W个,这是一个非常大的基数了,理论上能够满足绝大多数系统的并发量。 73 | 74 | 该方案依赖于系统时钟,需要考虑时钟回拨的问题。本地缓存上一次请求的lastTimestamp,一个线程过来获取id时,首先校验当前时间是否小于上一次ID生成的时间戳。如果小于说明系统时钟被修改过,回退在上一次ID生成时间之前应当抛出异常!如此可以解决运行中,系统时钟被修改的问题。 75 | 76 | 另一种情况是,server服务启动时,系统的时间被回拨(虽然比较极端,还是列在考虑中),这样有可能与之前生成的id冲突,全局不唯一。这边解决方法是利用项目的服务发现与注册组件consul,在consul集群存储最新的lastTimestamp,key为对应的machine-id。consul的一致性基于raft算法,并利用Gossip协议: 77 | >Consul uses a gossip protocol to manage membership and broadcast messages to the cluster. All of this is provided through the use of the Serf library. 78 | 79 | 具体的协议算法,可以参考[Gossip](https://www.consul.io/docs/internals/gossip.html)。 80 | 每次server实例启动时,实例化id生成bean的时候,会首先校验当前时间与consul集群中该worker对应的lastTimestamp大小,如果当前时间偏小,则抛出异常,服务启动失败并报警。 81 | 82 | 实例化时,进行校验: 83 | 84 | ```java 85 | public IdServiceImpl(long workerId, ConsulClient consulClient) { 86 | if (workerId > idMeta.MAX_ID || workerId < 0) { 87 | throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", idMeta.MAX_ID)); 88 | } 89 | this.workerId = workerId; 90 | this.consulClient = consulClient; 91 | validateStoredTimestamp(); 92 | log.info("worker starting. timestamp left shift {}, worker id bits {}, sequence bits {}, workerid {}", idMeta.TIMESTAMP_LEFT_SHIFT_BITS, idMeta.ID_BITS, idMeta.SEQUENCE_BITS, workerId); 93 | } 94 | ``` 95 | 96 | 校验函数: 97 | 98 | ```java 99 | /** 100 | * checks for timestamp by workerId when server starts. 101 | * if server starts for the first time, just let it go and log warns. 102 | * if current timestamp is smaller than the value stored in consul server, throw exception. 103 | */ 104 | private void validateStoredTimestamp() { 105 | long current = timeGen(); 106 | Response keyValueResponse = consulClient.getKVValue(String.valueOf(workerId)); 107 | if (keyValueResponse.getValue() != null) { 108 | lastTimestamp = Long.parseLong(keyValueResponse.getValue().getDecodedValue()); 109 | validateTimestamp(current, lastTimestamp, Periods.START); 110 | } else { 111 | log.warn(String.format("clock in consul is null. Generator works as for the 1st time.")); 112 | } 113 | } 114 | ``` 115 | 116 | validateTimestamp: 117 | 118 | ```java 119 | /** 120 | * 如果当前时间戳小于上一次ID生成的时间戳,说明系统时钟被修改过,回退在上一次ID生成时间之前应当抛出异常!!! 121 | * 122 | * @param lastTimestamp 上一次ID生成的时间戳 123 | * @param timestamp 当前时间戳 124 | */ 125 | private void validateTimestamp(long timestamp, long lastTimestamp, Periods period) { 126 | if (timestamp < lastTimestamp) { 127 | log.error(String.format("clock is moving backwards. Rejecting requests until %d.", lastTimestamp)); 128 | throw new IllegalStateException(String.format("Clock moved backwards in %s. Refusing to generate id for %d milliseconds", period, lastTimestamp - timestamp)); 129 | } 130 | } 131 | ``` 132 | 133 | 获取id方法: 134 | 135 | ```java 136 | /** 137 | * 生成ID(线程安全) 138 | * 139 | * @return id 140 | */ 141 | public synchronized long genId() { 142 | long timestamp = timeGen(); 143 | 144 | //如果当前时间小于上一次ID生成的时间戳,说明系统时钟被修改过,回退在上一次ID生成时间之前应当抛出异常!!! 145 | validateTimestamp(timestamp, lastTimestamp, Periods.RUNNING); 146 | 147 | //如果是同一时间生成的,则进行毫秒内sequence生成 148 | if (lastTimestamp == timestamp) { 149 | sequence = (sequence + 1) & IdMeta.SEQUENCE_MASK; 150 | //溢出处理 151 | if (sequence == 0) {//阻塞到下一毫秒,获得新时间戳 152 | timestamp = tilNextMillis(lastTimestamp); 153 | } 154 | } else {//时间戳改变,毫秒内sequence重置 155 | sequence = 0L; 156 | } 157 | //上次生成ID时间截 158 | lastTimestamp = timestamp; 159 | consulClient.setKVValue(String.valueOf(workerId), String.valueOf(lastTimestamp)); 160 | //移位并通过或运算组成64位ID 161 | return ((timestamp - idMeta.START_TIME) << idMeta.TIMESTAMP_LEFT_SHIFT_BITS) | (workerId << idMeta.ID_SHIFT_BITS) | sequence; 162 | } 163 | ``` 164 | 165 | **本文的源码地址: 166 | GitHub:https://github.com/keets2012/snowflake-id-generator 167 | 码云: https://gitee.com/keets/snowflake-id-generator** 168 | 169 | ### 订阅最新文章,欢迎关注我的公众号 170 | 171 | ![微信公众号](http://image.blueskykong.com/wechat-public-code.jpg) 172 | 173 | 174 | ---- 175 | 参考: 176 | 177 | 1. [www.consul.io](https://www.consul.io/docs) 178 | 2. [leaf](https://tech.meituan.com/MT_Leaf.html) 179 | 3. [Twitter的分布式自增ID算法snowflake (Java版)](http://www.cnblogs.com/relucent/p/4955340.html) 180 | --------------------------------------------------------------------------------