高效透明的主键策略

本文提出一种跨数据库主键生成方案,适用于多种数据库环境,支持高并发和集群部署。该方案采用步进式主键生成,利用单例模式减少数据库访问频率,提高性能。

前段时间有个朋友问我如何能在保证单个节点上的主键高效并且唯一,并且能支持的住较大的访问量。同时和所采用的数据库无关,也就是说我可以使用任何数据库都能采用这个策略

1、这里采用既有的主键策略可能使用uuid是最直接的解决方案,但是缺点就是uuid产生的字符串检索较慢而且无规律,并不是每个需求都希望主键看起来杂乱无章

2、常见的sequence以及nativie,hilo等策略在很多场景下需要特定的数据库支持


这里总结了一个简单的版本

我不打算针对每个表都做独立的主键自增,那个留给有兴趣的人自己扩展吧


一、核心思路

1、创建一个表,两个字段name,value,name记录属性名,value记录主键当前值

2、采用synchronized方法来保证单个应用节点对获取id方法的独立性

3、通过步长来提升获取主键ID的性能。例如:当前值是1000,我将步长设置成100,那么下次访问此表获取主键就是1100了

4、利用单例模式,将当前值1000到1100之间的步长在当前节点进行线性消费,这样我们就不需要每次需要主键id自增的时候,每次都访问数据库来做查询更新操作


二、实施策略

1、存储当前id的对象类

public class IdBlock {


    //下个主键id
  long nextId;
  //最新更新的数据库主键
  long lastId;

  public IdBlock(long nextId, long lastId) {
    this.nextId = nextId;
    this.lastId = lastId;
  }

  public long getNextId() {
    return nextId;
  }
  public long getLastId() {
    return lastId;
  }
}


2、访问数据库获取下个主键,这里设置了步长为100

public class GetNextIdBlockCmd implements Command<IdBlock> {
  
  private static final long serialVersionUID = 1L;
  
  //取得主键的步长,这里我们可以假定设置为100
  protected int idBlockSize;
  
  public GetNextIdBlockCmd(int idBlockSize) {
    this.idBlockSize = idBlockSize;
  }

  public IdBlock execute(CommandContext commandContext) {
	  //查询库里的主键id的最大值
    PropertyEntity property = (PropertyEntity) commandContext
      .getPropertyManager()
      .findPropertyById("next.dbid");
    //获取了库里逐渐id的最大值
    long oldValue = Long.parseLong(property.getValue());
    //将最大值增加了一个步长,这里假设我们步长为100,增加了100
    long newValue = oldValue+idBlockSize;
    //更新库里的最大主键为步长增加后的值
    property.setValue(Long.toString(newValue));
    //将原来的主键值和最新的主键值传到主键存放对象类中
    return new IdBlock(oldValue, newValue-1);
  }
}


3、获取下个主键的类(单例模式类,这里如果在spring配置文件中声明,就是单例了。保证在当前节点状态一致),这里在当前节点主键没有被消耗完毕是不需要访问数据库的

public class DbIdGenerator implements IdGenerator {

	//主键步长
  protected int idBlockSize;
  //下个主键id
  protected long nextId = 0;
  //数据库中存放的最新最大主键id
  protected long lastId = -1;
  
  //命令模式,就是用来做数据CRUD的,不必关注实现细节
  protected CommandExecutor commandExecutor;
  
  //获取下个主键
  public synchronized String getNextId() {
	  //当我们从数据库取得最新更新主键id 小于我们 的下个id的时候。也就是本地主键获取消耗完毕的时候
    if (lastId<nextId) {
    	//直接读库,并作下个主键以及数据库最新主键id的更新
      getNewBlock();
    }
    //如果我们下个主键id值还是小于数据库更新的id值,那么就自增,因为这一个id主键段是我们按照步长获取的,所以不会重复
    long _nextId = nextId++;
    return Long.toString(_nextId);
  }

  //带着步长去查询更新数据库,只有本地主键消耗完毕的时候才会调用这个方法
  protected synchronized void getNewBlock() {

	  //直接访问数据库来获取下个主键id,以及数据库最新的更新id。下个主键id理论上初始时候应该是 nextId+100=lastId
    IdBlock idBlock = commandExecutor.execute(new GetNextIdBlockCmd(idBlockSize));
    this.nextId = idBlock.getNextId();
    this.lastId = idBlock.getLastId();
  }

  public int getIdBlockSize() {
    return idBlockSize;
  }

  public void setIdBlockSize(int idBlockSize) {
    this.idBlockSize = idBlockSize;
  }
  
  public CommandExecutor getCommandExecutor() {
    return commandExecutor;
  }

  public void setCommandExecutor(CommandExecutor commandExecutor) {
    this.commandExecutor = commandExecutor;
  }
}



三、扩展思路:解决集群以及高并发的问题

1、如果我们需要给每个表做主键自增,这里只需要在查库的改成查询表名即可

2、如果我们需要多个集群节点同时读写,防止问题,我们可以给每个节点的id加一个前缀(一般采用ip的最后一个位号,或者ip值取余)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值