Sequence Generator

栏目: 数据库 · 发布时间: 7年前

内容简介:在如今的分布式场景下,MySql分库分表非常常见。我们不能用MySql的auto increment字段,这是单表的。我们需要保证一个逻辑表的若干物理表里的数据的主键互相不重复。因为如果存在重复的情况,当物理表扩容或缩容导致数据移动并进一步导致两条拥有相同ID的数据进入到一张表里时,就会造成主键冲突。所以,我们需要一个全局的,高性能的唯一主键生成器。GUID 或UUID 貌似就符合这个要求!但是,一来这类id不是数字的,生成不具有全局顺序性。二来,也是因为不是数字的,做MySql的主键,性能不如数字。所以

Sequence(唯一id) 生成器

在如今的分布式场景下,MySql分库分表非常常见。我们不能用 MySql 的auto increment字段,这是单表的。我们需要保证一个逻辑表的若干物理表里的数据的主键互相不重复。因为如果存在重复的情况,当物理表扩容或缩容导致数据移动并进一步导致两条拥有相同ID的数据进入到一张表里时,就会造成主键冲突。所以,我们需要一个全局的,高性能的唯一主键生成器。

GUID 或UUID 貌似就符合这个要求!但是,一来这类id不是数字的,生成不具有全局顺序性。二来,也是因为不是数字的,做MySql的主键,性能不如数字。所以,虽然生成的成本很低,也要放弃这个方案。

我们的方案是,在数据库中持久化一个数值,表示当前ID增长到了哪,要获取下一个ID时则带条件的去更新这个值为 当前值+1 (乐观锁),如果更新成功,则认为获取到了新的ID,如果不成功,则再试到成功为止。进一步的,为了在逻辑表和应用服务器都比较多的情况下,降低持有这个值的表过热,乐观锁碰撞过多的情况。允许每个应用服务器持有一个容量为n的ID块,然后n次内的ID获取在应用服务器内同步的完成,第n次获取,才向数据库发一次update操作,修改数据库从+1变成+n。大概示意图如下:

Sequence Generator

下面来详细看下实现

首先,由于上面提到的应用服务器内持有一个容量为n的id块,我们设计了一个内部类Step来实现这个功能。

static class Step {
        private long currentValue; // 当前值,调用incrementAndGet()返回下一个id
        private long endValue;  // 当前块的结束值,限定Step的容量

    	// 初始化时  endValue - currentValue = n   (blockSize)
        Step(long currentValue, long endValue) {
            this.currentValue = currentValue;
            this.endValue = endValue;
        }

        public void setCurrentValue(long currentValue) {
            this.currentValue = currentValue;
        }

        public void setEndValue(long endValue) {
            this.endValue = endValue;
        }
      
        // 因为这个方法 外面是在同步块里调用的,所以这里没用synchronize,
        // currentValue 也没用 AtomicLong
        public  long incrementAndGet() {
            return ++currentValue;
        }
    }

我们再看Sequence类:

public class Sequence {
    private int blockSize = 5; // 容量或者叫步长,一次从数据库中获取多少个ID
    private long startValue = 0; // id从几开始涨,在已有数据要迁移的情况下,可以设置为非零的数
    // 每个表一个Step,这个map持有了所有表的Step引用
    // 外面是同步的  所以也不用concurrentHashMap
    private Map<String,Step> stepMap = new HashMap<String, Step>();
    
    // 持有数据源和三条sql。用于持久化的表名是写死的:sequence_value
    // 数据源通过spring注入
    private DataSource dataSource;
    private final static String GET_SQL = "select id from sequence_value where name = ?";
    private final static String NEW_SQL = "insert into sequence_value (id,name) values (?,?)";
    // 这条 sql 里where条件里的id=?很重要,是一个乐观锁机制,防止过程中已经有别的进程(别的Sequence实例)
    // 对表进行了更新,导致id重复
    private final static String UPDATE_SQL = "update sequence_value set id = ?  where name = ? and id = ?";
    // ...
}

Sequence类的唯一入口方法 get的实现:

