基于Mysql的Sequence实现方法
团队更换新框架。新的业务全部使用新的框架,甚至是新的数据库--Mysql。
这边之前一直是使用oracle,各种订单号、流水号、批次号啥的,都是直接使用oracle的sequence提供的数字序列号。现在数据库更换成Mysql了,显然以前的老方法不能适用了。
需要新写一个:
•分布式场景使用
•满足一定的并发要求
找了一些相关的资料,发现mysql这方面的实现,原理都是一条数据库记录,不断update它的值。然后大部分的实现方案,都用到了函数。
贴一下网上的代码:
基于mysql函数实现
表结构
CREATETABLE`t_sequence`( `sequence_name`varchar(64)CHARACTERSETutf8COLLATEutf8_general_ciNOTNULLCOMMENT'序列名称', `value`int(11)NULLDEFAULTNULLCOMMENT'当前值', PRIMARYKEY(`sequence_name`) ) ENGINE=InnoDB DEFAULTCHARACTERSET=utf8COLLATE=utf8_general_ci ROW_FORMAT=COMPACT ;
获取下一个值
CREATEDEFINER=`root`@`localhost`FUNCTION`nextval`(sequence_namevarchar(64)) RETURNSint(11) BEGIN declarecurrentinteger; setcurrent=0; updatet_sequencetsett.value=t.value+1wheret.sequence_name=sequence_name; selectt.valueintocurrentfromt_sequencetwheret.sequence_name=sequence_name; returncurrent; end;
并发场景有可能会出问题,虽然可以在业务层加锁,但分布式场景就无法保证了,然后效率应该也不会高。
自己实现一个,java版
原理:
•读取一条记录,缓存一个数据段,如:0-100,将记录的当前值从0修改为100
•数据库乐观锁更新,允许重试
•读取数据从缓存中读取,用完再读取数据库
不废话,上代码:
基于java实现
表结构
每次update,都是将SEQ_VALUE设置为SEQ_VALUE+STEP
CREATETABLE`t_pub_sequence`( `SEQ_NAME`varchar(128)CHARACTERSETutf8NOTNULLCOMMENT'序列名称', `SEQ_VALUE`bigint(20)NOTNULLCOMMENT'目前序列值', `MIN_VALUE`bigint(20)NOTNULLCOMMENT'最小值', `MAX_VALUE`bigint(20)NOTNULLCOMMENT'最大值', `STEP`bigint(20)NOTNULLCOMMENT'每次取值的数量', `TM_CREATE`datetimeNOTNULLCOMMENT'创建时间', `TM_SMP`datetimeNOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMPCOMMENT'修改时间', PRIMARYKEY(`SEQ_NAME`) )ENGINE=InnoDBDEFAULTCHARSET=utf8mb4COMMENT='流水号生成表';
sequence接口
/** * *@authorcoderzl *@TitleMysqlSequence *@Description基于mysql数据库实现的序列 *@date2017/6/623:03 */ publicinterfaceMysqlSequence{ /** **获取指定sequence的序列号 *
*@paramseqNamesequence名 *@returnString序列号 */ publicStringnextVal(StringseqName); }
序列区间
用于本地缓存一段序列,从min到max区间
/** * * *@authorcoderzl *@TitleSequenceRange *@Description序列区间,用于缓存序列 *@date2017/6/622:58 */ @Data publicclassSequenceRange{ privatefinallongmin; privatefinallongmax; /***/ privatefinalAtomicLongvalue; /**是否超限*/ privatevolatilebooleanover=false; /** *构造. * *@parammin *@parammax */ publicSequenceRange(longmin,longmax){ this.min=min; this.max=max; this.value=newAtomicLong(min); } /** *Getsandincrement
* *@return */ publiclonggetAndIncrement(){ longcurrentValue=value.getAndIncrement(); if(currentValue>max){ over=true; return-1; } returncurrentValue; } }
BO
对应数据库记录
@Data publicclassMysqlSequenceBo{ /** *seq名 */ privateStringseqName; /** *当前值 */ privateLongseqValue; /** *最小值 */ privateLongminValue; /** *最大值 */ privateLongmaxValue; /** *每次取值的数量 */ privateLongstep; /***/ privateDatetmCreate; /***/ privateDatetmSmp; publicbooleanvalidate(){ //一些简单的校验。如当前值必须在最大最小值之间。step值不能大于max与min的差 if(StringUtil.isBlank(seqName)||minValue<0||maxValue<=0||step<=0||minValue>=maxValue||maxValue-minValue<=step||seqValuemaxValue){ returnfalse; } returntrue; } }
DAO
增删改查,其实就用到了改和查
publicinterfaceMysqlSequenceDAO{ /** * */ publicintcreateSequence(MysqlSequenceBobo); publicintupdSequence(@Param("seqName")StringseqName,@Param("oldValue")longoldValue,@Param("newValue")longnewValue); publicintdelSequence(@Param("seqName")StringseqName); publicMysqlSequenceBogetSequence(@Param("seqName")StringseqName); publicListgetAll(); }
Mapper
deletefromt_pub_sequence whereSEQ_NAME=#{seqName,jdbcType=VARCHAR} insertintot_pub_sequence(SEQ_NAME,SEQ_VALUE,MIN_VALUE,MAX_VALUE,STEP,TM_CREATE) values(#{seqName,jdbcType=VARCHAR},#{seqValue,jdbcType=BIGINT}, #{minValue,jdbcType=BIGINT},#{maxValue,jdbcType=BIGINT},#{step,jdbcType=BIGINT}, now()) updatet_pub_sequence setSEQ_VALUE=#{newValue,jdbcType=BIGINT} whereSEQ_NAME=#{seqName,jdbcType=VARCHAR}andSEQ_VALUE=#{oldValue,jdbcType=BIGINT} selectSEQ_NAME,SEQ_VALUE,MIN_VALUE,MAX_VALUE,STEP fromt_pub_sequence selectSEQ_NAME,SEQ_VALUE,MIN_VALUE,MAX_VALUE,STEP fromt_pub_sequence whereSEQ_NAME=#{seqName,jdbcType=VARCHAR}
接口实现
@Repository("mysqlSequence") publicclassMysqlSequenceImplimplementsMysqlSequence{ @Autowired privateMysqlSequenceFactorymysqlSequenceFactory; /** **获取指定sequence的序列号 *
* *@paramseqNamesequence名 *@returnString序列号 *@authorcoderzl */ @Override publicStringnextVal(StringseqName){ returnObjects.toString(mysqlSequenceFactory.getNextVal(seqName)); } }
工厂
工厂只做了两件事
•服务启动的时候,初始化数据库中所有sequence【完成序列区间缓存】
•获取sequence的下一个值
@Component publicclassMysqlSequenceFactory{ privatefinalLocklock=newReentrantLock(); /***/ privateMapholderMap=newConcurrentHashMap<>(); @Autowired privateMysqlSequenceDAOmsqlSequenceDAO; /**单个sequence初始化乐观锁更新失败重试次数*/ @Value("${seq.init.retry:5}") privateintinitRetryNum; /**单个sequence更新序列区间乐观锁更新失败重试次数*/ @Value("${seq.get.retry:20}") privateintgetRetryNum; @PostConstruct privatevoidinit(){ //初始化所有sequence initAll(); } /** * 加载表中所有sequence,完成初始化
*@returnvoid *@authorcoderzl */ privatevoidinitAll(){ try{ lock.lock(); ListboList=msqlSequenceDAO.getAll(); if(boList==null){ thrownewIllegalArgumentException("ThesequenceRecordisnull!"); } for(MysqlSequenceBobo:boList){ MysqlSequenceHolderholder=newMysqlSequenceHolder(msqlSequenceDAO,bo,initRetryNum,getRetryNum); holder.init(); holderMap.put(bo.getSeqName(),holder); } }finally{ lock.unlock(); } } /** * *@paramseqName *@returnlong *@authorcoderzl */ publiclonggetNextVal(StringseqName){ MysqlSequenceHolderholder=holderMap.get(seqName); if(holder==null){ try{ lock.lock(); holder=holderMap.get(seqName); if(holder!=null){ returnholder.getNextVal(); } MysqlSequenceBobo=msqlSequenceDAO.getSequence(seqName); holder=newMysqlSequenceHolder(msqlSequenceDAO,bo,initRetryNum,getRetryNum); holder.init(); holderMap.put(seqName,holder); }finally{ lock.unlock(); } } returnholder.getNextVal(); } }
单一sequence的Holder
•init()初始化其中包括参数校验,数据库记录更新,创建序列区间
•getNextVal()获取下一个值
publicclassMysqlSequenceHolder{ privatefinalLocklock=newReentrantLock(); /**seqName*/ privateStringseqName; /**sequenceDao*/ privateMysqlSequenceDAOsequenceDAO; privateMysqlSequenceBosequenceBo; /***/ privateSequenceRangesequenceRange; /**是否初始化*/ privatevolatilebooleanisInitialize=false; /**sequence初始化重试次数*/ privateintinitRetryNum; /**sequence获取重试次数*/ privateintgetRetryNum; /** *构造方法
*@TitleMysqlSequenceHolder *@paramsequenceDAO *@paramsequenceBo *@paraminitRetryNum初始化时,数据库更新失败后重试次数 *@paramgetRetryNum获取nextVal时,数据库更新失败后重试次数 *@return *@authorcoderzl */ publicMysqlSequenceHolder(MysqlSequenceDAOsequenceDAO,MysqlSequenceBosequenceBo,intinitRetryNum,intgetRetryNum){ this.sequenceDAO=sequenceDAO; this.sequenceBo=sequenceBo; this.initRetryNum=initRetryNum; this.getRetryNum=getRetryNum; if(sequenceBo!=null) this.seqName=sequenceBo.getSeqName(); } /** *初始化
*@Titleinit *@param *@returnvoid *@authorcoderzl */ publicvoidinit(){ if(isInitialize==true){ thrownewSequenceException("["+seqName+"]theMysqlSequenceHolderhasinited"); } if(sequenceDAO==null){ thrownewSequenceException("["+seqName+"]thesequenceDaoisnull"); } if(seqName==null||seqName.trim().length()==0){ thrownewSequenceException("["+seqName+"]thesequenceNameisnull"); } if(sequenceBo==null){ thrownewSequenceException("["+seqName+"]thesequenceBoisnull"); } if(!sequenceBo.validate()){ thrownewSequenceException("["+seqName+"]thesequenceBovalidatefail.BO:"+sequenceBo); } //初始化该sequence try{ initSequenceRecord(sequenceBo); }catch(SequenceExceptione){ throwe; } isInitialize=true; } /** *获取下一个序列号
*@TitlegetNextVal *@param *@returnlong *@authorcoderzl */ publiclonggetNextVal(){ if(isInitialize==false){ thrownewSequenceException("["+seqName+"]theMysqlSequenceHoldernotinited"); } if(sequenceRange==null){ thrownewSequenceException("["+seqName+"]thesequenceRangeisnull"); } longcurValue=sequenceRange.getAndIncrement(); if(curValue==-1){ try{ lock.lock(); curValue=sequenceRange.getAndIncrement(); if(curValue!=-1){ returncurValue; } sequenceRange=retryRange(); curValue=sequenceRange.getAndIncrement(); }finally{ lock.unlock(); } } returncurValue; } /** *初始化当前这条记录
*@TitleinitSequenceRecord *@Description *@paramsequenceBo *@returnvoid *@authorcoderzl */ privatevoidinitSequenceRecord(MysqlSequenceBosequenceBo){ //在限定次数内,乐观锁更新数据库记录 for(inti=1;i0){ sequenceRange=newSequenceRange(curBo.getSeqValue(),newValue-1); curBo.setSeqValue(newValue); this.sequenceBo=curBo; return; }else{ continue; } } //限定次数内,更新失败,抛出异常 thrownewSequenceException("["+seqName+"]sequenceBoupdateerror"); } /** * 检查新值是否合法新的当前值是否在最大最小值之间
*@paramcurValue *@paramcurBo *@returnboolean *@authorcoderzl */ privatebooleancheckCurrentValue(longcurValue,MysqlSequenceBocurBo){ if(curValue>curBo.getMinValue()&&curValue<=curBo.getMaxValue()){ returntrue; } returnfalse; } /** *重置sequence当前值:当前sequence达到最大值时,重新从最小值开始
*@TitleresetCurrentValue *@paramcurBo *@returnlong *@authorcoderzl */ privatelongresetCurrentValue(MysqlSequenceBocurBo){ returncurBo.getMinValue(); } /** *缓存区间使用完毕时,重新读取数据库记录,缓存新序列段
*@TitleretryRange *@paramSequenceRange *@authorcoderzl */ privateSequenceRangeretryRange(){ for(inti=1;i0){ sequenceRange=newSequenceRange(curBo.getSeqValue(),newValue-1); curBo.setSeqValue(newValue); this.sequenceBo=curBo; returnsequenceRange; }else{ continue; } } thrownewSequenceException("["+seqName+"]sequenceBoupdateerror"); } }
总结
•当服务重启或异常的时候,会丢失当前服务所缓存且未用完的序列
•分布式场景,多个服务同时初始化,或者重新获取sequence时,乐观锁会保证彼此不冲突。A服务获取0-99,B服务会获取100-199,以此类推
•当该sequence获取较为频繁时,增大step值,能提升性能。但同时服务异常时,损失的序列也较多
•修改数据库里sequence的一些属性值,比如step,max等,再下一次从数据库获取时,会启用新的参数
•sequence只是提供了有限个序列号(最多max-min个),达到max后,会循环从头开始。
•由于sequence会循环,所以达到max后,再获取,就不会唯一。建议使用sequence来做业务流水号时,拼接时间。如:20170612235101+序列号
业务id拼接方法
@Service publicclassJrnGeneratorService{ privatestaticfinalStringSEQ_NAME="T_SEQ_TEST"; /**sequence服务*/ @Autowired privateMySqlSequencemySqlSequence; publicStringgenerateJrn(){ try{ Stringsequence=mySqlSequence.getNextValue(SEQ_NAME); sequence=leftPadding(sequence,8); Calendarcalendar=Calendar.getInstance(); SimpleDateFormatsDateFormat=newSimpleDateFormat("yyyyMMddHHmmss"); Stringnowdate=sDateFormat.format(calendar.getTime()); nowdate.substring(4,nowdate.length()); Stringjrn=nowdate+sequence+RandomUtil.getFixedLengthRandom(6);//10位时间+8位序列+6位随机数=24位流水号 returnjrn; }catch(Exceptione){ //TODO } } privateStringleftPadding(Stringseq,intlen){ Stringres=""; Stringstr=""; if(seq.length()以上这篇基于Mysql的Sequence实现方法就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持毛票票。