├── src ├── main │ ├── java │ │ └── com │ │ │ └── easy │ │ │ └── id │ │ │ ├── service │ │ │ ├── generator │ │ │ │ ├── IdGeneratorFactory.java │ │ │ │ ├── IdGenerator.java │ │ │ │ └── AbstractIdGeneratorFactory.java │ │ │ ├── segment │ │ │ │ ├── SegmentIdService.java │ │ │ │ ├── SegmentEasyIdService.java │ │ │ │ ├── SegmentIdGeneratorFactory.java │ │ │ │ ├── SegmentCachedIdGenerator.java │ │ │ │ └── SegmentIdServiceImpl.java │ │ │ ├── EasyIdService.java │ │ │ └── snowflake │ │ │ │ ├── SnowflakeEasyIdService.java │ │ │ │ ├── Snowflake.java │ │ │ │ └── SnowflakeZKHolder.java │ │ │ ├── entity │ │ │ ├── Result.java │ │ │ ├── ResultCode.java │ │ │ ├── Segment.java │ │ │ └── SegmentId.java │ │ │ ├── exception │ │ │ ├── GetNextIdException.java │ │ │ ├── SegmentNotFoundException.java │ │ │ ├── FetchSegmentFailException.java │ │ │ └── SystemClockCallbackException.java │ │ │ ├── EasyIdGeneratorApplication.java │ │ │ ├── config │ │ │ ├── Module.java │ │ │ ├── GlobalExceptionHandler.java │ │ │ ├── ModuleCondition.java │ │ │ ├── BeanConfig.java │ │ │ └── DataSourceConfig.java │ │ │ ├── web │ │ │ ├── resp │ │ │ │ └── ApiResponse.java │ │ │ ├── SnowflakeEasyIdController.java │ │ │ └── SegmentEasyIdController.java │ │ │ └── util │ │ │ └── IPUtil.java │ └── resources │ │ ├── db1.properties │ │ ├── db2.properties │ │ ├── application.yml │ │ ├── docker │ │ └── docker-compose.yml │ │ └── schema.sql └── test │ └── java │ └── com │ └── easy │ └── id │ └── service │ └── SnowflakeEasyIdServiceTest.java ├── .gitignore ├── README.md └── pom.xml /src/main/java/com/easy/id/service/generator/IdGeneratorFactory.java: -------------------------------------------------------------------------------- 1 | package com.easy.id.service.generator; 2 | 3 | public interface IdGeneratorFactory { 4 | 5 | IdGenerator getIdGenerator(String businessType); 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/easy/id/service/segment/SegmentIdService.java: -------------------------------------------------------------------------------- 1 | package com.easy.id.service.segment; 2 | 3 | import com.easy.id.entity.SegmentId; 4 | 5 | public interface SegmentIdService { 6 | 7 | SegmentId fetchNextSegmentId(String businessType); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/easy/id/service/EasyIdService.java: -------------------------------------------------------------------------------- 1 | package com.easy.id.service; 2 | 3 | import java.util.Set; 4 | 5 | public interface EasyIdService { 6 | 7 | Long getNextId(String businessType); 8 | 9 | Set getNextIdBatch(String businessType, int batchSize); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/easy/id/service/generator/IdGenerator.java: -------------------------------------------------------------------------------- 1 | package com.easy.id.service.generator; 2 | 3 | import java.util.Set; 4 | 5 | /** 6 | * id 生成器 7 | */ 8 | public interface IdGenerator { 9 | 10 | Long nextId(); 11 | 12 | Set nextIds(int patchSize); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/easy/id/entity/Result.java: -------------------------------------------------------------------------------- 1 | package com.easy.id.entity; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class Result { 7 | 8 | private ResultCode code; 9 | private Long id; 10 | 11 | public Result(ResultCode code, Long id) { 12 | this.code = code; 13 | this.id = id; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/resources/db1.properties: -------------------------------------------------------------------------------- 1 | jdbcUrl=jdbc:mysql://localhost:3307/easy_id_generator?autoReconnect=true&useUnicode=true&characterEncoding=UTF-8 2 | driverClassName=com.mysql.cj.jdbc.Driver 3 | dataSource.user=root 4 | dataSource.password=123456 5 | dataSource.cachePrepStmts=true 6 | dataSource.prepStmtCacheSize=250 7 | dataSource.prepStmtCacheSqlLimit=2048 -------------------------------------------------------------------------------- /src/main/resources/db2.properties: -------------------------------------------------------------------------------- 1 | jdbcUrl=jdbc:mysql://localhost:3308/easy_id_generator?autoReconnect=true&useUnicode=true&characterEncoding=UTF-8 2 | driverClassName=com.mysql.cj.jdbc.Driver 3 | dataSource.user=root 4 | dataSource.password=123456 5 | dataSource.cachePrepStmts=true 6 | dataSource.prepStmtCacheSize=250 7 | dataSource.prepStmtCacheSqlLimit=2048 -------------------------------------------------------------------------------- /src/main/java/com/easy/id/exception/GetNextIdException.java: -------------------------------------------------------------------------------- 1 | package com.easy.id.exception; 2 | 3 | /** 4 | * 获取下一个id异常 5 | */ 6 | public class GetNextIdException extends RuntimeException { 7 | 8 | private static final long serialVersionUID = -5582536965946613712L; 9 | 10 | public GetNextIdException(String msg) { 11 | super(msg); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/easy/id/exception/SegmentNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.easy.id.exception; 2 | 3 | /** 4 | * 号段未找到 5 | */ 6 | public class SegmentNotFoundException extends RuntimeException { 7 | 8 | private static final long serialVersionUID = 6093487904562917327L; 9 | 10 | public SegmentNotFoundException(String msg) { 11 | super(msg); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/easy/id/exception/FetchSegmentFailException.java: -------------------------------------------------------------------------------- 1 | package com.easy.id.exception; 2 | 3 | public class FetchSegmentFailException extends RuntimeException { 4 | 5 | private static final long serialVersionUID = -6997616606690545563L; 6 | 7 | public FetchSegmentFailException(String msg) { 8 | super(msg); 9 | } 10 | 11 | public FetchSegmentFailException(Throwable cause) { 12 | super(cause); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 9090 3 | easy-id-generator: 4 | snowflake: 5 | enable: true 6 | zk: 7 | connection-string: 127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183 8 | load-worker-id-from-file-when-zk-down: true # 当zk不可访问时,从本地文件中读取之前备份的workerId 9 | segment: 10 | enable: false 11 | db-list: ["db1","db2"] 12 | fetch-segment-retry-times: 3 # 从数据库获取号段失败重试次数 13 | spring: 14 | profiles: 15 | active: prod 16 | -------------------------------------------------------------------------------- /src/main/java/com/easy/id/entity/ResultCode.java: -------------------------------------------------------------------------------- 1 | package com.easy.id.entity; 2 | 3 | public enum ResultCode { 4 | /** 5 | * 号段可以正常使用 6 | */ 7 | NORMAL(1), 8 | 9 | /** 10 | * 号段可以正常使用,并且需要异步加载下个号段 11 | */ 12 | SHOULD_LOADING_NEXT_SEGMENT(2), 13 | 14 | /** 15 | * 当前号段已使用完 16 | */ 17 | OVER(3); 18 | 19 | private int code; 20 | 21 | ResultCode(int code) { 22 | this.code = code; 23 | } 24 | 25 | public int code() { 26 | return this.code; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/easy/id/EasyIdGeneratorApplication.java: -------------------------------------------------------------------------------- 1 | package com.easy.id; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 6 | 7 | @SpringBootApplication 8 | @EnableConfigurationProperties 9 | public class EasyIdGeneratorApplication { 10 | public static void main(String[] args) { 11 | SpringApplication.run(EasyIdGeneratorApplication.class, args); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/easy/id/exception/SystemClockCallbackException.java: -------------------------------------------------------------------------------- 1 | package com.easy.id.exception; 2 | 3 | /** 4 | * @author zhangbingbing 5 | * @version 1.0.0 6 | * @Description 系统时间回调异常 7 | * @createTime 2020年06月01日 8 | */ 9 | public class SystemClockCallbackException extends RuntimeException { 10 | 11 | private static final long serialVersionUID = -6264588182225994225L; 12 | 13 | public SystemClockCallbackException(String msg) { 14 | super(msg); 15 | } 16 | 17 | public SystemClockCallbackException(String msg, Throwable cause) { 18 | super(msg, cause); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/easy/id/config/Module.java: -------------------------------------------------------------------------------- 1 | package com.easy.id.config; 2 | 3 | import org.springframework.context.annotation.Conditional; 4 | 5 | import java.lang.annotation.ElementType; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.RetentionPolicy; 8 | import java.lang.annotation.Target; 9 | 10 | @Target({ElementType.TYPE, ElementType.METHOD}) 11 | @Retention(RetentionPolicy.RUNTIME) 12 | @Conditional(ModuleCondition.class) 13 | public @interface Module { 14 | 15 | /** 16 | * 前缀 17 | */ 18 | String prefix() default "easy-id-generator"; 19 | 20 | /** 21 | * 组件名称 22 | */ 23 | String[] value(); 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/easy/id/config/GlobalExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.easy.id.config; 2 | 3 | import com.easy.id.web.resp.ApiResponse; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.web.bind.annotation.ControllerAdvice; 6 | import org.springframework.web.bind.annotation.ExceptionHandler; 7 | import org.springframework.web.bind.annotation.ResponseBody; 8 | 9 | @ControllerAdvice 10 | @Slf4j 11 | public class GlobalExceptionHandler { 12 | 13 | @ExceptionHandler(Throwable.class) 14 | @ResponseBody 15 | public ApiResponse handleException(Throwable e) { 16 | log.error(e.getMessage(), e); 17 | return ApiResponse.exception(e); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/test/java/com/easy/id/service/SnowflakeEasyIdServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.easy.id.service; 2 | 3 | import com.easy.id.service.snowflake.SnowflakeEasyIdService; 4 | 5 | import java.util.Set; 6 | 7 | /** 8 | * @author zhangbingbing 9 | * @version 1.0.0 10 | * @Description SnowflakeEasyIdService测试用例 11 | * @createTime 2020年06月01日 12 | */ 13 | class SnowflakeEasyIdServiceTest { 14 | 15 | public static void main(String[] args) { 16 | SnowflakeEasyIdService service = new SnowflakeEasyIdService(); 17 | int batchSize = 1000; 18 | Set nextIdBatch = service.getNextIdBatch("", batchSize); 19 | System.out.println(batchSize == nextIdBatch.size()); 20 | } 21 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | target 25 | .idea 26 | *.iml 27 | src/main/resources/docker/db1/**/ 28 | src/main/resources/docker/db2/**/ 29 | src/main/resources/docker/easy_id_generator_z1/ 30 | src/main/resources/application-prod.yml 31 | src/main/resources/db1-prod.properties 32 | src/main/resources/db2-prod.properties 33 | src/.DS_Store 34 | .DS_Store 35 | -------------------------------------------------------------------------------- /src/main/resources/docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | services: 3 | easy_id_generator_db1: 4 | image: 'mysql:5.7' 5 | container_name: 'easy_id_generator_db1' 6 | environment: 7 | - 'MYSQL_ROOT_PASSWORD=123456' 8 | - 'MYSQL_DATABASE=easy-id-generator' 9 | - 'TZ=UTC' 10 | restart: 'always' 11 | ports: 12 | - '3307:3306' 13 | volumes: 14 | - './db1/mysql:/etc/mysql/conf.d' 15 | - './db1/mysql/data:/var/lib/mysql' 16 | easy_id_generator_db2: 17 | image: 'mysql:5.7' 18 | container_name: 'easy_id_generator_db2' 19 | environment: 20 | - 'MYSQL_ROOT_PASSWORD=123456' 21 | - 'MYSQL_DATABASE=easy-id-generator' 22 | - 'TZ=UTC' 23 | restart: 'always' 24 | ports: 25 | - '3308:3306' 26 | volumes: 27 | - './db2/mysql:/etc/mysql/conf.d' 28 | - './db2/mysql/data:/var/lib/mysql' -------------------------------------------------------------------------------- /src/main/java/com/easy/id/entity/Segment.java: -------------------------------------------------------------------------------- 1 | package com.easy.id.entity; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * 假设有10台机器,那么他们的increment应该都为10,mod分别为0-9或者1-10 7 | */ 8 | @Data 9 | public class Segment { 10 | 11 | /** 12 | * 主键id 13 | */ 14 | private Long id; 15 | 16 | /** 17 | * 数据库乐观锁 18 | */ 19 | private Long version; 20 | 21 | /** 22 | * 业务类型 23 | */ 24 | private String businessType; 25 | 26 | /** 27 | * 当前号段最大的ID 28 | */ 29 | private Long maxId; 30 | 31 | /** 32 | * 步长:号段长度 33 | */ 34 | private Integer step; 35 | 36 | /** 37 | * 自增量 38 | */ 39 | private Integer increment; 40 | 41 | /** 42 | * 模数 43 | */ 44 | private Integer remainder; 45 | 46 | /** 47 | * 创建时间 48 | */ 49 | private Long createdAt; 50 | 51 | /** 52 | * 更新时间 53 | */ 54 | private Long updatedAt; 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/easy/id/service/segment/SegmentEasyIdService.java: -------------------------------------------------------------------------------- 1 | package com.easy.id.service.segment; 2 | 3 | import com.easy.id.config.Module; 4 | import com.easy.id.service.EasyIdService; 5 | import com.easy.id.service.generator.IdGeneratorFactory; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.stereotype.Service; 8 | 9 | import java.util.Set; 10 | 11 | @Service 12 | @Module(value = "segment.enable") 13 | public class SegmentEasyIdService implements EasyIdService { 14 | 15 | @Autowired 16 | private IdGeneratorFactory idGeneratorFactory; 17 | 18 | @Override 19 | public Long getNextId(String businessType) { 20 | return idGeneratorFactory.getIdGenerator(businessType).nextId(); 21 | } 22 | 23 | @Override 24 | public Set getNextIdBatch(String businessType, int batchSize) { 25 | return idGeneratorFactory.getIdGenerator(businessType).nextIds(batchSize); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/easy/id/service/segment/SegmentIdGeneratorFactory.java: -------------------------------------------------------------------------------- 1 | package com.easy.id.service.segment; 2 | 3 | import com.easy.id.config.Module; 4 | import com.easy.id.service.generator.AbstractIdGeneratorFactory; 5 | import com.easy.id.service.generator.IdGenerator; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.beans.factory.annotation.Qualifier; 8 | import org.springframework.stereotype.Component; 9 | 10 | import java.util.concurrent.ExecutorService; 11 | 12 | /** 13 | * 生成IdGenerator的工厂 14 | */ 15 | @Component 16 | @Module("segment.enable") 17 | public class SegmentIdGeneratorFactory extends AbstractIdGeneratorFactory { 18 | 19 | @Autowired 20 | private SegmentIdService segmentIdService; 21 | 22 | @Autowired 23 | @Qualifier(value = "fetchNextSegmentExecutor") 24 | private ExecutorService fetchNextSegmentExecutor; 25 | 26 | @Override 27 | protected IdGenerator createIdGenerator(String businessType) { 28 | return new SegmentCachedIdGenerator(fetchNextSegmentExecutor, segmentIdService, businessType); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EasyIdGenerator 生成全局唯一id 2 | 3 | - 递增性 4 | 5 | 插入数据库能够保证数据顺序写入,不会页分裂,磁盘利用率下降 6 | - 安全 7 | 8 | id不能包含敏感信息,不能被暴露递增规律 9 | 10 | ## 支持两种方式 11 | 12 | - 号段方式:利用mysql的自增功能 13 | 14 | ``` 15 | easy-id-generator: 16 | segment: 17 | enable: false/true # 关闭/开启 mysql自增功能 18 | db-list: ["db1","db2"] # 数据库配置:可以支持多个库,数据库配置文件名字按dbXXX格式,eg:db1,db2,db3.... 19 | fetch-segment-retry-times: 3 # 从数据库获取号段失败重试次数 20 | ``` 21 | 22 | - 雪花算法:workId是通过zk生产的 23 | 24 | ``` 25 | easy-id-generator: 26 | snowflake: 27 | enable: false/true # 关闭/开启雪花算法生成id 28 | zk: 29 | connection-string: 127.0.0.1:2181 # ip:port,ip2:prort zk链接信息 30 | load-worker-id-from-file-when-zk-down: true # 当zk不可访问时,从本地文件中读取之前备份的workerId 31 | ``` 32 | 33 | ## 环境配置 34 | 35 | - 下载docker 36 | - cmd命令行,cd到docker-compose.yml所在目录,执行docker-compose up -d 37 | 38 | ## 号段方式 39 | 40 | - 在数据库执行schema.sql脚本 41 | - 通过SegmentEasyIdController控制器,获取id 42 | 43 | ## 雪花算法方式 44 | 45 | - 通过SnowflakeEasyIdController控制器,获取id 46 | 47 | ## easy-id-generator-spring-boot-starter 48 | 49 | 在starter分支 50 | 51 | ## 作者邮箱 52 | 53 | - zbbpoplar@163.com 54 | -------------------------------------------------------------------------------- /src/main/java/com/easy/id/service/generator/AbstractIdGeneratorFactory.java: -------------------------------------------------------------------------------- 1 | package com.easy.id.service.generator; 2 | 3 | import java.util.Map; 4 | import java.util.concurrent.ConcurrentHashMap; 5 | 6 | public abstract class AbstractIdGeneratorFactory implements IdGeneratorFactory { 7 | 8 | private final Map idGeneratorMap = new ConcurrentHashMap<>(); 9 | 10 | /** 11 | * 使用模版设计模式,目的时让子类复用父类获取IdGenerator的固定逻辑 12 | */ 13 | @Override 14 | public IdGenerator getIdGenerator(String businessType) { 15 | // 双重判断 16 | if (idGeneratorMap.containsKey(businessType)) { 17 | return idGeneratorMap.get(businessType); 18 | } 19 | synchronized (this) { 20 | if (idGeneratorMap.containsKey(businessType)) { 21 | return idGeneratorMap.get(businessType); 22 | } 23 | IdGenerator idGenerator = createIdGenerator(businessType); 24 | idGeneratorMap.put(businessType, idGenerator); 25 | return idGenerator; 26 | } 27 | } 28 | 29 | protected abstract IdGenerator createIdGenerator(String businessType); 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/easy/id/service/snowflake/SnowflakeEasyIdService.java: -------------------------------------------------------------------------------- 1 | package com.easy.id.service.snowflake; 2 | 3 | import com.easy.id.config.Module; 4 | import com.easy.id.service.EasyIdService; 5 | import java.util.Set; 6 | import javax.annotation.PostConstruct; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Service; 10 | 11 | /** 12 | * @author zhangbingbing 13 | * @version 1.0.0 14 | * @Description 雪花算法实现 15 | * @createTime 2020年06月01日 16 | */ 17 | @Service 18 | @Module("snowflake.enable") 19 | @Slf4j 20 | public class SnowflakeEasyIdService implements EasyIdService { 21 | 22 | @Autowired 23 | private SnowflakeZKHolder snowflakeZKHolder; 24 | private Snowflake snowflake; 25 | 26 | @PostConstruct 27 | public void init() { 28 | snowflake = new Snowflake(snowflakeZKHolder.getWorkerID()); 29 | } 30 | 31 | @Override 32 | public Long getNextId(String businessType) { 33 | return snowflake.nextId(); 34 | } 35 | 36 | @Override 37 | public Set getNextIdBatch(String businessType, int batchSize) { 38 | return snowflake.nextIds(batchSize); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/easy/id/config/ModuleCondition.java: -------------------------------------------------------------------------------- 1 | package com.easy.id.config; 2 | 3 | import org.springframework.context.annotation.Condition; 4 | import org.springframework.context.annotation.ConditionContext; 5 | import org.springframework.core.env.Environment; 6 | import org.springframework.core.type.AnnotatedTypeMetadata; 7 | import org.springframework.util.MultiValueMap; 8 | 9 | import java.util.Optional; 10 | 11 | public class ModuleCondition implements Condition { 12 | @Override 13 | public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { 14 | final MultiValueMap attributes = metadata.getAllAnnotationAttributes(Module.class.getName()); 15 | if (attributes == null) { 16 | return true; 17 | } 18 | final String prefix = Optional.ofNullable(attributes.getFirst("prefix")).map(Object::toString).orElse(""); 19 | final Environment environment = context.getEnvironment(); 20 | for (Object value : attributes.get("value")) { 21 | String[] moduleName = (String[]) value; 22 | for (String module : moduleName) { 23 | if (environment.getProperty(prefix + "." + module, boolean.class, false)) { 24 | return true; 25 | } 26 | } 27 | } 28 | return false; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/easy/id/web/resp/ApiResponse.java: -------------------------------------------------------------------------------- 1 | package com.easy.id.web.resp; 2 | 3 | import lombok.Data; 4 | import lombok.extern.slf4j.Slf4j; 5 | 6 | import java.io.Serializable; 7 | 8 | @Data 9 | @Slf4j 10 | public class ApiResponse implements Serializable { 11 | 12 | private static final long serialVersionUID = -5959424433403902244L; 13 | 14 | private String msg = "成功"; 15 | private Boolean success = Boolean.TRUE; 16 | private T data; 17 | 18 | public static ApiResponse success() { 19 | return new ApiResponse<>(); 20 | } 21 | 22 | public static ApiResponse data(T t) { 23 | ApiResponse response = new ApiResponse<>(); 24 | response.setData(t); 25 | return response; 26 | } 27 | 28 | public static ApiResponse exception(Throwable throwable) { 29 | log.error("系统错误", throwable); 30 | ApiResponse apiResponse = new ApiResponse<>(); 31 | apiResponse.setSuccess(Boolean.FALSE); 32 | String message = throwable.getMessage(); 33 | if (message == null) { 34 | Throwable cause = throwable.getCause(); 35 | message = cause.getMessage(); 36 | } 37 | if (message == null) { 38 | message = "系统发生错误"; 39 | } 40 | apiResponse.setMsg(message); 41 | return apiResponse; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/easy/id/config/BeanConfig.java: -------------------------------------------------------------------------------- 1 | package com.easy.id.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | 6 | import java.util.concurrent.*; 7 | import java.util.concurrent.atomic.AtomicInteger; 8 | 9 | /** 10 | * @author zhangbingbing 11 | * @version 1.0.0 12 | * @createTime 2020年06月02日 13 | */ 14 | @Configuration 15 | public class BeanConfig { 16 | 17 | @Bean 18 | @Module(value = "snowflake.enable") 19 | public ScheduledExecutorService updateDataToZKScheduledExecutorService() { 20 | AtomicInteger threadIncr = new AtomicInteger(0); 21 | return new ScheduledThreadPoolExecutor(2, (r) -> { 22 | int incr = threadIncr.incrementAndGet(); 23 | if (incr >= 1000) { 24 | threadIncr.set(0); 25 | incr = 1; 26 | } 27 | return new Thread(r, "upload-data-to-zk-schedule-thread" + incr); 28 | }, new ThreadPoolExecutor.CallerRunsPolicy()); 29 | } 30 | 31 | @Bean 32 | @Module(value = "segment.enable") 33 | public ExecutorService fetchNextSegmentExecutor() { 34 | AtomicInteger threadIncr = new AtomicInteger(0); 35 | return new ThreadPoolExecutor(1, 2, 5, TimeUnit.MINUTES, new SynchronousQueue<>(), r -> { 36 | int incr = threadIncr.incrementAndGet(); 37 | if (incr >= 1000) { 38 | threadIncr.set(0); 39 | incr = 1; 40 | } 41 | return new Thread(r, "fetch-next-segment-thread-" + incr); 42 | }, new ThreadPoolExecutor.CallerRunsPolicy()); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/resources/schema.sql: -------------------------------------------------------------------------------- 1 | ## 有多少个库,就在多少库中执行 2 | create database easy_id_generator default charset utf8mb4; 3 | use easy_id_generator; 4 | create table if not exists segment 5 | ( 6 | id bigint unsigned auto_increment primary key comment '自增主键', 7 | version bigint default 0 not null comment '版本号', 8 | business_type varchar(63) default '' not null comment '业务类型,唯一', 9 | max_id bigint default 0 not null comment '当前最大id', 10 | step int default 0 null comment '步长', 11 | increment int default 1 not null comment '每次id增量', 12 | remainder int default 0 not null comment '余数', 13 | created_at bigint unsigned not null comment '创建时间', 14 | updated_at bigint unsigned not null comment '更新时间', 15 | constraint uniq_business_type unique (business_type) 16 | ) charset = utf8mb4 17 | engine Innodb comment '号段表'; 18 | 19 | # db1中执行 20 | insert into easy_id_generator.segment 21 | (version, business_type, max_id, step, increment, remainder, created_at, updated_at) 22 | values (1, 'order_business', 1000, 1000, 2, 0, now(), now()); 23 | # db2中执行 24 | insert into easy_id_generator.segment 25 | (version, business_type, max_id, step, increment, remainder, created_at, updated_at) 26 | values (1, 'order_business', 1000, 1000, 2, 1, now(), now()); 27 | ## 如果有N个库,需要在每个库执行插入一条记录 28 | insert into easy_id_generator.segment 29 | (version, business_type, max_id, step, increment, remainder, created_at, updated_at) 30 | values (1, 'order_business', 1000, 1000, N, 取值为[0, N - 1], now(), now()); 31 | 32 | -- increment和remainder的关系: 当需要10个库时,increment为10,remainder的值依次为0-9 33 | -------------------------------------------------------------------------------- /src/main/java/com/easy/id/web/SnowflakeEasyIdController.java: -------------------------------------------------------------------------------- 1 | package com.easy.id.web; 2 | 3 | import com.easy.id.config.Module; 4 | import com.easy.id.service.EasyIdService; 5 | import com.easy.id.web.resp.ApiResponse; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.beans.factory.annotation.Qualifier; 8 | import org.springframework.validation.annotation.Validated; 9 | import org.springframework.web.bind.annotation.GetMapping; 10 | import org.springframework.web.bind.annotation.RequestMapping; 11 | import org.springframework.web.bind.annotation.RequestParam; 12 | import org.springframework.web.bind.annotation.RestController; 13 | 14 | import javax.validation.constraints.Max; 15 | import javax.validation.constraints.Min; 16 | import java.util.Set; 17 | import java.util.stream.Collectors; 18 | 19 | /** 20 | * @author zhangbingbing 21 | * @version 1.0.0 22 | * @createTime 2020年05月29日 23 | */ 24 | @RestController 25 | @RequestMapping("/snowflake/ids") 26 | @Validated 27 | @Module(value = "snowflake.enable") 28 | public class SnowflakeEasyIdController { 29 | 30 | @Autowired 31 | @Qualifier("snowflakeEasyIdService") 32 | private EasyIdService easyIdService; 33 | 34 | @GetMapping("/next_id") 35 | public ApiResponse getNextId() { 36 | return ApiResponse.data(easyIdService.getNextId(null).toString()); 37 | } 38 | 39 | @GetMapping("/next_id/batches") 40 | public ApiResponse> getNextId(@RequestParam(value = "batches_size", defaultValue = "100") 41 | @Min(value = 0, message = "批量生成数必须大于0") 42 | @Max(value = 1000, message = "最大支持批量生成数为1000") Integer batchSize) { 43 | return ApiResponse.data(easyIdService.getNextIdBatch(null, batchSize).stream() 44 | .map(Object::toString).collect(Collectors.toSet())); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/easy/id/util/IPUtil.java: -------------------------------------------------------------------------------- 1 | package com.easy.id.util; 2 | 3 | import java.net.Inet6Address; 4 | import java.net.InetAddress; 5 | import java.net.NetworkInterface; 6 | import java.net.SocketException; 7 | import java.util.ArrayList; 8 | import java.util.Enumeration; 9 | import java.util.List; 10 | 11 | /** 12 | * @author zhangbingbing 13 | * @version 1.0.0 14 | * @Description ip工具类 15 | * @createTime 2020年06月02日 16 | */ 17 | public enum IPUtil { 18 | ; 19 | 20 | public static String getHostAddress() throws SocketException { 21 | return getHostAddress(null).get(0); 22 | } 23 | 24 | /** 25 | * 获取已激活网卡的ip地址 26 | * 27 | * @param interfaceName 网卡地址,null则获取所有 28 | * @return List 29 | */ 30 | public static List getHostAddress(String interfaceName) throws SocketException { 31 | List ips = new ArrayList<>(5); 32 | Enumeration networkInterfaces = NetworkInterface.getNetworkInterfaces(); 33 | while (networkInterfaces.hasMoreElements()) { 34 | NetworkInterface networkInterface = networkInterfaces.nextElement(); 35 | Enumeration allAddress = networkInterface.getInetAddresses(); 36 | while (allAddress.hasMoreElements()) { 37 | InetAddress address = allAddress.nextElement(); 38 | if (address.isLoopbackAddress()) { 39 | continue; 40 | } 41 | if (address instanceof Inet6Address) { 42 | continue; 43 | } 44 | String hostAddress = address.getHostAddress(); 45 | if (null == interfaceName) { 46 | ips.add(hostAddress); 47 | } else if (interfaceName.equals(networkInterface.getDisplayName())) { 48 | ips.add(hostAddress); 49 | } 50 | } 51 | } 52 | return ips; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/easy/id/web/SegmentEasyIdController.java: -------------------------------------------------------------------------------- 1 | package com.easy.id.web; 2 | 3 | import com.easy.id.config.Module; 4 | import com.easy.id.service.EasyIdService; 5 | import com.easy.id.web.resp.ApiResponse; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.beans.factory.annotation.Qualifier; 8 | import org.springframework.validation.annotation.Validated; 9 | import org.springframework.web.bind.annotation.GetMapping; 10 | import org.springframework.web.bind.annotation.RequestMapping; 11 | import org.springframework.web.bind.annotation.RequestParam; 12 | import org.springframework.web.bind.annotation.RestController; 13 | 14 | import javax.validation.constraints.Max; 15 | import javax.validation.constraints.Min; 16 | import javax.validation.constraints.NotEmpty; 17 | import java.util.Set; 18 | import java.util.stream.Collectors; 19 | 20 | /** 21 | * @author zhangbingbing 22 | * @version 1.0.0 23 | * @createTime 2020年05月29日 24 | */ 25 | @RestController 26 | @RequestMapping("/segment/ids") 27 | @Validated 28 | @Module(value = "segment.enable") 29 | public class SegmentEasyIdController { 30 | 31 | @Autowired 32 | @Qualifier("segmentEasyIdService") 33 | private EasyIdService easyIdService; 34 | 35 | @GetMapping("/next_id") 36 | public ApiResponse getNextId(@NotEmpty String businessType) { 37 | return ApiResponse.data(easyIdService.getNextId(businessType).toString()); 38 | } 39 | 40 | @GetMapping("/next_id/batches") 41 | public ApiResponse> getNextId(@RequestParam(value = "batches_size", defaultValue = "100") 42 | @Min(value = 0, message = "批量生成数必须大于0") 43 | @Max(value = 1000, message = "最大支持批量生成数为1000") Integer batchSize, 44 | @NotEmpty String businessType) { 45 | return ApiResponse.data(easyIdService.getNextIdBatch(businessType, batchSize).stream() 46 | .map(Object::toString).collect(Collectors.toSet())); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/easy/id/entity/SegmentId.java: -------------------------------------------------------------------------------- 1 | package com.easy.id.entity; 2 | 3 | import java.util.concurrent.atomic.AtomicLong; 4 | 5 | /** 6 | * 号段 7 | */ 8 | public class SegmentId { 9 | 10 | /** 11 | * 该号段最大支持的ID 12 | */ 13 | private final long maxId; 14 | /** 15 | * 每次增长量 16 | */ 17 | private final int increment; 18 | /** 19 | * 模数 20 | */ 21 | private final int remainder; 22 | /** 23 | * 当前id超过这个数时,开始异步加载下个号段 24 | */ 25 | private final long loadingNextSegmentAt; 26 | 27 | private final AtomicLong currentId; 28 | 29 | private volatile boolean hasInit; 30 | 31 | public SegmentId(Segment segment) { 32 | this.maxId = segment.getMaxId(); 33 | this.currentId = new AtomicLong(segment.getMaxId() - segment.getStep()); 34 | // 当该号段30%的id被使用完时,开始异步加载下一个号段 35 | this.loadingNextSegmentAt = currentId.get() + (segment.getStep() * 3L / 10); 36 | this.increment = segment.getIncrement(); 37 | this.remainder = segment.getRemainder(); 38 | init(); 39 | } 40 | 41 | public boolean useful() { 42 | return currentId.get() <= maxId; 43 | } 44 | 45 | public Result nextId() { 46 | init(); 47 | long id = currentId.addAndGet(increment); 48 | if (id > maxId) { 49 | return new Result(ResultCode.OVER, null); 50 | } 51 | if (id >= loadingNextSegmentAt) { 52 | return new Result(ResultCode.SHOULD_LOADING_NEXT_SEGMENT, id); 53 | } 54 | return new Result(ResultCode.NORMAL, id); 55 | } 56 | 57 | private void init() { 58 | if (hasInit) { 59 | return; 60 | } 61 | synchronized (this) { 62 | if (hasInit) { 63 | return; 64 | } 65 | long id = currentId.get(); 66 | if ((id % increment) == remainder) { 67 | hasInit = true; 68 | return; 69 | } 70 | for (int i = 0; i <= increment; i++) { 71 | id = currentId.incrementAndGet(); 72 | if ((id % increment) == remainder) { 73 | currentId.addAndGet(-increment); 74 | hasInit = true; 75 | return; 76 | } 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/com/easy/id/config/DataSourceConfig.java: -------------------------------------------------------------------------------- 1 | package com.easy.id.config; 2 | 3 | import com.zaxxer.hikari.HikariConfig; 4 | import com.zaxxer.hikari.HikariDataSource; 5 | import lombok.Setter; 6 | import org.springframework.boot.context.properties.ConfigurationProperties; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | 10 | import java.sql.Connection; 11 | import java.sql.SQLException; 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | import java.util.Random; 15 | 16 | /** 17 | * @author zhangbingbing 18 | * @version 1.0.0 19 | * @createTime 2020年05月29日 20 | */ 21 | @Configuration 22 | @Module(value = "segment.enable") 23 | @ConfigurationProperties(prefix = "easy-id-generator.segment") 24 | public class DataSourceConfig { 25 | 26 | @Setter 27 | private List dbList; 28 | 29 | @Bean 30 | public DynamicDataSource dynamicDataSource() { 31 | // 初始化数据库 32 | List hikariDataSourceList = new ArrayList<>(dbList.size()); 33 | for (String db : dbList) { 34 | HikariConfig config = new HikariConfig("/" + db + ".properties"); 35 | hikariDataSourceList.add(new HikariDataSource(config)); 36 | } 37 | return new DynamicDataSource(hikariDataSourceList); 38 | } 39 | 40 | public static class DynamicDataSource { 41 | 42 | private final List hikariDataSourceList; 43 | // 同一个线程共用一个数据库连接 44 | private final ThreadLocal connectionThreadLocal = new ThreadLocal<>(); 45 | 46 | public DynamicDataSource(List hikariDataSourceList) { 47 | this.hikariDataSourceList = hikariDataSourceList; 48 | } 49 | 50 | public Connection getConnection() throws SQLException { 51 | Connection connection = connectionThreadLocal.get(); 52 | if (connection != null) { 53 | return connection; 54 | } 55 | connection = hikariDataSourceList.get(new Random().nextInt(hikariDataSourceList.size())).getConnection(); 56 | // 设置隔离级别为RC 57 | connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); 58 | connectionThreadLocal.set(connection); 59 | return connection; 60 | } 61 | 62 | public void releaseConnection() throws SQLException { 63 | final Connection connection = connectionThreadLocal.get(); 64 | if (connection != null) { 65 | connectionThreadLocal.remove(); 66 | connection.close(); 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | org.springframework.boot 8 | spring-boot-starter-parent 9 | 2.1.2.RELEASE 10 | 11 | 12 | 13 | com.easy.id 14 | easy-id-generator 15 | 1.0-SNAPSHOT 16 | 17 | UTF-8 18 | UTF-8 19 | 1.8 20 | 1.8 21 | 4.12 22 | 8.0.25 23 | 1.2.78 24 | 1.18.22 25 | 2.6.0 26 | 27 | 28 | 29 | 30 | org.projectlombok 31 | lombok 32 | ${lombok.version} 33 | provided 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-starter-web 38 | 39 | 40 | com.zaxxer 41 | HikariCP 42 | 43 | 44 | mysql 45 | mysql-connector-java 46 | ${mysql.connector.version} 47 | 48 | 49 | org.springframework.boot 50 | spring-boot-configuration-processor 51 | true 52 | 53 | 54 | com.alibaba 55 | fastjson 56 | ${com.alibaba.fastjson.version} 57 | 58 | 59 | org.apache.curator 60 | curator-recipes 61 | ${curator.version} 62 | 63 | 64 | 65 | 66 | 67 | org.springframework.boot 68 | spring-boot-maven-plugin 69 | 2.1.2.RELEASE 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /src/main/java/com/easy/id/service/segment/SegmentCachedIdGenerator.java: -------------------------------------------------------------------------------- 1 | package com.easy.id.service.segment; 2 | 3 | import com.easy.id.entity.Result; 4 | import com.easy.id.entity.ResultCode; 5 | import com.easy.id.entity.SegmentId; 6 | import com.easy.id.exception.GetNextIdException; 7 | import com.easy.id.service.generator.IdGenerator; 8 | import lombok.extern.slf4j.Slf4j; 9 | 10 | import java.util.Collections; 11 | import java.util.HashSet; 12 | import java.util.Set; 13 | import java.util.concurrent.ExecutorService; 14 | 15 | /** 16 | * 不同businessType,拥有不同的IdGenerator 17 | */ 18 | @Slf4j 19 | public class SegmentCachedIdGenerator implements IdGenerator { 20 | 21 | private final ExecutorService fetchNextSegmentExecutor; 22 | private final SegmentIdService segmentIdService; 23 | private final String businessType; 24 | private final Object lock = new Object(); 25 | private volatile SegmentId currentSegmentId; 26 | private volatile SegmentId nextSegmentId; 27 | /** 28 | * 是否异步加载下个号段中 29 | */ 30 | private volatile boolean isLoadingNextSegment = false; 31 | 32 | public SegmentCachedIdGenerator(ExecutorService fetchNextSegmentExecutor, SegmentIdService segmentIdService, String businessType) { 33 | this.fetchNextSegmentExecutor = fetchNextSegmentExecutor; 34 | this.segmentIdService = segmentIdService; 35 | this.businessType = businessType; 36 | } 37 | 38 | /** 39 | * 如果当前号段不存在或者用完了,如果下个号段存在,优先使用下个号段 40 | */ 41 | private synchronized void loadCurrent() { 42 | if (currentSegmentId == null || !currentSegmentId.useful()) { 43 | // 下个号段没有加载 44 | if (nextSegmentId == null) { 45 | currentSegmentId = segmentIdService.fetchNextSegmentId(businessType); 46 | } 47 | // 下个号段已经加载过了,直接使用 48 | if (nextSegmentId != null) { 49 | currentSegmentId = nextSegmentId; 50 | nextSegmentId = null; 51 | } 52 | } 53 | } 54 | 55 | private void loadNext() { 56 | // 下个号段没有被使用||下个号段正在加载 57 | if (nextSegmentId != null || isLoadingNextSegment) { 58 | return; 59 | } 60 | synchronized (lock) { 61 | if (nextSegmentId == null && !isLoadingNextSegment) { 62 | isLoadingNextSegment = true; 63 | fetchNextSegmentExecutor.submit(() -> { 64 | try { 65 | log.debug("异步加载下个号段"); 66 | nextSegmentId = segmentIdService.fetchNextSegmentId(businessType); 67 | } catch (Exception e) { 68 | log.error("异步加载下个号段失败", e); 69 | } finally { 70 | isLoadingNextSegment = false; 71 | } 72 | }); 73 | } 74 | } 75 | } 76 | 77 | @Override 78 | public Long nextId() { 79 | while (!Thread.currentThread().isInterrupted()) { 80 | loadCurrent(); 81 | final Result result = currentSegmentId.nextId(); 82 | final ResultCode code = result.getCode(); 83 | if (code == ResultCode.OVER) { 84 | loadCurrent(); 85 | } 86 | if (code == ResultCode.NORMAL) { 87 | return result.getId(); 88 | } 89 | // 异步加载下一个号段 90 | if (code == ResultCode.SHOULD_LOADING_NEXT_SEGMENT) { 91 | loadNext(); 92 | return result.getId(); 93 | } 94 | } 95 | throw new GetNextIdException("get next id fail"); 96 | } 97 | 98 | @Override 99 | public Set nextIds(int patchSize) { 100 | if (patchSize == 1) { 101 | return Collections.singleton(nextId()); 102 | } 103 | Set ids = new HashSet<>(patchSize); 104 | for (int i = 0; i < patchSize; i++) { 105 | ids.add(nextId()); 106 | } 107 | return ids; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/com/easy/id/service/segment/SegmentIdServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.easy.id.service.segment; 2 | 3 | import com.easy.id.config.DataSourceConfig; 4 | import com.easy.id.config.Module; 5 | import com.easy.id.entity.Segment; 6 | import com.easy.id.entity.SegmentId; 7 | import com.easy.id.exception.FetchSegmentFailException; 8 | import com.easy.id.exception.SegmentNotFoundException; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.beans.factory.annotation.Value; 12 | import org.springframework.stereotype.Service; 13 | 14 | import java.sql.Connection; 15 | import java.sql.PreparedStatement; 16 | import java.sql.ResultSet; 17 | import java.sql.SQLException; 18 | 19 | /** 20 | * 默认启用号段的方式 21 | */ 22 | @Service 23 | @Slf4j 24 | @Module("segment.enable") 25 | public class SegmentIdServiceImpl implements SegmentIdService { 26 | 27 | private final static String SELECT_BY_BUSINESS_TYPE_SQL = "select * from segment where business_type=?"; 28 | private final static String UPDATE_SEGMENT_MAX_ID = "update segment set max_id= ?,version=?,updated_at=? where id =? and version=?"; 29 | @Value("${easy-id-generator.segment.fetch-segment-retry-times:2}") 30 | private int retry; 31 | @Autowired 32 | private DataSourceConfig.DynamicDataSource dynamicDataSource; 33 | 34 | @Override 35 | public SegmentId fetchNextSegmentId(String businessType) { 36 | // 获取segment的时候,有可能存在version冲突,需要重试 37 | Connection connection; 38 | try { 39 | connection = dynamicDataSource.getConnection(); 40 | connection.setAutoCommit(false); 41 | for (int i = 0; i < retry; i++) { 42 | PreparedStatement statement = connection.prepareStatement(SELECT_BY_BUSINESS_TYPE_SQL); 43 | statement.setObject(1, businessType); 44 | final ResultSet resultSet = statement.executeQuery(); 45 | if (!resultSet.next()) { 46 | throw new SegmentNotFoundException("can not find segment of " + businessType); 47 | } 48 | final Segment segment = SegmentMapperUtil.mapRow(resultSet); 49 | statement = connection.prepareStatement(UPDATE_SEGMENT_MAX_ID); 50 | statement.setObject(1, segment.getMaxId() + segment.getStep()); 51 | statement.setObject(2, segment.getVersion() + 1); 52 | statement.setObject(3, System.currentTimeMillis()); 53 | statement.setObject(4, segment.getId()); 54 | statement.setObject(5, segment.getVersion()); 55 | try { 56 | // 更新成功 57 | if (statement.executeUpdate() == 1) { 58 | connection.commit(); 59 | log.debug("fetch {} next segment {} success", businessType, segment); 60 | return new SegmentId(segment); 61 | } 62 | // 乐观锁冲突,重试 63 | log.debug("fetch {} next segment {} conflict,retry", businessType, segment); 64 | } catch (SQLException e) { 65 | connection.rollback(); 66 | throw e; 67 | } 68 | } 69 | // 在有限重试机会下,没有获取到segment 70 | throw new FetchSegmentFailException("fetch " + businessType + " next segment fail after retry " + retry + " times"); 71 | } catch (Exception e) { 72 | throw new FetchSegmentFailException(e); 73 | } finally { 74 | try { 75 | dynamicDataSource.releaseConnection(); 76 | } catch (SQLException e) { 77 | log.error("release connection error", e); 78 | } 79 | } 80 | } 81 | 82 | public static class SegmentMapperUtil { 83 | public static Segment mapRow(ResultSet rs) throws SQLException { 84 | Segment segment = new Segment(); 85 | segment.setId(rs.getLong("id")); 86 | segment.setVersion(rs.getLong("version")); 87 | segment.setBusinessType(rs.getString("business_type")); 88 | segment.setMaxId(rs.getLong("max_id")); 89 | segment.setIncrement(rs.getInt("increment")); 90 | segment.setRemainder(rs.getInt("remainder")); 91 | segment.setStep(rs.getInt("step")); 92 | segment.setCreatedAt(rs.getLong("created_at")); 93 | segment.setUpdatedAt(rs.getLong("updated_at")); 94 | return segment; 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/com/easy/id/service/snowflake/Snowflake.java: -------------------------------------------------------------------------------- 1 | package com.easy.id.service.snowflake; 2 | 3 | import com.easy.id.exception.SystemClockCallbackException; 4 | import java.util.HashSet; 5 | import java.util.Random; 6 | import java.util.Set; 7 | import java.util.concurrent.TimeUnit; 8 | import lombok.extern.slf4j.Slf4j; 9 | 10 | @Slf4j 11 | public class Snowflake { 12 | 13 | /** 14 | * 2020-06-01 00:00:00 (UTC+8) 15 | */ 16 | private final long startAt = 1590940800000L; 17 | /** 18 | * 机器号 19 | */ 20 | private final int workIdBits = 10; 21 | /** 22 | * 序列号 23 | */ 24 | private final int sequenceBits = 12; 25 | /** 26 | * workId偏移量 27 | */ 28 | private final int workIdShift = sequenceBits; 29 | /** 30 | * 时间戳偏移量 31 | */ 32 | private final int timestampShift = workIdBits + sequenceBits; 33 | private final int sequenceMask = ~(-1 << sequenceBits); 34 | private final Random random = new Random(); 35 | private int sequence = 0; 36 | private long lastTimestamp = -1L; 37 | /** 38 | * 最大的机器号 39 | */ 40 | private final int maxWorkId = ~(-1 << workIdBits); 41 | private final int workId; 42 | 43 | public Snowflake(int workerID) { 44 | if (workerID > maxWorkId) { 45 | throw new IllegalStateException( 46 | "the work id " + workerID + " greater than max work Id " + maxWorkId); 47 | } 48 | workId = workerID; 49 | log.info("snowflake work id {}", workId); 50 | } 51 | 52 | public synchronized long nextId() { 53 | long now = now(); 54 | // 时钟回调了 55 | if (now < lastTimestamp) { 56 | long offset = lastTimestamp - now; 57 | if (offset > 5) { 58 | throw new SystemClockCallbackException("system clock callback slow " + offset); 59 | } 60 | try { 61 | this.wait(offset << 1); 62 | } catch (InterruptedException e) { 63 | throw new SystemClockCallbackException("system clock callback slow " + offset); 64 | } 65 | } 66 | // 同一毫秒内 67 | if (now == lastTimestamp) { 68 | sequence = (sequence + 1) & sequenceMask; 69 | // 该毫秒内的sequence已经用完了 70 | if (sequence == 0) { 71 | sequence = random.nextInt(100); 72 | now = tillNextMill(lastTimestamp); 73 | } 74 | } 75 | // 从新的毫秒开始 76 | if (now > lastTimestamp) { 77 | sequence = random.nextInt(100); 78 | } 79 | lastTimestamp = now; 80 | return toId(lastTimestamp, workId, sequence); 81 | } 82 | 83 | public synchronized Set nextIds(int batchSize) { 84 | if ((batchSize & sequenceMask) == 0) { 85 | throw new IllegalArgumentException("batch size " + batchSize); 86 | } 87 | long now = now(); 88 | if (now < lastTimestamp) { 89 | long offset = lastTimestamp - now; 90 | if (offset > 5) { 91 | throw new SystemClockCallbackException("system clock callback slow " + offset); 92 | } 93 | try { 94 | this.wait(offset << 1); 95 | } catch (InterruptedException e) { 96 | throw new SystemClockCallbackException("system clock callback slow " + offset, e); 97 | } 98 | } 99 | Set nextIds = new HashSet<>(batchSize); 100 | while (nextIds.size() < batchSize) { 101 | // 在本毫秒 102 | if (now == lastTimestamp) { 103 | sequence = (sequence + 1) & sequenceMask; 104 | // 本毫秒内的sequence用完了 105 | if (sequence == 0) { 106 | sequence = random.nextInt(100); 107 | now = tillNextMill(lastTimestamp); 108 | } 109 | nextIds.add(toId(now, workId, sequence)); 110 | continue; 111 | } 112 | // 在新的毫秒 113 | if (now > lastTimestamp) { 114 | sequence = random.nextInt(100); 115 | int loop = batchSize - nextIds.size(); 116 | for (int i = 0; i < loop; i++) { 117 | sequence = sequence + 1; 118 | nextIds.add(toId(now, workId, sequence)); 119 | } 120 | } 121 | } 122 | lastTimestamp = now; 123 | return nextIds; 124 | } 125 | 126 | /** 127 | * 等待下个毫秒,防止等待期间系统时钟被回调,导致方法一直轮询 128 | */ 129 | private long tillNextMill(long lastTimestamp) { 130 | long timestamp; 131 | long offset; 132 | while (true) { 133 | timestamp = now(); 134 | offset = lastTimestamp - timestamp; 135 | if (offset < 0) { 136 | return timestamp; 137 | } 138 | // 时钟回拨不超过5毫秒 139 | if (offset < 5) { 140 | try { 141 | TimeUnit.MILLISECONDS.sleep(offset); 142 | } catch (InterruptedException ignore) { 143 | } 144 | } else { 145 | throw new SystemClockCallbackException( 146 | "timestamp check error,last timestamp " + lastTimestamp + ",now " + timestamp); 147 | } 148 | } 149 | } 150 | 151 | private long toId(long timestamp, int workId, int sequence) { 152 | return ((timestamp - startAt) << timestampShift) | ((long) workId << workIdShift) | sequence; 153 | } 154 | 155 | private long now() { 156 | return System.currentTimeMillis(); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/main/java/com/easy/id/service/snowflake/SnowflakeZKHolder.java: -------------------------------------------------------------------------------- 1 | package com.easy.id.service.snowflake; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.easy.id.config.Module; 5 | import com.easy.id.exception.SystemClockCallbackException; 6 | import com.easy.id.util.IPUtil; 7 | import lombok.AllArgsConstructor; 8 | import lombok.Data; 9 | import lombok.NoArgsConstructor; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.apache.curator.framework.CuratorFramework; 12 | import org.apache.curator.framework.CuratorFrameworkFactory; 13 | import org.apache.curator.retry.RetryUntilElapsed; 14 | import org.apache.zookeeper.CreateMode; 15 | import org.apache.zookeeper.data.Stat; 16 | import org.springframework.beans.factory.annotation.Autowired; 17 | import org.springframework.beans.factory.annotation.Qualifier; 18 | import org.springframework.beans.factory.annotation.Value; 19 | import org.springframework.stereotype.Component; 20 | import org.springframework.util.ResourceUtils; 21 | 22 | import javax.annotation.PostConstruct; 23 | import java.io.File; 24 | import java.io.FileNotFoundException; 25 | import java.io.IOException; 26 | import java.io.InputStream; 27 | import java.nio.file.Files; 28 | import java.util.HashMap; 29 | import java.util.Map; 30 | import java.util.Properties; 31 | import java.util.concurrent.ScheduledExecutorService; 32 | import java.util.concurrent.TimeUnit; 33 | 34 | @Slf4j 35 | @Component 36 | @Module(value = "snowflake.enable") 37 | public class SnowflakeZKHolder { 38 | 39 | private static final String SPLIT = "-"; 40 | /** 41 | * 保存所有数据持久的节点 42 | */ 43 | private static final String ZK_PATH = "/easy-id-generator/snowflake/forever"; 44 | /** 45 | * 持久化workerId,文件存放位置 46 | */ 47 | private static final String DUMP_PATH = "workerID/workerID.properties"; 48 | 49 | @Autowired 50 | @Qualifier("updateDataToZKScheduledExecutorService") 51 | private ScheduledExecutorService scheduledExecutorService; 52 | 53 | @Value("${easy-id-generator.snowflake.load-worker-id-from-file-when-zk-down:true}") 54 | private boolean loadWorkerIdFromFileWhenZkDown; 55 | /** 56 | * 本机地址 57 | */ 58 | private String localIp; 59 | /** 60 | * 本机端口 61 | */ 62 | @Value("${server.port}") 63 | private String localPort; 64 | /** 65 | * zk连接地址 66 | * eg: ip1:port1,ip2:port2 67 | */ 68 | @Value("${easy-id-generator.snowflake.zk.connection-string}") 69 | private String zkConnectionString; 70 | private Integer workerId; 71 | /** 72 | * 上次更新数据时间 73 | */ 74 | private long lastUpdateAt; 75 | 76 | private volatile boolean hasInitFinish = false; 77 | 78 | @PostConstruct 79 | public void postConstruct() { 80 | try { 81 | init(); 82 | } catch (Exception e) { 83 | throw new IllegalStateException(e); 84 | } 85 | } 86 | 87 | public int getWorkerID() { 88 | if (hasInitFinish) { 89 | return workerId; 90 | } 91 | throw new IllegalStateException("worker id not init"); 92 | } 93 | 94 | private void init() throws Exception { 95 | try { 96 | localIp = IPUtil.getHostAddress(); 97 | String localZKPath = ZK_PATH + "/" + localIp + ":" + localPort; 98 | CuratorFramework client = connectToZk(); 99 | client.start(); 100 | final Stat stat = client.checkExists().forPath(ZK_PATH); 101 | // 不存在根结点,第一次使用,创建根结点 102 | if (stat == null) { 103 | // 创建有序永久结点 /easy-id-generator/snowflake/forever/ip:port-xxx,并上传数据 104 | localZKPath = createPersistentSequentialNode(client, localZKPath, buildData()); 105 | workerId = getWorkerId(localZKPath); 106 | // 持久化workerId 107 | updateWorkerId(workerId); 108 | // 定时上报本机时间到zk 109 | scheduledUploadTimeToZK(client, localZKPath); 110 | hasInitFinish = true; 111 | return; 112 | } 113 | // Map 114 | Map localAddressWorkerIdMap = new HashMap<>(16); 115 | // Map 116 | Map localAddressPathMap = new HashMap<>(16); 117 | for (String key : client.getChildren().forPath(ZK_PATH)) { 118 | final String[] split = key.split("-"); 119 | localAddressPathMap.put(split[0], key); 120 | // value=zk有序结点的需要 121 | localAddressWorkerIdMap.put(split[0], Integer.valueOf(split[1])); 122 | } 123 | String localAddress = localIp + ":" + localPort; 124 | workerId = localAddressWorkerIdMap.get(localAddress); 125 | if (workerId != null) { 126 | localZKPath = ZK_PATH + "/" + localAddressPathMap.get(localAddress); 127 | // 校验时间是否回调 128 | checkTimestamp(client, localZKPath); 129 | scheduledUploadTimeToZK(client, localZKPath); 130 | updateWorkerId(workerId); 131 | hasInitFinish = true; 132 | return; 133 | } 134 | localZKPath = createPersistentSequentialNode(client, localZKPath, buildData()); 135 | workerId = Integer.parseInt((localZKPath.split("-"))[1]); 136 | scheduledUploadTimeToZK(client, localZKPath); 137 | updateWorkerId(workerId); 138 | hasInitFinish = true; 139 | } catch (Exception e) { 140 | if (!loadWorkerIdFromFileWhenZkDown) { 141 | throw e; 142 | } 143 | log.error("can load worker id from zk , try to load worker id from file", e); 144 | // 从本地文件中读取workerId,如果系统时针回调,可能会出现 145 | final Integer workerIdFromFile = loadWorkerIdFromFile(); 146 | if (workerIdFromFile != null) { 147 | workerId = workerIdFromFile; 148 | hasInitFinish = true; 149 | return; 150 | } 151 | throw e; 152 | } 153 | } 154 | 155 | 156 | private Integer getWorkerId(String localZKPath) { 157 | String[] split = localZKPath.split(SPLIT); 158 | return Integer.parseInt(split[1]); 159 | } 160 | 161 | /** 162 | * @return true 检查通过 163 | */ 164 | private void checkTimestamp(CuratorFramework client, String localZKPath) throws Exception { 165 | final Endpoint endpoint = JSON.parseObject(new String(client.getData().forPath(localZKPath)), Endpoint.class); 166 | // 该节点的时间不能大于最后一次上报的时间 167 | if (endpoint.getTimestamp() > System.currentTimeMillis()) { 168 | throw new SystemClockCallbackException("system clock callback"); 169 | } 170 | } 171 | 172 | private void scheduledUploadTimeToZK(CuratorFramework client, String localZKPath) { 173 | scheduledExecutorService.schedule(() -> { 174 | // 如果时针回调了就不同步 175 | if (System.currentTimeMillis() < lastUpdateAt) { 176 | return; 177 | } 178 | try { 179 | client.setData().forPath(localZKPath, buildData()); 180 | lastUpdateAt = System.currentTimeMillis(); 181 | log.debug("upload time to zk at" + lastUpdateAt); 182 | } catch (Exception e) { 183 | log.error("update init data error path is {} error is {}", localZKPath, e); 184 | } 185 | }, 5, TimeUnit.SECONDS); 186 | } 187 | 188 | private Integer loadWorkerIdFromFile() { 189 | try (InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream(DUMP_PATH)) { 190 | Properties properties = new Properties(); 191 | properties.load(resourceAsStream); 192 | final String workerID = properties.getProperty("workerID"); 193 | if (workerID != null) { 194 | return Integer.parseInt(workerID); 195 | } 196 | return null; 197 | } catch (IOException e) { 198 | log.error("load worker id from file error", e); 199 | } 200 | return null; 201 | } 202 | 203 | private void updateWorkerId(int workerId) { 204 | if (!loadWorkerIdFromFileWhenZkDown) { 205 | return; 206 | } 207 | try { 208 | String classpath = ResourceUtils.getURL("classpath:").getFile(); 209 | File file = new File(classpath + "/" + DUMP_PATH); 210 | if (!file.exists()) { 211 | boolean mkdirs = file.getParentFile().mkdirs(); 212 | if (!mkdirs) { 213 | log.error("mkdir {} error", file.getParentFile().toString()); 214 | return; 215 | } 216 | log.info("mkdir {}", file.toString()); 217 | } 218 | Files.write(file.toPath(), ("workerID=" + workerId).getBytes()); 219 | } catch (FileNotFoundException e) { 220 | log.error("", e); 221 | } catch (IOException e) { 222 | log.warn("write workerID to file {} error", DUMP_PATH, e); 223 | } 224 | } 225 | 226 | private String createPersistentSequentialNode(CuratorFramework client, String path, byte[] data) throws Exception { 227 | return client.create() 228 | .creatingParentsIfNeeded() 229 | .withMode(CreateMode.PERSISTENT_SEQUENTIAL) 230 | .forPath(path + "-", data); 231 | } 232 | 233 | private CuratorFramework connectToZk() { 234 | return CuratorFrameworkFactory.builder() 235 | .connectString(zkConnectionString) 236 | .retryPolicy(new RetryUntilElapsed((int) TimeUnit.SECONDS.toMillis(5), (int) TimeUnit.SECONDS.toMillis(1))) 237 | .connectionTimeoutMs((int) TimeUnit.SECONDS.toMillis(10)) 238 | .sessionTimeoutMs((int) TimeUnit.SECONDS.toMillis(6)) 239 | .build(); 240 | } 241 | 242 | private byte[] buildData() { 243 | return JSON.toJSONString(new Endpoint(localIp, localPort, System.currentTimeMillis())).getBytes(); 244 | } 245 | 246 | /** 247 | * 上传到zk的数据 248 | */ 249 | @Data 250 | @AllArgsConstructor 251 | @NoArgsConstructor 252 | private static class Endpoint { 253 | private String ip; 254 | private String port; 255 | private Long timestamp; 256 | } 257 | } 258 | --------------------------------------------------------------------------------