// 这个方法必须是同步的,防止多个线程同时获取同一表的id,导致重复
public synchronized long get(String sequenceName) {
        Step step = stepMap.get(sequenceName);
    	if(step == null) {
            // step == null 表示第一次获取这个表的id,后面的逻辑会继续走,从数据库中读入
            // 直接new一个Step放到map里备用,下次就能走else里的逻辑了
            step = new Step(startValue,startValue+blockSize);
            stepMap.put(sequenceName, step);
        } else {
            // 当前块还没用完,直接内存里返回下一个ID就好
            // 否则的话,表示当前块里id用完了,继续走下面的从数据库中获取的逻辑
            if (step.currentValue < step.endValue) {//默认为0和0,所以没有错
                return step.incrementAndGet();
            }
        }
    
        // 尝试blockSize次 从数据库获取下一个块
        for (int i = 0; i < blockSize; i++) {
            if (getNextBlock(sequenceName,step)) {
                //  一旦获取到,就直接内存里返回,不用再getNextBlock了
                return step.incrementAndGet();
            }
        }
    
        // 尝试了若干次都失败了,只能抛异常出去
        throw new RuntimeException("No more value.");
    }

我们再看看从数据库中获取下一个块的实现是怎么样的。其实从上面看到的3条sql基本上就已经了解得7788了。

private boolean getNextBlock(String sequenceName, Step step) {
    	// 发select语句,获取库里当前值
        Long value = getPersistenceValue(sequenceName);
        if (value == null) {
            try {
                // 如果没有,就初始化, 发insert语句,
                // insert的值是上面配置的 startValue, 默认为0, 返回的是刚插入的值
                value = newPersistenceValue(sequenceName); 
            } catch (Exception e) {
                // 如果初始化失败,说明有程序先初始化了,(别的进程也同时对这个表进行id获取)
                // 这里需要对sequence_value表的name字段做唯一索引限制。否则会有问题
                // 可能两个进程同时插入了两条name相同的数据
                log.error("newPersistenceValue error!");
                value = getPersistenceValue(sequenceName); 
            }
        }
        // 发update语句,申请下一个块,将库里的值update为 value+blockSize ,
        // 这样,这个进程里的请求就能直接内存返回
        // 而别的进程 去getNextBlock时,会改成更大的值,
        // 比如A进程持有的是 0-5, B进程持有的是 6-10
     	// 这条update语句是带条件更新的,前面已经有提到,也是为了防止并发产生id重复
        // 因为带条件更新可能失败, 所以 如果失败,外面的get方法要进行重试。 策略是重试blockSize次
        boolean b = saveValue(value,sequenceName) == 1;
        if (b) {
            // 成功获取到下一个块了,更新step对象,后面就可以用step.incrementAndGet()了。
            step.setCurrentValue(value);
            step.setEndValue(value+blockSize);
        }
        return b;
    }

到此,Sequence的核心实现就完成了。 getPersistenceValue newPersistenceValue saveValue 这三个jdbc操作数据库的方法没什么特别的,就不贴出来细看了。

有些时候,除了我们的主业务表比较大,id需求比较大,需要单独一个Sequence名字,其他很多小表可以共用一个默认的Sequence名字,所以,就又提供了一个 SequenceUtil 类。作为Sequence的一个代理,如果没有对应名字的id序列,就用默认的序列。

public class SequenceUtil {
	public long get(String name) {
        Sequence sequence = null;
        if (sequenceMap != null) {
            sequence = sequenceMap.get(name);
        }
        if (sequence == null) {
            // 如果从持有的map里没取到这个名字的Sequence的话,就取默认的
            if (defaultSequence != null) {
                return defaultSequence.get(name);
            } else {
                // 默认的还没有配置,就抛异常
                throw new RuntimeException("sequence " + name + " undefined!");
            }
        }
        // 代理到具体的Sequence实例 进行id的get
        return sequence.get(name);
    }
}

OK,到此为止这个精巧的设计就介绍完了。如果要生成id的表太多,导致Sequence_value表过热,可以把BlockSize调大一些,减少对库的操作次数。甚至可以设置几个不同的实例,连接不同的数据源。 这个设计有一个需要注意的地方,就是生成的id是大体上全局有序的,但不是严格有序的。比如A实例持有该1-5,B实例持有6-10。生成的id可能是 1,6,2,7……

都看到这了,说明是真爱!那就给个赠品吧。flickr的Ticket Server设计。这我们这个设计有许多相似之处。供参考:http://code.flickr.net/2010/02/08/ticket-servers-distributed-unique-primary-keys-on-the-cheap/


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Web Security Testing Cookbook

Web Security Testing Cookbook

Paco Hope、Ben Walther / O'Reilly Media / 2008-10-24 / USD 39.99

Among the tests you perform on web applications, security testing is perhaps the most important, yet it's often the most neglected. The recipes in the Web Security Testing Cookbook demonstrate how dev......一起来看看 《Web Security Testing Cookbook》 这本书的介绍吧!

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具