Java基于数据库表的ID生成序列
Oracle和Postgresql等数据库都有主键序列生成功能,但存在一定局限性,例如:每条数据生成都需要查询一次序列、没有其它业务属性、过度依赖特定数据库环境等。为解决上述问题就需要由业务代码与数据库表相结合来编写一套分布式高性能的ID序列生成方法(修复了在分布式集群环境中的一些bug)。
基本业务流程
客户端获取ID时先判断本地内存是否有足够生成ID个数,如果有就在ID池中获取一个,否则从数据中批量取出一部分到本地内存,然后在取出一个提供给相应业务使用。考虑到在多线程和分布式集群环境下,需要做到高性能、高可靠性和ID生成不重复,就需要结合本地锁和分布式锁来保证。使用本地电脑测试100线程循环100次,大概1秒可生成3W个ID。以下为代码实现:
数据库表结构
DROP TABLE IF EXISTS "public"."pub_c_id_sequence";
CREATE TABLE "public"."pub_c_id_sequence" (
"guid" varchar(32) COLLATE "pg_catalog"."default" NOT NULL,
"func_name" varchar(50) COLLATE "pg_catalog"."default" NOT NULL,
"seq_no" int8 NOT NULL DEFAULT 0,
"remark" varchar(100) COLLATE "pg_catalog"."default",
"create_datetime" timestamp(6) NOT NULL,
"update_datetime" timestamp(6),
"enable_flag" varchar(2) COLLATE "pg_catalog"."default" NOT NULL DEFAULT 1
)
;
ALTER TABLE "public"."pub_c_id_sequence" OWNER TO "postgres";
COMMENT ON COLUMN "public"."pub_c_id_sequence"."func_name" IS '功能名称';
COMMENT ON COLUMN "public"."pub_c_id_sequence"."seq_no" IS '序列号';
COMMENT ON COLUMN "public"."pub_c_id_sequence"."remark" IS '描述';
COMMENT ON COLUMN "public"."pub_c_id_sequence"."enable_flag" IS '是否删除';
CREATE INDEX "pub_c_id_sequence_func_name_idx" ON "public"."pub_c_id_sequence" USING btree (
"func_name" COLLATE "pg_catalog"."default" "pg_catalog"."text_ops" ASC NULLS LAST,
"create_datetime" "pg_catalog"."timestamp_ops" ASC NULLS LAST
);
ALTER TABLE "public"."pub_c_id_sequence" ADD CONSTRAINT "pub_c_id_sequence_pk" PRIMARY KEY ("guid");
数据库表映射DO
@Data
@ApiModel(value = "FocusDeployDO对象", description = "ID序列表")
@TableName("pub_c_id_sequence")
public class IdSequenceDO {
/**
* 标识号
*/
@NotEmpty(groups = IdSequenceDO.class, message = "主键ID")
@ApiModelProperty("主键ID")
@TableId(type = IdType.ASSIGN_ID, value = "guid")
private String guid;
/**
* 功能名称
*/
@NotEmpty(groups = IdSequenceDO.class, message = "功能名称不能为空")
@ApiModelProperty("主键ID")
@TableField(value = "func_name", jdbcType = JdbcType.VARCHAR)
private String funcName;
/**
* 序列号
*/
@ApiModelProperty("序列号")
@TableField(value = "seq_no", jdbcType = JdbcType.BIGINT, javaType = true)
private Long seqNo;
/**
* 描述
*/
@ApiModelProperty("描述")
@TableField(value = "remark", jdbcType = JdbcType.VARCHAR)
private String remark;
/**
* 创建时间
*/
@ApiModelProperty("创建时间")
@TableField(value = "create_datetime")
private Date createDatetime;
/**
* 更新时间
*/
@ApiModelProperty("更新时间")
@TableField(value = "update_datetime")
private Date updateDatetime;
/**
* 是否删除
*/
@ApiModelProperty("是否删除")
@TableField(value = "enable_flag", jdbcType = JdbcType.VARCHAR)
private String enableFlag;
}
序列生成业务层接口
public interface IdSequenceService {
/**
* 获取新的ID
*
* @return ID
*/
Long getAlarmId() throws InterruptedException;
/**
* 获取新的ID
*
* @return ID
*/
Long getId() throws InterruptedException;
/**
* 获取新的ID
*
* @param funcName 功能名称
* @return ID
*/
Long getId(String funcName) throws InterruptedException;
/**
* 获取新的ID
*
* @param funcName 功能名称
* @param steps 步长
* @return ID
*/
Long getId(String funcName, int steps) throws InterruptedException;
/**
* 获取新的ID
*
* @param funcName 功能名称
* @param steps 步长
* @param remark 描述
* @return ID
*/
Long getId(String funcName, int steps, String remark) throws InterruptedException;
}
序列生成业务处理
@Component
@Slf4j
public class IdSequenceServiceImpl extends ServiceImpl<IdSequnceMapper, IdSequenceDO> implements IdSequenceService {
/**
* ID锁名称
*/
private static final String ID_SEQ_LOCK = "mcc:pub:id:lock:";
/**
* ID获取失败重试次数
*/
private static final int GET_ID_RETRY_COUNT = 10;
/**
* ID获取失败超时时间(毫秒)
*/
private static final int GET_ID_RETRY_TIMEOUT = 10;
/**
* ID获取超时时间(毫秒)
*/
private static final int GET_ID_TIMEOUT = GET_ID_RETRY_COUNT * GET_ID_RETRY_TIMEOUT + 10;
/**
* ID缓存
*/
private Map<String, IdSeqVo> idCache = new HashMap<>(5);
@Resource
private RedissonClient redissonClient;
/**
* ID锁
*/
private NameReentrantLock idLock;
@Resource
private TransactionTemplate transactionTemplate;
public IdSequenceServiceImpl() {
idLock = new NameReentrantLock();
}
/**
* 获取新的ID
*
* @return ID
*/
@Override
// @Transactional(propagation = Propagation.NOT_SUPPORTED)
public String getAlarmId() {
String dateStr = utcNow("yyyyMMdd");
Long id = getId("ALARM-" + dateStr, 100, "用于ICC或VCS创建接警单使用");
return dateStr + id;
}
/**
* 获取新的ID
*
* @return ID
*/
@Override
public Long getId() {
return getId("DEFAULT");
}
/**
* 获取新的ID
*
* @param funcName 功能名称
* @return ID
*/
@Override
public Long getId(String funcName) {
return getId(funcName, 1000);
}
/**
* 获取新的ID
*
* @param funcName 功能名称
* @return ID
*/
@Override
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public String getIdStr(String funcName) {
return String.valueOf(getId(funcName, 1000));
}
/**
* 获取新的ID
*
* @param funcName 功能名称
* @param steps 步长
* @return ID
*/
@Override
public Long getId(String funcName, int steps) {
return getId(funcName, steps, null);
}
/**
* 获取新的ID
*
* @param funcName 功能名称
* @param steps 步长
* @return ID
*/
@Override
public String getIdStr(String funcName, int steps) {
return String.valueOf(getId(funcName, steps, null));
}
/**
* 获取新的ID
*
* @param funcName 功能名称
* @param steps 步长
* @param remark 描述
* @return ID
*/
@Override
public Long getId(String funcName, int steps, String remark) {
Assert.hasText(funcName, "功能名称不能为空!");
Assert.isTrue(steps > 0 && steps < 10000, "步长须在0 ~ 10000之间!");
try {
// 读取缓存中的ID并自增加1
if (idLock.tryLock(funcName, GET_ID_TIMEOUT, TimeUnit.SECONDS)) {
IdSeqVo idSeqVo = idCache.get(funcName);
if (idSeqVo != null && !idSeqVo.isExhausted()) {
return idSeqVo.getId();
}
for (int i = 0; i < GET_ID_RETRY_COUNT; i++) {
// 如果缓存中没有就从存储中获取
RLock lock = redissonClient.getLock(ID_SEQ_LOCK + funcName);
if (lock.tryLock(GET_ID_RETRY_TIMEOUT, GET_ID_RETRY_TIMEOUT, TimeUnit.SECONDS)) {
try {
return getIdFromStore(funcName, steps, remark);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
}
}
throw new InterruptedException("从存储中获取ID超时");
} catch (Exception e) {
log.error("获取ID失败!", e);
throw new RuntimeException(e.getMessage());
} finally {
idLock.unlock(funcName);
}
}
/**
* 从存储中获取新的ID
*
* @param funcName 功能名称
* @param steps 步长
* @param remark 描述
* @return ID
*/
private Long getIdFromStore(String funcName, int steps, String remark) {
IdSeqVo id = transactionTemplate.execute((action) -> {
// 查询数据库里是否有此序列
LambdaQueryWrapper<IdSequenceDO> queryWrapper = Wrappers.lambdaQuery(IdSequenceDO.class)
.eq(IdSequenceDO::getFuncName, funcName)
.eq(IdSequenceDO::getDelete, DbFiledConstant.DATA_ENABLE_STR);
IdSequenceDO idSeq = this.getBaseMapper().selectOne(queryWrapper);
// 如果存在就更新,否则就新增一个序列
if (idSeq == null) {
idSeq = new IdSequenceDO();
idSeq.setSeqNo(Long.valueOf(steps));
idSeq.setCreateDatetime(new Date());
idSeq.setFuncName(funcName);
idSeq.setDelete(DbFiledConstant.DATA_ENABLE_STR);
idSeq.setRemark(remark);
this.getBaseMapper().insert(idSeq);
} else {
idSeq.setSeqNo(idSeq.getSeqNo() + steps);
idSeq.setUpdateDatetime(new Date());
this.getBaseMapper().updateById(idSeq);
}
IdSeqVo idSeqVO = idCache.get(funcName);
if (idSeqVO == null) {
idSeqVO = new IdSeqVo();
}
idSeqVO.setCurrent(new AtomicLong(idSeq.getSeqNo() - steps));
idSeqVO.setEnd(idSeq.getSeqNo());
idCache.put(funcName, idSeqVO);
log.info("idSeq database:{} local:{}", idSeq.getSeqNo(), idSeqVO.getCurrent().get());
return idSeqVO;
});
return id.getId();
}
@Setter
@Getter
class IdSeqVo {
/**
* 序列已用尽标识
*/
public static final long SEQUENCE_EXHAUSTED_FLAG = -0L;
/**
* 当前序列
*/
private AtomicLong current;
/**
* 最大序列
*/
private Long end;
/**
* 获取一个新的ID
*
* @return
*/
public long getId() {
if (isExhausted()) {
throw new RuntimeException("ID序列已用尽!");
}
return current.incrementAndGet();
}
/**
* 判断序列是否用尽
*
* @return
*/
public boolean isExhausted() {
if (current == null) {
throw new RuntimeException("ID序列没有初始化!");
}
return current.get() >= end;
}
}
public static LocalDateTime utcNow() {
LocalDateTime dateTime = LocalDateTime.now(ZoneId.of("UTC"));
return dateTime;
}
public static String utcNow(String pattern) {
LocalDateTime dateTime = utcNow();
return dateTime.format(DateTimeFormatter.ofPattern(pattern));
}
}
本地锁
public class NameReentrantLock {
private Map<String, ReentrantLock> lockMap = new HashMap<>(5);
/**
* 长时加锁
*
* @param name 名称
* @param timeout 超时时间
* @param unit 超时单位
* @return
* @throws InterruptedException
*/
public boolean tryLock(String name, long timeout, TimeUnit unit) throws InterruptedException {
ReentrantLock lock = lockMap.get(name);
if (lock == null) {
synchronized (this) {
lock = lockMap.get(name);
if (lock == null) {
lock = new ReentrantLock();
lockMap.put(name, lock);
}
}
}
return lock.tryLock(timeout, unit);
}
/**
* 判断是否加锁
* @param name
* @return
*/
public boolean isLocked(String name) {
ReentrantLock lock = lockMap.get(name);
if (lock == null) {
return false;
}
return lock.isLocked();
}
/**
* 解锁
* @param name
*/
public void unlock(String name) {
ReentrantLock lock = lockMap.get(name);
if (lock == null || !lock.isLocked()) {
return;
}
lock.unlock();
}
}
测试代码
for (int i = 0; i < 100; i++) {
new Thread(() -> {
try {
for (int j = 0; j < 100; j++) {
log.info("----------" + idSequenceService.getAlarmId());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}