看了網(wǎng)上很多生成自增ID的策略搪桂,最終給出的都是雪花算法螃壤,leaf算法。但是卻沒(méi)有滿足咱們對(duì)于自定義生成規(guī)則的需求倡怎。
在業(yè)務(wù)上有一部分ID往往是有規(guī)則的,比如某個(gè)產(chǎn)品的訂單號(hào)往往是“產(chǎn)品標(biāo)志+時(shí)間+n位流水”贱枣,類似這樣的訂單規(guī)則监署,使用雪花算法是滿足不了業(yè)務(wù)需求的,所以我們得設(shè)計(jì)一套自己的自定義ID生成器纽哥。
“產(chǎn)品標(biāo)志+時(shí)間+n位流水”規(guī)則中钠乏,難點(diǎn)無(wú)非在于n位流水號(hào)的生成,因?yàn)檫@個(gè)流水號(hào)需要保證在多次請(qǐng)求中不會(huì)產(chǎn)生重復(fù)的訂單號(hào)春塌。
首先晓避,咱們根據(jù)業(yè)務(wù)需求,先制定對(duì)應(yīng)的規(guī)則表達(dá)式:
id:
generator:
expressions:
# 產(chǎn)品標(biāo)志
TEST:
# 表達(dá)式只壳,pid指的是產(chǎn)品標(biāo)志
exp: "$(pid)$(yearMonthDayHms)$(id:6:0)"
# 字段名:初始值:最大值:最小數(shù)量:擴(kuò)容數(shù)量:初始數(shù)量:增長(zhǎng)步長(zhǎng)
initFields: ["id"]
表達(dá)式對(duì)應(yīng)的實(shí)體類:
@Data
@Configuration
@ConfigurationProperties(prefix = "id.generator")
public class IDExpressionProperties {
private Map<String, SerialIdConfig> expressions;
@Data
public static class SerialIdConfig {
// 表達(dá)式
private String exp;
// 初始化字段
private String[] initFields;
}
}
通過(guò)spring解析獲得對(duì)應(yīng)的IDExpressionProperties實(shí)體類俏拱,拿到咱們自定義的配置
通過(guò)規(guī)則表達(dá)式可以看出,類似“$(pid)”這樣的表達(dá)式吼句,咱們可以抽象成接口自定義生成锅必。比如咱們定義一個(gè)VariableGenerator接口或者抽象類,遇到pid就從spring ioc容器中調(diào)用pidVariableGenerator這個(gè)Bean的生成方法獲取pid的值惕艳。遇到y(tǒng)earMonthDayHms就調(diào)用yearMonthDayHmsVariableGenerator這個(gè)Bean的生成方法獲取yearMonthDayHms指定的值搞隐。
@Configuration
public class SerialConfig {
@Autowired private IDExpressionProperties idExpressionProperties;
/**
** 咱們要?jiǎng)?chuàng)建一個(gè)ID工廠類驹愚,專門用來(lái)生成ID的類
** 使用方法:
** @Autowired
** private IDFactory idFactory;
** String id = idFactory.get("產(chǎn)品標(biāo)志");
*/
@Bean(initMethod = "init")
public IDFactory serialIdFactory() {
return new IDFactory(idExpressionProperties.getExpressions());
}
}
變量生成器
import org.apache.commons.lang3.StringUtils;
/**
* 變量生成器
* @className VariableGenerator
* @date: 2021/2/18 下午2:53
* @description:
*/
public abstract class VariableGenerator {
public static final String COLON = ":";
/**
* apply是生成目標(biāo)字符串的方法
*/
protected abstract String apply(ExpressionElement e, Expression expression);
/**
* apply的后置處理方法劣纲,默認(rèn)處理字符串不足的情況下逢捺,補(bǔ)足對(duì)應(yīng)的填充數(shù)據(jù)
*/
public String andThen(ExpressionElement e, Expression expression) {
String variableValue = apply(e, expression);
int count = e.getCount();
String fillStringValue = e.getFillStringValue();
if (StringUtils.isNotBlank(variableValue)) {
if (count > 0) {
variableValue = StringUtils.leftPad(variableValue, count, fillStringValue);
} else {
variableValue =
StringUtils.rightPad(variableValue, Math.abs(count), fillStringValue);
}
}
return variableValue;
}
}
ID生成器
- 初始化的時(shí)候根據(jù)表達(dá)式配置確定是哪些字段需要初始化,根據(jù)初始化字段調(diào)用指定的Bean執(zhí)行初始化
- 通過(guò)get方法傳入的參數(shù)key獲取指定的規(guī)則表達(dá)式癞季,根據(jù)指定的表達(dá)式調(diào)用對(duì)應(yīng)的生成器Bean實(shí)例蒸甜,調(diào)用指定的方法生成目標(biāo)值,最后拼接出最終的ID
public class IDFactory {
// 變量生成器
@Autowired(required = false)
private Map<String, VariableGenerator> variableGeneratorMap;
// 字段初始化生成器
@Autowired(required = false)
private Map<String, InitFieldGenerator> initFieldGeneratorMap;
// 構(gòu)造函數(shù)余佛,參數(shù)是生成規(guī)則
public IDFactory(Map<String, IDExpressionProperties.SerialIdConfig> expressionMap) {
this.expressionMap = expressionMap;
}
// 實(shí)例化后執(zhí)行
public void init() {
// 如果沒(méi)有規(guī)則表達(dá)式柠新,那么直接就結(jié)束
if (CollectionUtils.isEmpty(this.expressionMap)) {
return;
}
for (Map.Entry<String, IDExpressionProperties.SerialIdConfig> e :
this.expressionMap.entrySet()) {
String key = e.getKey();
// 規(guī)則表達(dá)式
IDExpressionProperties.SerialIdConfig config = e.getValue();
// 初始化字段參數(shù)
String[] initFields = config.getInitFields();
// 如果沒(méi)有初始化字段生成器,直接結(jié)束
if (CollectionUtils.isEmpty(initFieldGeneratorMap)) {
return;
}
// 根據(jù)初始化規(guī)則辉巡,執(zhí)行初始化操作
for (String initField : initFields) {
String fieldName = initField;
// 獲取初始化字段名稱
if (StringUtils.contains(initField, VariableGenerator.COLON)) {
fieldName = StringUtils.substringBefore(initField, VariableGenerator.COLON);
}
// 根據(jù)字段名稱獲取對(duì)應(yīng)的初始化生成器的Bean實(shí)例
InitFieldGenerator initFieldGenerator =
initFieldGeneratorMap.get(
fieldName + InitFieldGenerator.INIT_FIELD_GENERATOR);
if (Objects.nonNull(initFieldGenerator)) {
// 執(zhí)行字段初始化操作
initFieldGenerator.generator(key, initField);
}
}
}
}
/**
* 表達(dá)式
*
* <p>pid:expression格式
*/
private Map<String, IDExpressionProperties.SerialIdConfig> expressionMap;
/**
* 根據(jù)指定的key規(guī)則生成id
*
* @param key
* @return
*/
public String get(String key) {
// key為空直接拋異常
if (StringUtils.isBlank(key)) {
throw new IllegalArgumentException("無(wú)效的參數(shù)值:" + key);
}
// 獲取規(guī)則表達(dá)式
IDExpressionProperties.SerialIdConfig serialIdConfig = expressionMap.get(key);
// 表達(dá)式字符串
String expressionString = serialIdConfig.getExp();
// 為空直接拋異常
if (StringUtils.isBlank(expressionString)) {
throw new IllegalArgumentException("沒(méi)有找到對(duì)應(yīng)的表達(dá)式");
}
// 解析指定的表達(dá)式
Expression expression = parse(key, expressionString);
// 匹配得出最終結(jié)果
return matchExpression(expression);
}
// 生成器名稱后綴
private static final String VARIABLE_GENERATOR = "VariableGenerator";
// 循環(huán)遍歷表達(dá)式中所有的自定義變量恨憎,獲取指定Bean實(shí)例,執(zhí)行目標(biāo)方法后得出最終ID
private String matchExpression(Expression expression) {
// 獲取變量列表郊楣,例如pid憔恳,yearMonthDayHms等
List<ExpressionElement> elements = expression.getElements();
// 如果沒(méi)有任何變量,那么直接返回原表達(dá)式净蚤,說(shuō)明表達(dá)式是一個(gè)常量
if (CollectionUtils.isEmpty(elements)) {
return expression.getExpression();
}
// 獲取原表達(dá)式钥组,用來(lái)替換變量,生成最終的ID
String expressionString = expression.getExpression();
// 循環(huán)遍歷變量列表
for (ExpressionElement e : elements) {
// 拼接Bean的名稱
String beanName = e.getVariableName() + VARIABLE_GENERATOR;
// 從map中取出指定的Bean
VariableGenerator variableGenerator = variableGeneratorMap.get(beanName);
// 如果沒(méi)有取到今瀑,那么直接忽略程梦,說(shuō)明沒(méi)有創(chuàng)建該表達(dá)式對(duì)應(yīng)的生成器
if (Objects.isNull(variableGenerator)) {
continue;
}
// 調(diào)用目標(biāo)方法生成字符串
String variableValue = variableGenerator.andThen(e, expression);
// 如果不為空,就替換掉原表達(dá)式中的變量橘荠;就是用具體生成的值替換變量表達(dá)式
// “$(pid)$(yearMonthDayHms)$(id:6:0)”會(huì)被替換成“TEST$(yearMonthDayHms)$(id:6:0)”
if (StringUtils.isNotBlank(variableValue)) {
expressionString =
StringUtils.replace(expressionString, e.getOriginString(), variableValue);
}
}
// 返回最終結(jié)果
return expressionString;
}
// 正則表達(dá)式屿附,用來(lái)解析$(pid)$(yearMonthDayHms)$(id:6:0)表達(dá)式
private static final Pattern EXPRESSION_PATTERN = Pattern.compile("\\$\\((.+?)\\)");
private static final Map<String, Expression> EXPRESSION_MAP = Maps.newConcurrentMap();
/**
* 解析$(pid)$(yearMonthDayHms)$(id:6:0)
*
* @param expressionString
* @return
*/
private Expression parse(String key, String expressionString) {
// 檢查一下緩存中是否有解析號(hào)的表達(dá)式
Expression expression = EXPRESSION_MAP.get(key);
// 緩存不為空的話,直接返回
if (Objects.nonNull(expression)) {
return expression;
}
// 否則哥童,直接解析
synchronized (EXPRESSION_MAP) {
// 雙重檢查挺份,避免重復(fù)解析
expression = EXPRESSION_MAP.get(key);
if (Objects.nonNull(expression)) {
return expression;
}
// 生成表達(dá)式對(duì)象
expression = new Expression();
expression.setKey(key);
expression.setExpression(expressionString);
List<ExpressionElement> expressionElements = Lists.newArrayList();
Matcher matcher = EXPRESSION_PATTERN.matcher(expressionString);
while (matcher.find()) {
// 正則表達(dá)式,找出$()變量表達(dá)式贮懈,類似id:6:0
String expressionVariable = matcher.group(1);
// 表達(dá)式切割匀泊,分離出冒號(hào)分隔的參數(shù)
String[] expressionVariables =
StringUtils.splitByWholeSeparatorPreserveAllTokens(
expressionVariable, VariableGenerator.COLON);
ExpressionElement expre = new ExpressionElement();
// 變量名稱id
expre.setVariableName(expressionVariables[0]);
// 原生表達(dá)式$(id:6:0),便于后面直接替換
expre.setOriginString(matcher.group());
if (expressionVariables.length > 1) {
// 獲取填充的最終長(zhǎng)度
expre.setCount(CastUtil.castInt(expressionVariables[1]));
}
if (expressionVariables.length > 2) {
// 獲取填充值
expre.setFillStringValue(expressionVariables[2]);
}
expressionElements.add(expre);
}
expression.setElements(expressionElements);
// 放入本地緩存
EXPRESSION_MAP.put(key, expression);
}
// 返回解析出來(lái)的表達(dá)式
return expression;
}
}
import lombok.Data;
import java.util.List;
/**
* @className Expression
* @date: 2021/2/18 下午2:53
* @description: 解析$(pid)$(year)$(month)$(day)$(id:6:0)這種類型的表達(dá)式
*/
@Data
public class Expression {
/** pid */
private String key;
/** 表達(dá)式 */
private String expression;
/** 解析結(jié)果 */
private List<ExpressionElement> elements;
}
/**
* @author zouwei
* @className ExpressionElement
* @date: 2021/2/18 下午2:56
* @description: 解析${id:6:0}這種類型的標(biāo)記
*/
@Data
public class ExpressionElement {
// 原生變量表達(dá)式
private String originString;
// 變量名稱
private String variableName;
// 總長(zhǎng)度
private int count;
// 填充值朵你,默認(rèn)是空字符
private String fillStringValue = StringUtils.SPACE;
}
初始化字段生成器
public abstract class InitFieldGenerator {
public static final String INIT_FIELD_GENERATOR = "InitFieldGenerator";
// 執(zhí)行初始化操作
public abstract String generator(String key, String initField);
}
以上代碼咱們已經(jīng)把整體的初始化各聘、ID生成邏輯全部搞定,剩下的就是需要把對(duì)應(yīng)的接口填充完畢就行撬呢。
針對(duì)表達(dá)式
$(pid)$(yearMonthDayHms)$(id:6:0)
咱們分別需要實(shí)現(xiàn)pidVariableGenerator伦吠、yearMonthDayHmsVariableGenerator、idVariableGenerator
@Bean
public VariableGenerator pidVariableGenerator() {
return new VariableGenerator() {
@Override
public String apply(ExpressionElement e, Expression expression) {
return expression.getKey();
}
};
}
private static final String YEAR_MONTH_DAY_HOUR_MINUTE_SECOND_FORMAT = "yyyyMMddHHmmss";
@Bean
public VariableGenerator yearMonthDayHmsVariableGenerator() {
return new VariableGenerator() {
@Override
public String apply(ExpressionElement e, Expression expression) {
return DateUtil.format(new DateTime(), YEAR_MONTH_DAY_HOUR_MINUTE_SECOND_FORMAT);
}
};
}
因?yàn)樵蹅兊淖栽鰅d是使用的redis的lua腳本實(shí)現(xiàn)的,所以會(huì)依賴redis毛仪。利用了redis執(zhí)行l(wèi)ua腳本的原子性搁嗓。
@Bean
public SerialIDVariableGenerator idVariableGenerator() {
return new SerialIDVariableGenerator();
}
public class SerialIDVariableGenerator extends VariableGenerator {
@Autowired private RedisTemplate redisTemplate;
private InitParams initParams;
// 構(gòu)造函數(shù)
public void initParams(String key, String initFields) {
this.initParams = parse(key, initFields);
}
/**
* 解析表達(dá)式 字段名:初始值:最大值:最小數(shù)量:擴(kuò)容數(shù)量:初始數(shù)量:增長(zhǎng)步長(zhǎng)
*
* @param initField
*/
private InitParams parse(String key, String initField) {
InitParams initParams = new InitParams();
if (StringUtils.contains(initField, COLON)) {
String[] params = StringUtils.splitByWholeSeparatorPreserveAllTokens(initField, COLON);
initParams.setFieldName(key + COLON + params[0]);
initParams.setField(params);
} else {
initParams.setFieldName(key + COLON + initField);
initParams.updateFields();
}
return initParams;
}
// 執(zhí)行l(wèi)ua腳本,生成對(duì)應(yīng)的自增id
public String generate() {
String fieldName = this.initParams.getFieldName();
return executeLua(
fieldName,
initParams.getInitValue(),
initParams.getMaxValue(),
initParams.getMinCount(),
initParams.getInitCount(),
initParams.getExpansionStep(),
initParams.getIncrStep());
}
// 執(zhí)行生成函數(shù)
@Override
protected String apply(ExpressionElement e, Expression expression) {
return generate();
}
// 執(zhí)行l(wèi)ua腳本
private String executeLua(
String key,
int initValue,
int maxValue,
int minCount,
int initCount,
int expansionStep,
int incrStep) {
// 執(zhí)行l(wèi)ua腳本
DefaultRedisScript<String> defaultRedisScript = new DefaultRedisScript<>();
defaultRedisScript.setResultType(String.class);
defaultRedisScript.setScriptText(LUA_SCRIPT);
RedisSerializer<String> serializer = redisTemplate.getStringSerializer();
String result =
CastUtil.castString(
redisTemplate.execute(
defaultRedisScript,
serializer,
serializer,
Lists.newArrayList(key),
CastUtil.castString(initValue),
CastUtil.castString(maxValue),
CastUtil.castString(minCount),
CastUtil.castString(initCount),
CastUtil.castString(expansionStep),
CastUtil.castString(incrStep)));
return result;
}
@Data
private static class InitParams {
/** 默認(rèn)初始值 */
private static final int DEFAULT_INIT_VALUE = 1;
/** 默認(rèn)最大值 */
private static final int DEFAULT_MAX_VALUE = 9999;
/** 默認(rèn)最小數(shù)量 */
private static final int DEFAULT_MIN_COUNT = 30;
/** 默認(rèn)初始數(shù)量 */
private static final int DEFAULT_INIT_COUNT = 100;
/** 默認(rèn)擴(kuò)容數(shù)量 */
private static final int DEFAULT_EXPANSION_STEP = 50;
/** 默認(rèn)自增步長(zhǎng) */
private static final int DEFAULT_INCR_STEP = 1;
private final int[] params = {
0,
DEFAULT_INIT_VALUE,
DEFAULT_MAX_VALUE,
DEFAULT_MIN_COUNT,
DEFAULT_EXPANSION_STEP,
DEFAULT_INIT_COUNT,
DEFAULT_INCR_STEP
};
/** 字段名稱箱靴,其實(shí)就是key */
private String fieldName;
/** 初始值 */
private int initValue;
/** 最大值 */
private int maxValue;
/** 最小數(shù)量 */
private int minCount;
/** 擴(kuò)容步長(zhǎng) */
private int expansionStep;
/** 初始數(shù)量 */
private int initCount;
/** 自增步長(zhǎng) */
private int incrStep;
public void setField(Object[] objects) {
if (ArrayUtils.isEmpty(objects) || ArrayUtils.getLength(objects) < 2) {
return;
}
for (int i = 1; i < objects.length; i++) {
Object obj = objects[i];
params[i] = CastUtil.castInt(obj);
}
updateFields();
}
public void updateFields() {
this.initValue = params[1];
this.maxValue = params[2];
this.minCount = params[3];
this.expansionStep = params[4];
this.initCount = params[5];
this.incrStep = params[6];
}
}
// 該腳本的執(zhí)行邏輯
// 在redis中生成一個(gè)隊(duì)列腺逛,指定初始化長(zhǎng)度,第一個(gè)初始值衡怀,最大值棍矛,隊(duì)列最小數(shù)量,每次擴(kuò)容的數(shù)量抛杨,自增的步長(zhǎng)
// 1.如果隊(duì)列不存在够委,就初始化隊(duì)列,按照給定的初始化長(zhǎng)度怖现,初始值茁帽,自增步長(zhǎng),最大值等參數(shù)創(chuàng)建一個(gè)隊(duì)列
// 2.如果隊(duì)列中值的數(shù)量超過(guò)隊(duì)列最小數(shù)量屈嗤,那么直接pop出一個(gè)值
// 3.如果小于最小數(shù)量潘拨,那么直接循環(huán)生成指定步長(zhǎng)的自增ID
// 4.最終會(huì)pop出第一個(gè)數(shù)值
// 5.如果是初始化的話,會(huì)返回success饶号,否則就直接pop出第一個(gè)ID
private static final String LUA_SCRIPT =
"local key=KEYS[1]\nlocal initValue=tonumber(ARGV[1])\nlocal maxValue=tonumber(ARGV[2])\nlocal minCount=tonumber(ARGV[3])\nlocal initCount=tonumber(ARGV[4])\nlocal expansionStep=tonumber(ARGV[5])\nlocal incrStep=tonumber(ARGV[6])\nlocal len=redis.call('llen',key)\nlocal isInit=true\nlocal loop=initCount\nlocal nextValue=initValue\nif len>minCount\nthen\nreturn redis.call('lpop',key)\nend\nif len>0\nthen\nisInit=false\nloop=len+expansionStep\nnextValue=tonumber(redis.call('rpop',key))\nend\nwhile(len<loop)\ndo\nif nextValue>maxValue\nthen\nnextValue=initValue\nend\nredis.call('rpush',key,nextValue)\nnextValue=nextValue+incrStep\nlen=len+1\nend\nif isInit\nthen\nreturn 'success'\nend\nreturn redis.call('lpop',key)";
}
根據(jù)配置中的初始化字段的配置規(guī)則铁追,咱們還需要一個(gè)idInitFieldGenerator初始化字段生成器
@Bean("idInitFieldGenerator")
public SerialIdInitFieldGenerator serialIdInitFieldGenerator() {
return new SerialIdInitFieldGenerator(idVariableGenerator());
}
public class SerialIdInitFieldGenerator extends InitFieldGenerator {
private SerialIDVariableGenerator serialIDVariableGenerator;
public SerialIdInitFieldGenerator(SerialIDVariableGenerator serialIDVariableGenerator) {
this.serialIDVariableGenerator = serialIDVariableGenerator;
}
// 利用了SerialIDVariableGenerator變量生成器的方法初始化
@Override
public String generator(String key, String initField) {
serialIDVariableGenerator.initParams(key, initField);
return serialIDVariableGenerator.generate();
}
}
總結(jié)
1.核心生成邏輯還是利用了redis執(zhí)行l(wèi)ua腳本的原子性
2.把表達(dá)式的生成邏輯拆分到具體的接口實(shí)現(xiàn)中去,方便規(guī)則的自定義擴(kuò)展
目前粗略測(cè)試下來(lái)茫船,線程并發(fā)的情況下大概1000個(gè)/s的生成速率琅束。還有比較大的優(yōu)化空間。