单号生成器

498 阅读3分钟

方法定义:

/**
     * 生成单号
     *
     * @param sessionName   数据库会话名称
     * @param clazz         持久化映射类 类 类型
     * @param fieldName     单号字段
     * @param preCode       前缀
     * @param dateFormatter 时间格式
     * @param strongSerial  是否强连续(是:阻塞,否:非阻塞)
     * @return 单号
     */
String generate(String sessionName, Class<? extends AbstractEntity> clazz, String fieldName, String preCode, String dateFormatter, ResetRule resetRule, Boolean strongSerial);

使用:

1.弱连续(不保证单号的连续性,非阻塞)

String code = IdGenerator.getInstance().generate("biscuits", Order.class, "code", "10011039", "yyMMdd", ResetRule.DAY, false);

2.强连续(保证单号的连续性,阻塞)

IdGenerator idGenerator = IdGenerator.getInstance();
try{
    String preCode = "10011039";
    String code = idGenerator.generate("biscuits", Order.class, "code", preCode, "yyMMdd", ResetRule.DAY, true);
    // 执行业务代码
    // ...
    idGenerator.commit(preCode);
} catch(Exception e){
    idGenerator.rollback(preCode);
}

弱连续非阻塞实现思路

  • 使用 maxSnMap 存储每个单据对应的最序号
  • 生成单号时先从 maxSnMap 中查找对应单据的最大单据序号
  • 如果找不到则先给 当前单据 上双重锁查询数据库中的最大值
  • 如果获取锁失败则等待重试

源码

package org.hv.biscuits.core.sn;

import org.hv.biscuits.core.session.ActiveSessionCenter;
import org.hv.biscuits.function.BiscuitsSupplier;
import org.hv.pocket.criteria.Restrictions;
import org.hv.pocket.exception.SessionException;
import org.hv.pocket.model.AbstractEntity;
import org.hv.pocket.session.Session;
import org.hv.pocket.session.SessionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.LocalDate;
import java.time.Month;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicLong;

/**
 * 单号生成器
 *
 * @author wujianchuan 2020/11/2 17:32
 */
public class IdGenerator {
    private static final Logger LOGGER = LoggerFactory.getLogger(IdGenerator.class);
    private static final IdGenerator INSTANCE = new IdGenerator();
    private final Map<String/* pre code */, AtomicLong/* max serial number */> maxSnMap = new HashMap<>(60);
    private final Map<String/* pre code */, Long/* thread id */> lockMap = new HashMap<>(60);
    private final Map<String/* pre code */, LocalDate/* latest date */> latestDateMap = new HashMap<>(60);
    private static final int MAX_RETRY_TIMES = 6;

    private IdGenerator() {
    }

    public static IdGenerator getInstance() {
        return INSTANCE;
    }

    /**
     * 生成单号
     *
     * @param sessionName   数据库会话名称
     * @param clazz         持久化映射类 类 类型
     * @param fieldName     单号字段
     * @param preCode       前缀
     * @param dateFormatter 时间格式
     * @param resetRule     清零规则
     * @param strongSerial  是否强连续(是:阻塞,否:非阻塞)
     * @return 单号
     */
    public String generate(String sessionName, Class<? extends AbstractEntity> clazz, String fieldName, String preCode, String dateFormatter, ResetRule resetRule, Boolean strongSerial) throws Exception {
        LocalDate now = LocalDate.now();
        String dateStr = now.format(DateTimeFormatter.ofPattern(dateFormatter));
        BiscuitsSupplier<AtomicLong> maxSnSupplier = () -> {
            Object todayMaxCode;
            Session parentSession = ActiveSessionCenter.getCurrentSession();
            if (parentSession == null) {
                Session newSession = SessionFactory.getSession(sessionName);
                if (newSession == null) {
                    throw new SessionException("请正确开启数据库会话");
                }
                newSession.open();
                todayMaxCode = newSession.createCriteria(clazz).add(Restrictions.like(fieldName, preCode + dateStr + "_____")).max(fieldName);
                newSession.close();
            } else {
                todayMaxCode = parentSession.createCriteria(clazz).add(Restrictions.like(fieldName, preCode + dateStr + "_____")).max(fieldName);
            }
            latestDateMap.put(preCode, now);
            return todayMaxCode == null ? new AtomicLong(0) : new AtomicLong(Long.parseLong(todayMaxCode.toString().replace(preCode + dateStr, "")));
        };
        return (strongSerial == null || strongSerial) ? this.generateStrongSerial(preCode, dateStr, resetRule, maxSnSupplier) : this.generateWeakSerial(preCode, dateStr, resetRule, maxSnSupplier);
    }

    /**
     * 单号提交
     *
     * @param preCode 前缀
     */
    public void commit(String preCode) {
        boolean lock = lockMap.get(preCode) != null;
        if (lock) {
            lockMap.remove(preCode);
        }
    }

    /**
     * 单号回滚
     *
     * @param preCode 前缀
     */
    public void rollback(String preCode) {
        boolean lock = lockMap.get(preCode) != null;
        if (lock) {
            maxSnMap.get(preCode).decrementAndGet();
            lockMap.remove(preCode);
        }
    }

