讲讲Mongodb的24位ObjectId的无损压缩
2014-02-25 20:04
513 查看
一、问题背景
工作中,需要开发一个第三方接口用来同步数据,接口要求数据 ID 为字符串,且最长为16个字符,但是我公司保存数据使用的是Mongodb,其ID是ObjectId类型的,将其实例化为String时是24个字符,显然这个问题需要解决。
解决方法想到了几个:
1.比如再增加一个ID字段专门用于接口,而原有业务仍使用ObjectId;
2.或者另外再增加一个ID映射表。
3.将24位OjbectId截取某一段。
无疑前两种解决方案都需要在原有代码上进行修改,不管是额外维护一个字段还是一张表,都是非常麻烦的。而第三种方案会遇到一个问题就是两种ID无法互相转换。完美的解决方案是将24位的ObjectId压缩为一个16位或更少的ID,并且可以转换回来。
要压缩OjbectId,就要先了解OjbectId是什么以及是怎么生成的:(参考:/article/6991010.html
)
====================我是引用开始分割线========================
举个例子:OjbectId(“4e7020cb7cac81af7136236b”)这个24位的字符串,实际上它是由一组十六进制的字符构成,每个字节两位的十六进制数字,总共用了12字节的存储空间。官网中对ObjectId的规范,如图所示。
![](http://pic002.cnblogs.com/images/2011/83478/2011091823160647.png)
1) Time
时间戳。将objectid的前4位进行提取“4e7020cb”,然后按照十六进制转为十进制,变为“1315971275”,这个数字就是一个时间戳。通过时间戳的转换,就成了易看清的时间格式,如图3所示。
![](http://pic002.cnblogs.com/images/2011/83478/2011091823170212.png)
图3 时间戳的转换
2) Machine
机器。接下来的三个字节就是“7cac81”,这三个字节是所在主机的唯一标识符,一般是机器主机名的散列值,这样就确保了不同主机生成不同的机器hash值,确保在分布式中不造成冲突,这也就是在同一台机器生成的objectId中间的字符串都是一模一样的原因。
3) PID
进程ID。上面的Machine是为了确保在不同机器产生的objectId不冲突,而pid就是为了在同一台机器不同的mongodb进程产生了objectId不冲突,接下来的“af71”两位就是产生objectId的进程标识符。
4) INC
自增计数器。前面的九个字节是保证了一秒内不同机器不同进程生成objectId不冲突,这后面的三个字节“36236b”是一个自动增加的计数器,用来确保在同一秒内产生的objectId也不会发现冲突,允许256的3次方等于16777216条记录的唯一性。
总的来看,objectId的前4个字节时间戳,记录了文档创建的时间;接下来3个字节代表了所在主机的唯一标识符,确定了不同主机间产生不同的objectId;后2个字节的进程id,决定了在同一台机器下,不同mongodb进程产生不同的objectId;最后通过3个字节的自增计数器,确保同一秒内产生objectId的唯一性。ObjectId的这个主键生成策略,很好地解决了在分布式环境下高并发情况主键唯一性问题,值得学习借鉴。
====================我是引用结束分割线========================
上面引用了不少,其中有一个非常重要的信息是它是由一组十六进制的字符构成,24个字符,换句话说就是总共占用12字节,也就是96bit,每一个字符占用4bit。
由此,反正总共是96bit,假如用32进制来表示,也就是每5个bit用一个字符来表示,不就可以缩短字符串长度了么,可以缩短到 96 / 5 = 19.2 个字符,悲剧,还是不够。正面算得算到什么时候啊,还是反过来吧,要求是用16个字符来表示这96bit信息,那么一个字符就要能表示 96 / 16 = 6bit 的内容,仔细看看,一个字符表示6bit内容这不就是 2 ^ 6bit = 64 进制么,就像 2 ^ 1bit
= 2 进制是用一个字符(‘0’或者‘1’)来表示1bit内容,2 ^ 4bit = 16 进制用一个字符来表示4bit内容么。
16进制需要(0~9 a~f)16个符号来表示,64进制就需要64个符号,(0~9 a~z A~Z)加起来才62个符号,还少两个,可以用标点符号么,这个就要参考Base64编码(Wiki:http://zh.wikipedia.org/wiki/Base64 )了,一句话解释就是“Base64是一种基于64个可打印字符来表示二进制数据的表示方法。”。
还没完,我们的接口中id是放在URL中传输的,标准的Base64并不适合直接放在URL里传输,因为URL编码器会把标准Base64中的“/”和“+”字符变为形如“%XX”的形式,而这些“%”号在存入数据库时还需要再进行转换,因为ANSI SQL中已将“%”号用作通配符。
因此,可采用一种用于URL的改进Base64编码,将标准Base64中的“+”和“/”分别改成了“-”和“_”,这样就免去了在URL编解码和数据库存储时所要作的转换,避免了编码信息长度在此过程中的增加,并统一了数据库、表单等处对象标识符的格式。
二、具体实现
还是以上面的例子来看,ObjectId为“4e7020cb7cac81af7136236b”,由于是将24个字符压缩成16个字符,也就是每3个字符压缩成2个字符,因此我们取前3个字符作为示例:
1、16进制:'4' 'e' '7'
2、其中e表示的是14,所以三个字符转换为2进制形式就是:0100,1110,0111
3、上面每4bit便表示了一个字符,现在要转换成6bit表示一个字符,也就是 (0100<<2)+(1110>>2)=010011,(1110&0x3)<<4+0111=100111
4、通过移位和相加操作,就将3个4bit的信息保存到2个6bit中了,现在将这两个6bit用10进制来表示就是19,39
5、将19,39转换为64进制的字符表示:‘j',’D‘
通过以上步骤就将“4e7”转换为“jD”了,同样的道理,将以上步骤反过来运行就可以将“jD”转回“4e7”了。
重复以上步骤,可以将“4e7020cb7cac81af7136236b”转换为“jD0wOTOIwqZNdydH”。
我相信程序君们更喜欢直接看代码,下面就贴上上述算法的java实现:
三、附:位操作符
或操作符:| ,示例:5 | 3 = 0101b | 0011b = 0111b = 7
非操作符:~ ,示例: ~5 = ~0000...0101b = 1111...1010b = -6
异或操作符: ^ ,示例: 5 ^ 3 = 0101b ^ 0011b = 0110b = 6
与操作符: & ,示例: 5 & 3 = 0101b & 0011b = 0001b = 1
左移操作符:<< ,示例:5 << 35 = 0101b << ( 35%32) = 0101b << 3 = 0010 1000 = 40
算数右移:>> ,示例1: 5 >> 2 = 0101b >> 2 = 0001b = 1 ;示例2:-5 >> 2 = 1111...1011b >> 2 = 1111...1110b = -2
逻辑右移:>>> ,示例:-5 >>> 2 = 1111...1011b >>> 2 = 0011...1110b = 1073741822
工作中,需要开发一个第三方接口用来同步数据,接口要求数据 ID 为字符串,且最长为16个字符,但是我公司保存数据使用的是Mongodb,其ID是ObjectId类型的,将其实例化为String时是24个字符,显然这个问题需要解决。
解决方法想到了几个:
1.比如再增加一个ID字段专门用于接口,而原有业务仍使用ObjectId;
2.或者另外再增加一个ID映射表。
3.将24位OjbectId截取某一段。
无疑前两种解决方案都需要在原有代码上进行修改,不管是额外维护一个字段还是一张表,都是非常麻烦的。而第三种方案会遇到一个问题就是两种ID无法互相转换。完美的解决方案是将24位的ObjectId压缩为一个16位或更少的ID,并且可以转换回来。
要压缩OjbectId,就要先了解OjbectId是什么以及是怎么生成的:(参考:/article/6991010.html
)
====================我是引用开始分割线========================
举个例子:OjbectId(“4e7020cb7cac81af7136236b”)这个24位的字符串,实际上它是由一组十六进制的字符构成,每个字节两位的十六进制数字,总共用了12字节的存储空间。官网中对ObjectId的规范,如图所示。
![](http://pic002.cnblogs.com/images/2011/83478/2011091823160647.png)
1) Time
时间戳。将objectid的前4位进行提取“4e7020cb”,然后按照十六进制转为十进制,变为“1315971275”,这个数字就是一个时间戳。通过时间戳的转换,就成了易看清的时间格式,如图3所示。
![](http://pic002.cnblogs.com/images/2011/83478/2011091823170212.png)
图3 时间戳的转换
2) Machine
机器。接下来的三个字节就是“7cac81”,这三个字节是所在主机的唯一标识符,一般是机器主机名的散列值,这样就确保了不同主机生成不同的机器hash值,确保在分布式中不造成冲突,这也就是在同一台机器生成的objectId中间的字符串都是一模一样的原因。
3) PID
进程ID。上面的Machine是为了确保在不同机器产生的objectId不冲突,而pid就是为了在同一台机器不同的mongodb进程产生了objectId不冲突,接下来的“af71”两位就是产生objectId的进程标识符。
4) INC
自增计数器。前面的九个字节是保证了一秒内不同机器不同进程生成objectId不冲突,这后面的三个字节“36236b”是一个自动增加的计数器,用来确保在同一秒内产生的objectId也不会发现冲突,允许256的3次方等于16777216条记录的唯一性。
总的来看,objectId的前4个字节时间戳,记录了文档创建的时间;接下来3个字节代表了所在主机的唯一标识符,确定了不同主机间产生不同的objectId;后2个字节的进程id,决定了在同一台机器下,不同mongodb进程产生不同的objectId;最后通过3个字节的自增计数器,确保同一秒内产生objectId的唯一性。ObjectId的这个主键生成策略,很好地解决了在分布式环境下高并发情况主键唯一性问题,值得学习借鉴。
====================我是引用结束分割线========================
上面引用了不少,其中有一个非常重要的信息是它是由一组十六进制的字符构成,24个字符,换句话说就是总共占用12字节,也就是96bit,每一个字符占用4bit。
由此,反正总共是96bit,假如用32进制来表示,也就是每5个bit用一个字符来表示,不就可以缩短字符串长度了么,可以缩短到 96 / 5 = 19.2 个字符,悲剧,还是不够。正面算得算到什么时候啊,还是反过来吧,要求是用16个字符来表示这96bit信息,那么一个字符就要能表示 96 / 16 = 6bit 的内容,仔细看看,一个字符表示6bit内容这不就是 2 ^ 6bit = 64 进制么,就像 2 ^ 1bit
= 2 进制是用一个字符(‘0’或者‘1’)来表示1bit内容,2 ^ 4bit = 16 进制用一个字符来表示4bit内容么。
16进制需要(0~9 a~f)16个符号来表示,64进制就需要64个符号,(0~9 a~z A~Z)加起来才62个符号,还少两个,可以用标点符号么,这个就要参考Base64编码(Wiki:http://zh.wikipedia.org/wiki/Base64 )了,一句话解释就是“Base64是一种基于64个可打印字符来表示二进制数据的表示方法。”。
还没完,我们的接口中id是放在URL中传输的,标准的Base64并不适合直接放在URL里传输,因为URL编码器会把标准Base64中的“/”和“+”字符变为形如“%XX”的形式,而这些“%”号在存入数据库时还需要再进行转换,因为ANSI SQL中已将“%”号用作通配符。
因此,可采用一种用于URL的改进Base64编码,将标准Base64中的“+”和“/”分别改成了“-”和“_”,这样就免去了在URL编解码和数据库存储时所要作的转换,避免了编码信息长度在此过程中的增加,并统一了数据库、表单等处对象标识符的格式。
二、具体实现
还是以上面的例子来看,ObjectId为“4e7020cb7cac81af7136236b”,由于是将24个字符压缩成16个字符,也就是每3个字符压缩成2个字符,因此我们取前3个字符作为示例:
1、16进制:'4' 'e' '7'
2、其中e表示的是14,所以三个字符转换为2进制形式就是:0100,1110,0111
3、上面每4bit便表示了一个字符,现在要转换成6bit表示一个字符,也就是 (0100<<2)+(1110>>2)=010011,(1110&0x3)<<4+0111=100111
4、通过移位和相加操作,就将3个4bit的信息保存到2个6bit中了,现在将这两个6bit用10进制来表示就是19,39
5、将19,39转换为64进制的字符表示:‘j',’D‘
通过以上步骤就将“4e7”转换为“jD”了,同样的道理,将以上步骤反过来运行就可以将“jD”转回“4e7”了。
重复以上步骤,可以将“4e7020cb7cac81af7136236b”转换为“jD0wOTOIwqZNdydH”。
我相信程序君们更喜欢直接看代码,下面就贴上上述算法的java实现:
/** * @author xumeng * */ public class ObjectIdConverter { /** * 将 64进制编码长度为16位的ID 转换为 16进制编码的长度为24位的ID * @param qunarHotelId * @return */ public static String unCompressObjectId(String shortId){ if(shortId == null || shortId.length() != 16){ throw new IllegalArgumentException(); } StringBuilder res = new StringBuilder(24); char[] str = shortId.toCharArray(); for(int i = 0; i < str.length; i += 2){ int pre = char2Int(str[i]),end = char2Int(str[i+1]); res.append(int2Char( (pre >> 2) )); res.append(int2Char( ((pre & 3) << 2) + (end >> 4) )); res.append(int2Char( end & 15 )); } return res.toString(); } /** * 将 16进制编码的长度为24位的ID 转换为 64进制编码长度为16位的ID * @param tdxHotelId * @return */ public static String compressObjectId(String objectId){ if(objectId == null || objectId.length() != 24){ throw new IllegalArgumentException(); } StringBuilder res = new StringBuilder(16); char[] str = objectId.toCharArray(); for(int i = 0; i < str.length; i += 3){ int pre = char2Int(str[i]),mid = char2Int(str[i+1]),end = char2Int(str[i+2]); res.append(int2Char( (pre << 2) + (mid >> 2) )); res.append(int2Char( ((mid & 3) << 4) + end )); } return res.toString(); } /** * 支持64进制bit转字符 * 0~9,a~z,A~Z,-,_ * @param i */ private static char int2Char(int i){ if(i >= 0 && i <= 9){ return (char) ('0'+i); }else if(i >= 10 && i <= 35){ return (char) ('a'+i-10); }else if(i >= 36 && i <= 61){ return (char) ('A'+i-36); }else if(i == 62){ return '-'; }else if(i == 63){ return '_'; }else{ throw new IllegalArgumentException(); } } /** * 支持64进制字符转bit * 0~9,a~z,A~Z,-,_ * @param c */ private static int char2Int(char c){ if(c >= '0' && c <= '9'){ return c-'0'; }else if(c >= 'a' && c <= 'z'){ return 10+c-'a'; }else if(c >= 'A' && c <= 'Z'){ return 36+c-'A'; }else if(c=='-'){ return 62; }else if(c=='_'){ return 63; }else{ throw new IllegalArgumentException(); } } }
三、附:位操作符
或操作符:| ,示例:5 | 3 = 0101b | 0011b = 0111b = 7
非操作符:~ ,示例: ~5 = ~0000...0101b = 1111...1010b = -6
异或操作符: ^ ,示例: 5 ^ 3 = 0101b ^ 0011b = 0110b = 6
与操作符: & ,示例: 5 & 3 = 0101b & 0011b = 0001b = 1
左移操作符:<< ,示例:5 << 35 = 0101b << ( 35%32) = 0101b << 3 = 0010 1000 = 40
算数右移:>> ,示例1: 5 >> 2 = 0101b >> 2 = 0001b = 1 ;示例2:-5 >> 2 = 1111...1011b >> 2 = 1111...1110b = -2
逻辑右移:>>> ,示例:-5 >>> 2 = 1111...1011b >>> 2 = 0011...1110b = 1073741822
相关文章推荐
- 在RedHatEnterpriseServer 6.1下安装mongodb(一)
- MongoDB 查询优化分析
- MongoDB T-shirt 来了
- NoSQL高级培训课程-HBase&&MongoDB(两天版)
- MongoDB安装成为Windows服务及日常使用遇到问题总结
- 30分钟学MongoDB系列——限定集与大文件存储篇
- MongoDB学习笔记(四)--索引 && 性能优化
- [转载]MongoDB设置访问权限、设置用户
- mongodb与关系型数据库相比的优缺点zz
- Mongodb启动命令mongod参数说明
- MongoDB学习笔记之 第1章 MongoDB的安装
- 【MongoDB for Java】Java操作MongoDB
- MongoDB 基本命令
- 30分钟学MongoDB系列——高级查询与操作篇
- Linux下安装并启动MongoDB
- Linux下安装并启动MongoDB
- Linux下安装并启动MongoDB
- mongodb
- mongoDB研究笔记:分片集群部署
- 《MongoDB权威指南》学习整理----MongoDB文档查询