前言
我们知道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");