环境是这样的:服务器是用java做的, 数据库是mongodb
需求是这样的:我们的系统里要生成一个唯一id,前面的部分有一定的格式,并和时间关联, 精确到微秒,考虑到同一微秒内有可能存在并发情况, 所以后面在加两位序列号, 系统需要定义为1毫秒内的并发小于100个,所以后面两位就够用了。 java服务器端有多台机器都可以用来生成这个唯一id,所以需要在不同的机器上不能生成相同的序列号,所以需要在某一点上做全局的范围同步来保存这序列 号的唯一性。 其实如果不考虑需求里的唯一id是有一定意义的格式的, 用uuid或mongodb的objectid都是更好的选择,完全不需要在某一点上进行同步,性能会更好。
这个可以生成序列号的点, 我们可以做一个序列号生成服务器来对应, 也可以用数据库来对应。 单单为这个简单的功能准备一个服务器来做显然不合适。 但是我们用的mongodb并没有类似于mysql或oracle中的select for update这样的锁机制。 所以没有办法简单的对这个序列号做原子操作。 但是mongodb的对单个document进行update操作中有很是具有原子性的, 例如
- $set
- $unset
- $inc
- $push
- $pushall
- $pull
- $pullall
我们可以利用这些原子操作,在数据库层以乐观锁的形式来实现循环序列字段。为了方便调用我把这段逻辑做成数据库中的javascript函数。 类似与mysql中的存储过程。
首先我们需要一个collection来存放序列号,并对需要的需要的序列号进行初始化。我们叫它counters。
- db.counters.save({_id:"serialno1", val:0, maxval:99})
然后我们想system.js里添加一个javascript函数
- db.system.js.save({_id:"getnextuniqueseq",
- value:function (keyname) {
- var seqobj = db.counters.findone({_id:keyname});
- if (seqobj == null) {
- print("can not find record with key: " keyname);
- return -1;
- }
- // the max value of sequence
- var maxval = seqobj.maxval;
- // the current value of sequence
- var curval = seqobj.val;
- while(true){
- // if curval reach max, reset it
- if(curval >= maxval){
- db.counters.update({_id : keyname, val : curval}, { $set : { val : 0 }}, false, false);
- var err = db.getlasterrorobj();
- if( err && err.code ) {
- print( "unexpected error reset data: " tojson( err ) );
- return -2;
- } else if (err.n == 0){
- // fail to reset value, may be reseted by others
- print("fail to reset value: ");
- }
- // get current value again.
- seqobj = db.counters.findone({_id:keyname});
- maxval = seqobj.maxval;
- curval = seqobj.val;
- continue;
- }
- // if curval not reach the max, inc it;
- // increase
- db.counters.update({_id : keyname, val : curval}, { $inc : { val : 1 }}, false, false);
- var err = db.getlasterrorobj();
- if( err && err.code ) {
- print( "unexpected error inc val: " tojson( err ) );
- return -3;
- } else if (err.n == 0){
- // fail to reset value, may be increased by others
- print("fail to inc value: ");
- // get current value again.
- seqobj = db.counters.findone({_id:keyname});
- maxval = seqobj.maxval;
- curval = seqobj.val;
- continue;
- } else {
- var retval = curval 1;
- print("success to get seq : " retval);
- // increase successful
- return retval;
- }
- }
- }
- });
上面这段会把指定的序列号的val值 1,如果val达到上限则从0开始。所以叫循环序列。
其实上面的实现在原理上和java里的atomicinteger系列的功能实现是类似的,利用循环重试和原子性的cas来实现。这种实现方式在多线程的环境里由于锁(monitor)的范围很小,所以并发性上比排他锁要好一些。
下面我们用java来测试一下这个函数的正确性。 即在多线程的情况下会不会得到重复的序列号。
第一个测试,val=0, maxval=2000, java端20个线程每个线程循环调用100次。 共2000次。 所以正确的情况下,从0到1999应该每个数字只出现一次。
- @test
- public void testgetnextuniqueseq1() throws exception {
- final int thread_count = 20;
- final int loop_count = 100;
- mongo mongoclient = new mongo("172.17.2.100", 27017);
- db db = mongoclient.getdb("im");
- db.authenticate("imadmin", "imadmin".tochararray());
- basicdbobject q = new basicdbobject();
- q.put("_id", "unique_key");
- basicdbobject upd = new basicdbobject();
- basicdbobject set = new basicdbobject();
- set.put("val", 0);
- set.put("maxval", thread_count * loop_count);
- upd.put("$set", set);
- db.getcollection("counters").update(q, upd);
- thread[] threads = new thread[thread_count];
- final int[][] results = new int[thread_count][loop_count];
- for (int i = 0; i < thread_count; i ) {
- final int temp_i = i;
- threads[i] = new thread("" i) {
- @override
- public void run() {
- try {
- mongo mongoclient = new mongo("172.17.2.100", 27017);
- db db = mongoclient.getdb("im");
- db.authenticate("imadmin", "imadmin".tochararray());
- for (int j = 0; j < loop_count; j ) {
- object result = db.eval("getnextuniqueseq(\"unique_key\")");
- system.out.printf("thread %s, seq=%d\n", thread.currentthread().getname(), ((double) result).intvalue());
- results[temp_i][j] = ((double) result).intvalue();
- }
- } catch (unknownhostexception e) {
- e.printstacktrace();
- }
- }
- };
- }
- for (thread thread : threads) {
- thread.start();
- }
- for (thread thread : threads) {
- thread.join();
- }
- for (int num = 1; num <= loop_count * thread_count; num ) {
- // every number appear 1 times only!
- int times = 0;
- for (int j = 0; j < thread_count; j ) {
- for (int k = 0; k < loop_count; k ) {
- if (results[j][k] == num)
- times ;
- }
- }
- assertequals(1, times);
- }
- }
然后我们再测试一下循环的情况。 val=0, maxval=99。 同样是java端20个线程每个线程循环调用100次。 共2000次。这次从0到99的数字每个应该取得20次。
- @test
- public void testgetnextuniqueseq2() throws exception {
- final int thread_count = 20;
- final int loop_count = 100;
- mongo mongoclient = new mongo("172.17.2.100", 27017);
- db db = mongoclient.getdb("im");
- db.authenticate("imadmin", "imadmin".tochararray());
- basicdbobject q = new basicdbobject();
- q.put("_id", "unique_key");
- basicdbobject upd = new basicdbobject();
- basicdbobject set = new basicdbobject();
- set.put("val", 0);
- set.put("maxval", loop_count);
- upd.put("$set", set);
- db.getcollection("counters").update(q, upd);
- thread[] threads = new thread[thread_count];
- final int[][] results = new int[thread_count][loop_count];
- for (int i = 0; i < thread_count; i ) {
- final int temp_i = i;
- threads[i] = new thread("" i) {
- @override
- public void run() {
- try {
- mongo mongoclient = new mongo("172.17.2.100", 27017);
- db db = mongoclient.getdb("im");
- db.authenticate("imadmin", "imadmin".tochararray());
- for (int j = 0; j < loop_count; j ) {
- object result = db.eval("getnextuniqueseq(\"unique_key\")");
- system.out.printf("thread %s, seq=%d\n", thread.currentthread().getname(), ((double) result).intvalue());
- results[temp_i][j] = ((double) result).intvalue();
- }
- } catch (unknownhostexception e) {
- e.printstacktrace();
- }
- }
- };
- }
- for (thread thread : threads) {
- thread.start();
- }
- for (thread thread : threads) {
- thread.join();
- }
- for (int num = 1; num <= loop_count; num ) {
- // every number appear 20 times only!
- int times = 0;
- for (int j = 0; j < thread_count; j ) {
- for (int k = 0; k < loop_count; k ) {
- if (results[j][k] == num)
- times ;
- }
- }
- assertequals(20, times);
- }
- }
这个测试跑了几次都是正确的。
由于没有可以进行对比其他的实现方式(例如排他锁)所以没有做性能测试。
写在最后。 虽然mongodb支持类似于存储过程的stored javascript,但是其实不建议使用这个来解决复杂问题。主要原因是没法调试,维护起来太不方便。而且在2.4之前mongodb对服务端 javascript支持并不是很好, 一个mongod进程同时只能执行一段javascript。如果能在应用层解决掉还是在应用层里实现逻辑比较好。