    /**
     * 生成强连续单号生成
     *
     * @param preCode       前缀
     * @param dateStr       日期格式
     * @param resetRule     清零规则
     * @param maxSnSupplier 获取最大序号
     * @return 单号
     */
    private String generateStrongSerial(String preCode, String dateStr, ResetRule resetRule, BiscuitsSupplier<AtomicLong> maxSnSupplier) throws Exception {
        // NOTE: 获取锁(首次获取||多次获取);使用 synchronized (preCode) 会大量消耗内存故使用 Map。
        boolean lock = lockMap.putIfAbsent(preCode, Thread.currentThread().getId()) == null || lockMap.get(preCode).equals(Thread.currentThread().getId());
        if (lock) {
            // NOTE: 先从堆中获取当太难最大序列号如果没有再从数据库查询。
            AtomicLong maxSn = this.maxSnMap.get(preCode);
            if (maxSn == null) {
                maxSn = maxSnSupplier.get();
                this.maxSnMap.put(preCode, maxSn);
            } else {
                LocalDate now = LocalDate.now();
                LocalDate latestDate = latestDateMap.get(preCode);
                switch (resetRule) {
                    case MONTH:
                        now = LocalDate.of(now.getYear(), now.getMonth(), 0);
                        latestDate = LocalDate.of(latestDate.getYear(), latestDate.getMonth(), 0);
                        break;
                    case YEAR:
                        now = LocalDate.of(now.getYear(), Month.JANUARY, 0);
                        latestDate = LocalDate.of(latestDate.getYear(), Month.JANUARY, 0);
                        break;
                    default:
                        throw new IllegalArgumentException(String.format("不支持的规则 - %s", resetRule));
                }
                if (now.isAfter(latestDate)) {
                    maxSn = new AtomicLong(0);
                    latestDateMap.put(preCode, LocalDate.now());
                }
            }
            return preCode + dateStr + String.format("%05d", maxSn.incrementAndGet());
        } else {
            long sleepMillis = 2;
            for (int i = 1; i < MAX_RETRY_TIMES; i++) {
                Thread.sleep(sleepMillis);
                if (lockMap.putIfAbsent(preCode, Thread.currentThread().getId()) == null) {
                    return this.generateStrongSerial(preCode, dateStr, resetRule, maxSnSupplier);
                }
                sleepMillis = sleepMillis * 2;
            }
            throw new TimeoutException(String.format("重试次数超过%s次", MAX_RETRY_TIMES));
        }
    }

    /**
     * 生成弱连续单号生成
     *
     * @param preCode       前缀
     * @param dateStr       日期格式
     * @param resetRule     清零规则
     * @param maxSnSupplier 获取最大序号
     * @return 单号
     */
    private String generateWeakSerial(String preCode, String dateStr, ResetRule resetRule, BiscuitsSupplier<AtomicLong> maxSnSupplier) throws Exception {
        AtomicLong maxSn = this.maxSnMap.get(preCode);
        if (maxSn == null) {
            // NOTE: 获取锁 使用 synchronized (preCode) 会大量消耗内存故使用 Map。
            boolean lock = lockMap.putIfAbsent(preCode, Thread.currentThread().getId()) == null;
            if (lock) {
                maxSn = this.maxSnMap.get(preCode);
                if (maxSn == null) {
                    maxSn = maxSnSupplier.get();
                    this.maxSnMap.put(preCode, maxSn);
                }
                lockMap.remove(preCode);
                return preCode + dateStr + String.format("%05d", maxSn.incrementAndGet());
            } else {
                long sleepMillis = 2;
                for (int i = 1; i < MAX_RETRY_TIMES; i++) {
                    Thread.sleep(sleepMillis);
                    if (this.maxSnMap.get(preCode) != null) {
                        return this.generateWeakSerial(preCode, dateStr, resetRule, maxSnSupplier);
                    }
                    sleepMillis = sleepMillis * 2;
                }
                throw new TimeoutException(String.format("重试次数超过%s次", MAX_RETRY_TIMES));
            }
        }
        LocalDate now = LocalDate.now();
        LocalDate latestDate = latestDateMap.get(preCode);
        switch (resetRule) {
            case MONTH:
                now = LocalDate.of(now.getYear(), now.getMonth(), 0);
                latestDate = LocalDate.of(latestDate.getYear(), latestDate.getMonth(), 0);
                break;
            case YEAR:
                now = LocalDate.of(now.getYear(), Month.JANUARY, 0);
                latestDate = LocalDate.of(latestDate.getYear(), Month.JANUARY, 0);
                break;
            default:
                throw new IllegalArgumentException(String.format("不支持的规则 - %s", resetRule));
        }
        if (now.isAfter(latestDate)) {
            if (!maxSn.compareAndSet(maxSn.get(), 0)) {
                return this.generateWeakSerial(preCode, dateStr, resetRule, maxSnSupplier);
            }
            latestDateMap.put(preCode, LocalDate.now());
        }
        return preCode + dateStr + String.format("%05d", maxSn.incrementAndGet());
    }
}

package org.hv.biscuits.core.sn;

/**
 * 单号清零规则
 *
 * @author wujianchuan 2020/11/8 13:53
 */
public enum ResetRule {
    /**
     * 一年清一次
     */
    YEAR,
    /**
     * 一个月清一次
     */
    MONTH,
    /**
     * 一天清一次
     */
    DAY;
}