分布式单调递增ID生成主键(序列号自增id(当前日期+序列号))

版权声明:转载请注明出处! https://blog.csdn.net/Dream_bin/article/details/88620737

 1. 问题描述

           接口介绍:为实现某个功能。有个批量入库接口支持批量数据入库。接口功能:每条数据要是在库中则更新原有数据。要是不在库中则插入。接口维护一属性updateVerstion,该属性要求根据接收的数据生成单调递增的自增ID(要求为数字)。该接口请求量较大,需要考虑性能问题。。。

           问题:每次接收批量数据入库,updateVerstion属性生成方式MongoDB插入时间戳精确到毫秒级别(14位时间戳)。在测试服务器测试发现每批入库的数据大约有十几个,二十几 updateVerstion值一样。这就说明,接口批量入库在一毫秒内能够入库十几条数据。

            以下是当时入库时的设计代码(简写版本):

 // 1. 将对象转换为Document
 private Document convertToBson(LayoutFile layoutFile) {
        Document newdoc = new Document();
        newdoc.put("fileId", layoutFile.getFileId());
        // 略过每个属性
        return new Document("$set", newdoc)
                .append("$setOnInsert", new 
             Document("createtime",layoutFile.getCreatetime())); //插入数据时生成的creatime   
        .append("$currentDate",new BasicDBObject("updateVer",true));//mongo维护的updateVer    
 }
 // 2. 入库代码
UpdateOptions updateOptions = new UpdateOptions().upsert(true);
        List<UpdateOneModel<LayoutFile>> updates = new ArrayList<UpdateOneModel<LayoutFile>>();
        for (LayoutFile layoutFile : layoutFiles) {
            Bson filter = new Document("fileId", layoutFile.getFileId());
            if (log.isDebugEnabled()) {
                log.info("filedId:" + layoutFile.getFileId());
            }
            UpdateOneModel<LayoutFile> updateOneModel = new UpdateOneModel(filter, convertToBson(layoutFile), updateOptions);
            updates.add(updateOneModel);
        }
        try {
            bulkWriteResult = collection.bulkWrite(session, updates, new BulkWriteOptions().ordered(false));
        } catch (Exception e) {
            log.error("insetLayoutFileList  error message {}", e.getMessage(), e);
            throw e;
        }

该方式是依靠MongoDB入库时间生成的updateVerstion。避免了在分布式的情况下生成重复的版本。但是由于性能的原因,导致updateVerstion有重复的,每毫秒内可以支持入库好几十条。

解决方案:根据业务场景选择了方案。

      (1) updateVersion是有14位数字组成前八位是当前日期例如(20190217)后六位是redis自增主键(key),当主键不足六位时,用零在前面补足六位。
      (2)Key 设置过期时间,每天凌晨自动过期。
      (3)根据某个属性分类(例如本次开发的分库,每个库单独使用一个单调递增的主键  如内网,外网等)

实现方式:

在上述代码中去掉:

.append("$currentDate",new BasicDBObject("updateVer",true));//mongo维护的updateVe

构建Docment的时候调用jedisUtil.getUpdateVerson(key) 生成为了唯一的单调递增的主键。

@Slf4j
@Component
public class JedisUtil {

    @Resource
    private JedisPool jedisPool;

    private final static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");

    public String getUpdateVerson(String key) {
        String updateversion = "";
        try {
            long incr = setIncr(key, Integer.valueOf(DateUtil.getSecsToEndOfCurrentDay().toString()));
            updateversion = getUPdateVersion(incr);
        } catch (IOException e) {
            e.printStackTrace();
            log.error("getUpdateVerson error IOException", e.getMessage());
        } catch (ParseException e) {
            log.error("getUpdateVerson error ParseException", e.getMessage());
        }
        return updateversion;

    }

    /**
     * 序号时当前时间+四位当掉递增数字(数字不足6位用零筹齐)
     *
     * @param incr
     * @return
     */
    private String getUPdateVersion(long incr) {
        String curDate = dateFormat.format(new Date());

        int length = String.valueOf(incr).length();
        String prefix = "";
        for (int j = 0; j < 6 - length; j++) {
            prefix += "0";
        }
        return curDate + prefix + incr;
    }

    /**
     * 对某个键的值自增
     *
     * @param key          键
     * @param cacheSeconds 超时时间,0为不超时
     * @return
     */
    private long setIncr(String key, int cacheSeconds) throws IOException {
        long result = 0;
        XyJedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            // 如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作,且将key的有效时间设置为长期有效.
            result = jedis.incr(key);
            if (cacheSeconds != 0 || "1".equals(result)) {
                jedis.expire(key, cacheSeconds);
            }
            log.debug("setIncr " + key + " = " + result);
        } catch (Exception e) {
            log.error("setIncr Errot" + key + "result", e.getMessage());
        } finally {
            jedis.close();
        }
        return result;
    }

}

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;

