Java 基于数据库 实现sequence序列功能

499 阅读3分钟

前言

我们知道oracle提供sequence序列号,而mysql没有提供。本方案设计解决了应用系统在采用不同数据库时共用一种方法生成sequence,降低不同数据库异构带来的编码工作量。

方案设计

本方案设计满足:

  • 分布式场景使用
  • 满足一定的并发要求

总体设计思路:

  • 数据库表设计为name和value两个字段,name为主键。
  • 服务实例A 读取一条记录,缓存一个数据段,如1-100,将记录的当前值修改为100。
  • 服务实例B 读取一条记录,这时候库中记录值为100,那么它占据101-200这个数据段,将记录当前值修改为200。
  • 数据库更新值时采用悲观行锁,保证序列不冲突。
  • 服务实例A 在 1-100之间从缓存读取,减少数据库访问频率,用完再读取数据库(这时候数据库当前值可能是500,那么它将占据501-600数据段,数据库更新为600)

基于Java实现代码如下:

表结构

CREATE TABLE `custom_sequence`  (
  name           VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci not null COMMENT '序列名称',
  next_val       bigint not null COMMENT '下一个序列值',
  PRIMARY KEY (name) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '自定义序列' ROW_FORMAT = Dynamic;

SequenceId

public class SequenceId {
    private static final long STEP = 100;
    private SequenceIdProvider provider;
    private String name;
    private long beginValue;
    private long value;
    private boolean exist;
    protected SequenceId(SequenceIdProvider provider, String name, long beginValue) {
        this.provider = provider;
        this.name = name;
        this.beginValue = beginValue;
        this.value = -1;
        this.exist = false;
        if (beginValue <= 0) {
            throw new IllegalArgumentException("begin value must be great than zero.");
        }
    }
    
    public synchronized long nextVal() {
        if (value < 0) {
            value = move();
        } else {
            if (value % STEP == 0) {
                value = move();
            } else {
                value++;
            }
        }
        return value;
    }
    /**
     * 将序列值 往前挪一个step,数据库寸的最新step值 用 nextVal表示
     * 如果序列不存在,则新建序列,nextVal = beginValue - 1 + step;
     * 如果序列存在,更新序列值 nextVal = nextVal + step
     *
     * @return
     */
    private long move() {
        if(!exist){
            exist = provider.sequenceExist(name);
        }
        long nextVal = -1;
        if (!exist) {
            nextVal = beginValue - 1 + STEP;
            try {
                provider.addSequence(name, nextVal);
            } catch (Exception e) {
                //生成序列失败,则表明序列被人抢先一步,序列值设置为已存在
                exist = true;
            }
        }
        if (exist) {
            nextVal = provider.updateSequence(name, STEP);
        }
        return nextVal - STEP + 1;
    }
}

SequenceIdProvider

import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

public interface SequenceIdProvider {
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    default boolean sequenceExist(String name) {
        String sql = String.format("select count(1) from custom_sequence where name = '%s'", name);
        long count = query(sql);
        return count != 0;
    }
    
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    default void addSequence(String name, long nextVal) {
        String sql = String.format("insert into custom_sequence(name,next_val) values('%s', %s)", name, nextVal);
        update(sql);
    }
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    default long updateSequence(String name, long step) {
        //加悲观行锁
        String readNextValWithLock = String.format("select next_val from custom_sequence where name='%s' for update", name);
        long nextVal = query(readNextValWithLock);
        nextVal = nextVal + step;
        String updateSql = String.format("update custom_sequence set next_val = next_val + %s where name = '%s'", step, name);
        update(updateSql);
        return nextVal;
    }
    
    void update(String sql);

    long query(String sql);
}

SequenceUtil(工具类使用入口)

import java.lang.ref.SoftReference;
import java.util.HashMap;
import java.util.Map;

public class SequenceUtil {
    /**
     * 缓存变量
     */
    private transient static final Map<String, SoftReference<SequenceId>> cache;
    
    static {
        cache = new HashMap<>();
    }
    
    public static long nextVal(String name) {
        return nextVal(name, 1);
    }
    
    public static long nextVal(String name, long begin) {
        return create(name, begin).nextVal();
    }
    
    private static SequenceId create(String name, long begin) {
        SoftReference<SequenceId> softReference = cache.get(name);
        SequenceId sequenceId = null;
        if (softReference != null) {
            sequenceId = softReference.get();
        }
        if (sequenceId == null) {
            softReference = null;
            sequenceId = new SequenceId(getSequenceIdProvider(), name, begin);
            cache.put(name, new SoftReference<>(sequenceId));
        }
        return sequenceId;
    }
    
    private static SequenceIdProvider getSequenceIdProvider() {
        SequenceIdProvider sequenceIdProvider = SpringContextHolder.getBean(SequenceIdProvider.class);
        return sequenceIdProvider;
    }
}

我的项目中持久层使用了mybatis,以下是基于mybatis的实现代码,可结合项目中的具体框架做相应变更

MybatisSequenceIdProvider(SequenceIdProvider实现类)

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service("mybatisSequenceIdProvider")
public class MybatisSequenceIdProvider implements SequenceIdProvider {

    @Autowired
    private CommonMapper commonMapper;

    @Override
    public void update(String sql) {
        commonMapper.update(sql);
    }

    @Override
    public long query(String sql) {
        return commonMapper.query(sql);
    }
}

CommonMapper(Dao实现)

import org.apache.ibatis.annotations.*;
import org.springframework.stereotype.Repository;

@Repository
@Mapper
public interface CommonMapper {
    @Select("${sqlStr}")
    long query(@Param(value = "sqlStr") String sqlStr);

    @Update("${sqlStr}")
    void update(@Param(value = "sqlStr") String sqlStr);
}

使用方法

使用方法非常简单,假设序列名称为 dept_tbl

long seq = SequenceUtil.nextVal("dept_tbl");