在项目开发中,经常遇到根据给定关键字生成系统唯一顺序号的场景,本文整理了两种不同的实现方式。
1. 通过数据库加锁方式生成顺序号
该方案主要通过对数据库中表记录的加锁读写来实现的,该表中的记录对应不同关键字的顺序号生成信息,并且,为了提高生成顺序号的效率,可以一次生成指定步长个数的顺序号并存入本地缓存中。
该方案首先需要在数据库建立用于生成顺序号的表SEQUENCE_NUMBER,表结构如表1所示:
表1 表结构
相应的,定义该表对应的Domain:
public class SequenceNumberDomain{ private keyName;//关键字名 private currentKey; //当前值 private maxKey; //最大值 private step; //步长 //省略get和set方法 }
其次,需要定义用于存储顺序号生成信息的本地缓存,本地缓存可以通过定义CouncurrentHashMap实现。
至此,所有的准备工作都已完成,可以进行顺序号的生成了,生成顺序号的主要伪代码如下:
ConcurrentHashMap<String, SequenceNumberDomain> localCache = new ConcurrentHashMap();
该方法的优点是可以一次生成step个顺序号存入缓存,减少了对数据库的访问次数,而且缓存中只存储了step个顺序号,即使缓存数据丢失,系统最多损失掉step个顺序号,不会导致生成重复的顺序号或者丢失太多顺序号。但是,该方案也具有缺点,首先步长难以确定,步长的设置和系统的并发量有关,若步长太短,且会频繁的访问数据库,降低生成顺序号的效率,若步长过长,则缓存丢失会损失较多顺序号;其次,如果系统是分布式部署,不同服务只能访问自己的数据库,若要生成指定关键字的全系统唯一顺序号,则可能需要通过rpc接口、分布式事务等方式等从表中分别取一定数量且不交叉的顺序号存入各自的本地缓存,实现较为复杂。
2. 通过redis生成顺序号
若系统部署了redis服务,则顺序号也可以借助redis的incr命令实现,incr命令是将key中储存的数字值加一,如果key不存在,key的值会先被初始化为0,然后再执行incr操作,而且incr命令为原子操作,不会产生并发问题。
该方案的伪代码如下:
//key 待生成顺序号的关键字,step 步长 pubilic long getSequenceNumber(Stirng key, int step){ //开启事务(代码省略) //从本地缓存中获取该key对应的SequenceNumberDomain,若本地缓存没有,则创建新的SequenceNumberDomain放入本地缓存。 SequenceNumberDomain sequenceNumberDomain= localCache.get(key); if(sequenceNumberDomain == null){ //selectForUpdate 通过 select * from SEQUENCE_NUMBER for update 从数据库查询表中当前key的信息; sequenceNumberDomain = SequenceNumberDAO.selectForUpdate(key); //sequenceNumberDomain为空,说明数据库中SEQUENCE_NUMBER没有这个关键字的记录,需要把该记录插入进去 if(sequenceNumberDomain==null){ sequenceNumberDomain = new SequenceNumberDomain(); sequenceNumberDomain.setKeyName(key); sequenceNumberDomain.setCurrentKey(step + 1); sequenceNumberDomain.setStep(step); SequenceNumberDAO.SequenceNumberDAO.insert(sequenceNumberDomain); //将sequenceNumberDomain设置好相关信息存入缓存,由于数据库中没有该关键字的记录,所以缓存中当前值为1,且最大值为step,相当于缓存中存储了从1到step的顺序号。 sequenceNumberDomain.setCurrentKey(1); sequenceNumberDomain.setMaxKey(step); sequenceNumberDomain.setStep(step); localCache.push(key, sequenceNumberDomain); //将生成的顺序号返回 return 1; }else { //如果数据库中有该关键字对应的记录,则根据该记录生成顺序号,同时更新该记录信息 long sequenceNumber = sequenceNumberDomain.getCurrentKey(); sequenceNumberDomain.setCurrentKey(sequenceNumber + step); sequenceNumberDomain.setStep(step); SequenceNumberDAO.SequenceNumberDAO.update(sequenceNumberDomain); //设置缓存中能获取到的顺序号的最大值 sequenceNumberDomain.setMaxKey(sequenceNumber + step - 1); localCache.push(key, sequenceNumberDomain); return sequenceNumber; } } else { //如果缓存中可以获取到该key对应的sequenceNumberDomain且需要的顺序号不超过缓存中的最大顺序号,则直接通过缓存生成顺序号,若需要的顺序号超过缓存中的最大顺序号,则需要从数据库获取该key对应的记录,根据数据库中的信息生成顺序号 if(sequenceNumberDomain.getCurrentKey()+1<=sequenceNumberDomain.getMaxKey()){ long sequenceNumber = sequenceNumberDomain.getCurrentKey()+1 ; sequenceNumberDomain.setCurrentKey( sequenceNumberDomain.getCurrentKey()+1); localCache.push(key,sequenceNumberDomain); }else{ sequenceNumberDomain = SequenceNumberDAO.selectForUpdate(key); long sequenceNumber = sequenceNumberDomain.getCurrentKey(); sequenceNumberDomain.setCurrentKey(sequenceNumber + step); sequenceNumberDomain.setStep(step); SequenceNumberDAO.SequenceNumberDAO.update(sequenceNumberDomain); //设置缓存中能获取到的顺序号的最大值 sequenceNumberDomain.setMaxKey(sequenceNumber + step - 1); localCache.push(key, sequenceNumberDomain); return sequenceNumber; } } //结束事务 代码省略 }
该方案实现简单,且多个微服务可以访问同一个redis服务,不需要通过rpc接口即可根据关键字生成全系统唯一的顺序号,而且由于redis存取速度非常快,所以即使每次只生成一个顺序号,该方案的效率也会非常高。但是该方案的缺点在于如果redis宕机,则数据可能由于没有及时备份而与磁盘上的数据不一致,导致redis重启后生成的顺序号重复。
作者:王欢