/**
 * 日期检测工具
 * 判断参数的格式是否为“yyyyMMddHHmm”格式的合法日期字符串
 */
public class DateUtil {
    public static boolean isValidDate(String str) {
        boolean convertSuccess = true;
        SimpleDateFormat format = new SimpleDateFormat("yyyyMMddHHmmss");
        try {
            format.setLenient(false);
            format.parse(str);
        } catch (ParseException e) {
            // 如果throw java.text.ParseException或者NullPointerException,就说明格式不对
            convertSuccess = false;
        }
        return convertSuccess;
    }

    /**
     * 获取第二天凌晨0点毫秒数
     * @return
     */
    public static Date nextDayFirstDate() throws ParseException {
        Calendar cal = Calendar.getInstance();
        cal.setTime(new Date());
        cal.add(Calendar.DAY_OF_YEAR, 1);
        cal.set(Calendar.HOUR_OF_DAY, 00);
        cal.set(Calendar.MINUTE, 0);
        cal.set(Calendar.SECOND, 0);
        return cal.getTime();
    }

    /**
     * 获取当前时间到明天凌晨0点相差秒数
     * @return
     */
    public static Long getSecsToEndOfCurrentDay() throws ParseException {

        Long secsOfNextDay  = nextDayFirstDate().getTime();
        //将当前时间转为毫秒数
        Long secsOfCurrentTime = new Date().getTime();
        //将时间转为秒数
        Long distance = (secsOfNextDay - secsOfCurrentTime)/1000;
        if (distance > 0 && distance != null){
            return distance;
        }

        return new Long(0);

    }


}

通过这样updateVersion 就可以生成了。

分析/总结:

1. 其中是利用redis 单调递增属性,规避分布式的情况下生成的重复的ID主键的问题。

2. 设置日期(年月日)为开始updateVersion,以及数据凌晨过期。

    为了应对Redis主键万一某天撑爆。或者是Redis宏碁的情况下主键数据错误导致大批量数据updateVersion重复。每天key过期使得数据可靠性提升。而且updateVersion前八位都是入库的当前时间有利于数据帅选。

    预留六位redis自增主键也有一定的风险。风险在于当每天某个库入库条件大于一百万时,updateVersion 会超过14位数的限制,导致出问题。

3. 性能损耗点。比量入库的性能损耗点之一就是每次构建入库信息时,每条数据需要需要设置过期时间秒数。

扩展: 

     redis生成自增主键还有另一种方式实现,不过由于一些原因没有使用。

一下是简洁版的实现方式(ps : 以下代码参照别的网上资源改写):

/**
*自增主键工具
*/
public class IncUtis {
 
	@Autowired
	RedisTemplate<String, Object> redisTemplate;
	
	public String getInceId() {
	    SimpleDateFormat sdf=new SimpleDateFormat("yyyyMMdd");
	    Date date=new Date();
	    String formatDate=sdf.format(date);
	    String key=formatDate+"key";
	    Long incr = getIncr(key, getCurrent2TodayEndMillisTime());
	    if(incr==0) {
	    	incr = getIncr(key, getCurrent2TodayEndMillisTime());//从000001开始
	    }
	    DecimalFormat df=new DecimalFormat("000000");//序列号
	    return formatDate+df.format(incr);
	}
	
	public Long getIncr(String key, long liveTime) {
            RedisAtomicLong entityIdCounter = new RedisAtomicLong(key, redisTemplate.getConnectionFactory());
            Long increment = entityIdCounter.getAndIncrement();
 
            if ((null == increment || increment.longValue() == 0) && liveTime > 0) {//初始设置过期时间
                entityIdCounter.expire(liveTime, TimeUnit.MILLISECONDS);//单位毫秒
            }
            return increment;
        }
 
	//现在到今天结束的毫秒数
	public Long getCurrent2TodayEndMillisTime() {
	    Calendar todayEnd = Calendar.getInstance();
	    // Calendar.HOUR 12小时制
	    // HOUR_OF_DAY 24小时制
	    todayEnd.set(Calendar.HOUR_OF_DAY, 23); 
	    todayEnd.set(Calendar.MINUTE, 59);
	    todayEnd.set(Calendar.SECOND, 59);
	    todayEnd.set(Calendar.MILLISECOND, 999);
	    return todayEnd.getTimeInMillis()-new Date().getTime();
	}
	
}

使用代码时需要添加pom.xml 依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>2.1.3.RELEASE</version>
</dependency>

1. RedisAtomicLong 实现自动递增。RedisAtomicLong类使用起来更加简洁方便。(性能方便未测试过,不知道哪个方式更加好。)

猜你喜欢

转载自blog.csdn.net/Dream_bin/article/details/88620737