本文紧接着上文高并发系统-分布式唯一ID生成(一)
2. 生成方案
2.4 号段模式
号段模式,则是指发号服务每次会从数据库获取一批 ID,然后将这批 ID 缓存到发号服务本地。
业务系统每次向发号服务请求 ID 时,后者会先判断其本地是否还有可用的 ID 分配给业务系统。如果有则直接分配,反之则再次访问数据库来批量获取 ID。
1. 优点
显然在号段模式下,由于发号服务不用每次都请求数据库
提高了系统的可用性、可靠性
后续由于业务拓展、业务系统激增时,对基于号段模式的设计方案进行分库分表也非常便于实现
2. 缺点
依赖数据库,数据库本身有一些缺陷
ID无业务含义
3. 应用场景
应用在一些并发量中等,不想依赖额外中间件(如REDIS)和发号服务场景。 比如生成订单号场景,可通过应用服务横向扩展
4. 案例
下面是一个基于号段模式生成CODE的案例,可集成在微服务本身,只需要在数据库增加一张表即可。
数据库设计
数据库表如下:
sql
drop TABLE IF EXISTS `code_seq`;
create TABLE `code_seq` (
`env` VARCHAR(10) not null COMMENT '环境编号',
`prefix` int(10) not null COMMENT 'code前缀',
`seq` int(10) not null COMMENT '当前序列',
`update_time` datetime not null DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY(`prefix`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 comment 'CODE编号表';
可在code_seq
预制多条数据,每条相当于是一个自增主键,有很好的扩展性。
每次获取一批号段,需要更新seq
和update_time
预制数据如下
sql
INSERT INTO code_seq (env, prefix, seq)
VALUES
('T1', 1001, 0),
('T1', 1002, 0),
('T1', 1003, 0),
('T1', 1004, 0),
('T1', 1005, 0),
('T1', 1006, 0),
('T1', 1007, 0),
('T1', 1008, 0),
('T1', 1009, 0),
('T1', 1010, 0);
代码设计
整体UML类图如下:
CodeHandler
:主类,提供初始化和生成Code方法
主要采用并发原子类减少并发依赖,从数据库可以捞取一批codePrefixSet
,在code缓存不到UPDATE_THRESHOLD
比例时,会域加载下一批code到缓存中
java
public class CodeHandler {
private static final double UPDATE_THRESHOLD = 0.9f;
private final String name;
private final Set<Integer> codePrefixSet = new ConcurrentSkipListSet<>();
private final BlockingQueue<CodeSegment> codeSegmentBlockingQueue = new LinkedBlockingQueue<>(1);
private final AtomicBoolean needLoading = new AtomicBoolean(false);
private final AtomicBoolean statusHealthy = new AtomicBoolean(false);
private final String idc = "T1";
private CodeSeqService codeSeqService;
private CodeTpsMonitor codeTpsMonitor;
private Executor loadingExecutor;
public CodeHandler(String name) {
this.name = name;
}
public void setCodeSeqService(CodeSeqService codeSeqService) {
this.codeSeqService = codeSeqService;
}
public void init() {
this.loadingExecutor = Executors.newSingleThreadExecutor(new BasicThreadFactory.Builder().namingPattern("order-handle-" + name).daemon(true).priority(Thread.MAX_PRIORITY).build());
this.codeTpsMonitor = new CodeTpsMonitor(name);
this.codeTpsMonitor.start();
initQueue();
}
private void initQueue() {
CodeSeqRet codeSeqRet = loadCodeAlloc();
try {
codeSegmentBlockingQueue.put(new CodeSegment(codeSeqRet.getPrefix(), codeSeqRet.getEnd(), codeSeqRet.getStart()));
} catch (InterruptedException e) {
log.error("initQueue put error", e);
}
}
private CodeSeqRet loadCodeAlloc() {
CodeSeqRet result = null;
try {
loadCodePrefix();
int retryCount = 0;
while (Objects.isNull(result) && !codePrefixSet.isEmpty() && retryCount <= 2) {
int index = SecureRandomUtil.getInstance().nextInt(codePrefixSet.size());
Integer prefix = (codePrefixSet.toArray(new Integer[codePrefixSet.size()]))[index];
CodeSeqRet codeSeqRet = codeSeqService.generateCodeByPrefix(idc, prefix, codeTpsMonitor.getStep().get());
int retCode = Objects.isNull(codeSeqRet) ? 1 : codeSeqRet.getResult();
if (retCode == 0) {
result = codeSeqRet;
} else if (retCode == 1) {
this.codePrefixSet.remove(prefix);
if (this.codePrefixSet.isEmpty()) {
loadCodePrefix();
}
} else {
retryCount++;
}
}
} finally {
this.statusHealthy.set(result != null);
}
if (Objects.isNull(result)) {
throw new IllegalStateException("load code from db error!");
}
return result;
}
private void loadCodePrefix() {
if (!codePrefixSet.isEmpty()) {
return;
}
codePrefixSet.addAll(codeSeqService.selectPrefixByEnv(idc));
if (codePrefixSet.isEmpty()) {
throw new IllegalStateException("no code prefix,plz check db config.");
}
}
public String getCode() {
this.codeTpsMonitor.increase();
String code = null;
int retryNum = 0;
while (Objects.isNull(code)) {
CodeSegment curSegment = this.codeSegmentBlockingQueue.peek();
if (Objects.nonNull(curSegment)) {
if (curSegment.getIdle() <= UPDATE_THRESHOLD * codeTpsMonitor.getStep().get() && this.needLoading.compareAndSet(false, true)) {
this.loadingExecutor.execute(new CodeLoader());
}
code = curSegment.getCode();
if (Objects.isNull(code)) {
this.codeSegmentBlockingQueue.poll();
}
} else {
if (!this.statusHealthy.get() || retryNum > 2) {
throw new IllegalStateException("create code failed,no available codes.");
}
}
retryNum++;
if (Objects.isNull(code)) {
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(10L));
}
}
return code;
}
private class CodeLoader implements Runnable {
@Override
public void run() {
CodeSeqRet codeSeqRet = null;
while (Objects.isNull(codeSeqRet)) {
try {
codeSeqRet = loadCodeAlloc();
} catch (Exception e) {
log.error("load code error.", e);
LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
}
}
try {
CodeHandler.this.codeSegmentBlockingQueue.put(new CodeSegment(codeSeqRet.getPrefix(), codeSeqRet.getEnd(), codeSeqRet.getStart()));
} catch (InterruptedException e) {
log.error("CodeLoader put queue error", e);
}
CodeHandler.this.needLoading.set(false);
}
}
}
CodeSegment
记录prefix以及当前队列值和剩余余量方法
java
public class CodeSegment {
private final int prefix;
private final int maxSeq;
private AtomicLong curSequence;
public CodeSegment(int prefix,int maxSeq,long start) {
this.prefix = prefix;
this.maxSeq = maxSeq;
curSequence = new AtomicLong(start);
}
public String getCode() {
long value = curSequence.getAndIncrement();
if (value <= maxSeq) {
return prefix + String.format(Locale.ENGLISH,"%07d",value);
} else {
return null;
}
}
public long getIdle() {
return maxSeq - curSequence.get() + 1;
}
}
CodeTpsMonitor
是TPS监控器,可以根据CODE生成值动态调整预获取CODE的批量值
java
public class CodeTpsMonitor implements Runnable {
public static final int INITIAL_BATCH_COUNT = 100;
private static final int MAX_BATCH_COUNT = 1000;
private final AtomicInteger step = new AtomicInteger(INITIAL_BATCH_COUNT);
private final String name;
private AtomicInteger count = new AtomicInteger(0);
private long startTime;
private ScheduledExecutorService scheduledExecutorService;
public CodeTpsMonitor(String name) {
this.name = name;
}
public void start() {
scheduledExecutorService = new ScheduledThreadPoolExecutor(1, new BasicThreadFactory.Builder()
.namingPattern("check-" + name + "-order-thread").daemon(true).build());
scheduledExecutorService.scheduleWithFixedDelay(this, 1, 5, TimeUnit.SECONDS);
}
public void increase() {
count.incrementAndGet();
}
public AtomicInteger getStep() {
return step;
}
@Override
public void run() {
//重置count和时间
long start = startTime;
int reqNum = count.getAndSet(0);
startTime = System.currentTimeMillis();
long timeCost = startTime - start;
final long tps = reqNum * 1000 / timeCost;
int newBatchCount;
if (tps < INITIAL_BATCH_COUNT) {
newBatchCount = INITIAL_BATCH_COUNT;
} else if (tps > MAX_BATCH_COUNT) {
newBatchCount = MAX_BATCH_COUNT;
} else {
newBatchCount = (int) tps;
}
step.set(newBatchCount);
}
}
CodeSeqService
是与数据库交互服务
java
public class CodeSeqService {
private static final int MAX_SEQ = 999999999;
@Resource
private CodeSeqMapper codeSeqMapper;
public List<Integer> selectPrefixByEnv(String env) {
return codeSeqMapper.selectPrefixByEnv(env, MAX_SEQ);
}
@Transactional(timeout = 300, isolation = Isolation.REPEATABLE_READ, rollbackFor = Throwable.class)
public CodeSeqRet generateCodeByPrefix(String env, Integer prefix, Integer step) {
log.info("generateCodeByPrefix begin env={},prefix={},step={}", env, prefix, step);
//加上行锁
CodeSeq codeSeq = codeSeqMapper.selectCodeSeqByPrefix(env, prefix);
CodeSeqRet codeSeqRet = new CodeSeqRet();
codeSeqRet.setResult(0);
codeSeqRet.setPrefix(prefix);
if (Objects.isNull(codeSeq) || Objects.isNull(codeSeq.getSequence())) {
codeSeqRet.setResult(1);
return codeSeqRet;
}
if (codeSeq.getSequence() > MAX_SEQ) {
codeSeqRet.setResult(1);
return codeSeqRet;
}
if (MAX_SEQ - codeSeq.getSequence() + 1 < step) {
codeSeqRet.setStart(codeSeq.getSequence());
codeSeqRet.setEnd(MAX_SEQ);
} else {
codeSeqRet.setStart(codeSeq.getSequence());
codeSeqRet.setEnd(codeSeq.getSequence() + step - 1);
}
codeSeq.setSequence(codeSeqRet.getEnd() + 1);
int ret = codeSeqMapper.updateCodeSeqByPrefix(codeSeq);
if (ret <= 0) {
log.info("update error,ret={},codeSeq={}", ret, codeSeq);
codeSeqRet.setResult(2);
return codeSeqRet;
}
return codeSeqRet;
}
java
public class CodeSeqRet {
private int prefix;
/**
* 0:正常 1:无可用 2:失败
*/
private Integer result;
private Integer start;
private Integer end;
}
java
public interface CodeSeqMapper {
List<Integer> selectPrefixByEnv(@Param("env") String env,@Param("maxSeq") Integer maxSeq);
CodeSeq selectCodeSeqByPrefix(@Param("env") String env, @Param("prefix") Integer prefix);
int updateCodeSeqByPrefix(CodeSeq codeSeq);
}
xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.toby.dynamic.data.source.db.dao.config.CodeSeqMapper">
<resultMap id="paramConfig" type="com.toby.dynamic.data.source.db.model.CodeSeq">
<result column="env" property="env" jdbcType="VARCHAR"/>
<result column="prefix" property="prefix" jdbcType="INTEGER"/>
<result column="seq" property="sequence" jdbcType="INTEGER"/>
</resultMap>
<select id="selectPrefixByEnv" resultType="java.lang.Integer">
select
`prefix`
from code_seq where `env`=#{env} and `seq` <= #{maxSeq};
</select>
<select id="selectCodeSeqByPrefix" resultMap="paramConfig">
select `env`,`prefix`,`seq` from code_seq where `env` = #{env} and `prefix` = #{prefix} for update;
</select>
<update id="updateCodeSeqByPrefix" parameterType="com.toby.dynamic.data.source.db.model.CodeSeq" >
update code_seq set `seq` = #{sequence} where `env` = #{env} and `prefix` = #{prefix};
</update>
</mapper>
最终TEST用例调用如下:
java
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
@Slf4j
public class CodeTest {
@Autowired
private CodeSeqService codeSeqService;
@Test
public void createCodeTestCase01() {
log.info("createCodeTestCase01 begin.");
CodeHandler codeHandler = new CodeHandler("code-create");
codeHandler.setCodeSeqService(codeSeqService);
codeHandler.init();
ExecutorService executorService = Executors.newFixedThreadPool(5, new ThreadFactory() {
private volatile int index = 0;
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "t" + (index++));
}
});
CountDownLatch countDownLatch = new CountDownLatch(5);
executorService.submit(new CreateCodeJob(codeHandler, 200, countDownLatch));
executorService.submit(new CreateCodeJob(codeHandler, 200, countDownLatch));
executorService.submit(new CreateCodeJob(codeHandler, 200, countDownLatch));
executorService.submit(new CreateCodeJob(codeHandler, 200, countDownLatch));
executorService.submit(new CreateCodeJob(codeHandler, 200, countDownLatch));
// LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(10));
try {
countDownLatch.await();
} catch (InterruptedException e) {
log.error("countDownLatch.await() error", e);
}
executorService.shutdown();
}
private static class CreateCodeJob implements Runnable {
private CodeHandler codeHandler;
private int times;
private CountDownLatch countDownLatch;
public CreateCodeJob(CodeHandler codeHandler, int times, CountDownLatch countDownLatch) {
this.codeHandler = codeHandler;
this.times = times;
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
while (times > 0) {
String code = codeHandler.getCode();
log.info("threadName:{},code={}", Thread.currentThread().getName(), code);
times--;
}
countDownLatch.countDown();
}
}
}