0%

丰富的数据结构使得redis的设计非常的有趣。不像关系型数据库那样,DEV和DBA需要深度沟通,review每行sql语句,也不像memcached那样,不需要DBA的参与。redis的DBA需要熟悉数据结构,并能了解使用场景。

下面举一些常见适合kv数据库的例子来谈谈键值的设计,并与关系型数据库做一个对比,发现关系型的不足之处。

用户登录系统

记录用户登录信息的一个系统, 我们简化业务后只留下一张表。

关系型数据库的设计

1
2
3
4
5
6
7
8
mysql> select * from login;
+---------+----------------+-------------+---------------------+
| user_id | name | login_times | last_login_time |
+---------+----------------+-------------+---------------------+
| 1 | ken thompson | 5 | 2011-01-01 00:00:00 |
| 2 | dennis ritchie | 1 | 2011-02-01 00:00:00 |
| 3 | Joe Armstrong | 2 | 2011-03-01 00:00:00 |
+---------+----------------+-------------+---------------------+

user_id表的主键,name表示用户名,login_times表示该用户的登录次数,每次用户登录后,login_times会自增,而last_login_time更新为当前时间。

redis的设计

关系型数据转化为KV数据库,我的方法如下:

key 表名:主键值:列名

value 列值

一般使用冒号做分割符,这是不成文的规矩。比如在php-admin for redis系统里,就是默认以冒号分割,于是user:1 user:2等key会分成一组。于是以上的关系数据转化成kv数据后记录如下:

1
2
3
4
5
6
7
8
9
10
11
Set login:1:login_times 5
Set login:2:login_times 1
Set login:3:login_times 2

Set login:1:last_login_time 2011-1-1
Set login:2:last_login_time 2011-2-1
Set login:3:last_login_time 2011-3-1

set login:1:name "ken thompson"
set login:2:name "dennis ritchie"
set login:3:name "Joe Armstrong"

这样在已知主键的情况下,通过get、set就可以获得或者修改用户的登录次数和最后登录时间和姓名。

一般用户是无法知道自己的id的,只知道自己的用户名,所以还必须有一个从name到id的映射关系,这里的设计与上面的有所不同。

1
2
3
set "login:ken thompson:id"      1
set "login:dennis ritchie:id" 2
set "login:Joe Armstrong:id" 3

这样每次用户登录的时候业务逻辑如下(python版),r是redis对象,name是已经获知的用户名。

1
2
3
4
5
6
#获得用户的id
uid = r.get("login:%s:id" % name)
#自增用户的登录次数
ret = r.incr("login:%s:login_times" % uid)
#更新该用户的最后登录时间
ret = r.set("login:%s:last_login_time" % uid, datetime.datetime.now())

如果需求仅仅是已知id,更新或者获取某个用户的最后登录时间,登录次数,关系型和kv数据库无啥区别。一个通过btree pk,一个通过hash,效果都很好。

假设有如下需求,查找最近登录的N个用户。开发人员看看,还是比较简单的,一个sql搞定。

1
select * from login order by last_login_time desc limit N

DBA了解需求后,考虑到以后表如果比较大,所以在last_login_time上建个索引。执行计划从索引leafblock 的最右边开始访问N条记录,再回表N次,效果很好。

过了两天,又来一个需求,需要知道登录次数最多的人是谁。同样的关系型如何处理?DEV说简单

1
select * from login order by login_times desc limit N

DBA一看,又要在login_time上建立一个索引。有没有觉得有点问题呢,表上每个字段上都有素引。

关系型数据库的数据存储的的不灵活是问题的源头,数据仅有一种储存方法,那就是按行排列的堆表。统一的数据结构意味着你必须使用索引来改变sql的访问路径来快速访问某个列的,而访问路径的增加又意味着你必须使用统计信息来辅助,于是一大堆的问题就出现了。

没有索引,没有统计计划,没有执行计划,这就是kv数据库。

redis里如何满足以上的需求呢? 对于求最新的N条数据的需求,链表的后进后出的特点非常适合。我们在上面的登录代码之后添加一段代码,维护一个登录的链表,控制他的长度,使得里面永远保存的是最近的N个登录用户。

1
2
3
4
#把当前登录人添加到链表里
ret = r.lpush("login:last_login_times", uid)
#保持链表只有N位
ret = redis.ltrim("login:last_login_times", 0, N-1)

这样需要获得最新登录人的id,如下的代码即可

1
last_login_list = r.lrange("login:last_login_times", 0, N-1)

另外,求登录次数最多的人,对于排序,积分榜这类需求,sorted set非常的适合,我们把用户和登录次数统一存储在一个sorted set里。

1
2
3
zadd login:login_times 5 1
zadd login:login_times 1 2
zadd login:login_times 2 3

这样假如某个用户登录,额外维护一个sorted set,代码如此

1
2
#对该用户的登录次数自增1
ret = r.zincrby("login:login_times", 1, uid)

那么如何获得登录次数最多的用户呢,逆序排列取的排名第N的用户即可

1
ret = r.zrevrange("login:login_times", 0, N-1)

可以看出,DEV需要添加2行代码,而DBA不需要考虑索引什么的。

TAG系统

tag在互联网应用里尤其多见,如果以传统的关系型数据库来设计有点不伦不类。我们以查找书的例子来看看redis在这方面的优势。

关系型数据库的设计

两张表,一张book的明细,一张tag表,表示每本的tag,一本书存在多个tag。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
mysql> select * from book;
+------+-------------------------------+----------------+
| id | name | author |
+------+-------------------------------+----------------+
| 1 | The Ruby Programming Language | Mark Pilgrim |
| 1 | Ruby on rail | David Flanagan |
| 1 | Programming Erlang | Joe Armstrong |
+------+-------------------------------+----------------+

mysql> select * from tag;
+---------+---------+
| tagname | book_id |
+---------+---------+
| ruby | 1 |
| ruby | 2 |
| web | 2 |
| erlang | 3 |
+---------+---------+

假如有如此需求,查找即是ruby又是web方面的书籍,如果以关系型数据库会怎么处理?

1
2
select b.name, b.author  from tag t1, tag t2, book b
where t1.tagname = 'web' and t2.tagname = 'ruby' and t1.book_id = t2.book_id and b.id = t1.book_id

tag表自关联2次再与book关联,这个sql还是比较复杂的,如果要求即ruby,但不是web方面的书籍呢?

关系型数据其实并不太适合这些集合操作。

redis的设计

首先book的数据肯定要存储的,和上面一样。

1
2
3
4
5
6
7
set book:1:name     "The Ruby Programming Language"
Set book:2:name "Ruby on rail"
Set book:3:name "Programming Erlang"

set book:1:author "Mark Pilgrim"
Set book:2:author "David Flanagan"
Set book:3:author "Joe Armstrong"

tag表我们使用集合来存储数据,因为集合擅长求交集、并集

1
2
3
4
sadd tag:ruby 1
sadd tag:ruby 2
sadd tag:web 2
sadd tag:erlang 3

那么,即属于ruby又属于web的书?

1
inter_list = redis.sinter("tag.web", "tag:ruby")

即属于ruby,但不属于web的书?

1
inter_list = redis.sdiff("tag.ruby", "tag:web")

属于ruby和属于web的书的合集?

1
inter_list = redis.sunion("tag.ruby", "tag:web")

简单到不行阿。

从以上2个例子可以看出在某些场景里,关系型数据库是不太适合的,你可能能够设计出满足需求的系统,但总是感觉的怪怪的,有种生搬硬套的感觉。

尤其登录系统这个例子,频繁的为业务建立索引。放在一个复杂的系统里,ddl(创建索引)有可能改变执行计划。导致其它的sql采用不同的执行计划,业务复杂的老系统,这个问题是很难预估的,sql千奇百怪。要求DBA对这个系统里所有的sql都了解,这点太难了。这个问题在oracle里尤其严重,每个DBA估计都碰到过。对于MySQL这类系统,ddl又不方便(虽然现在有online ddl的方法)。碰到大表,DBA凌晨爬起来在业务低峰期操作,这事我没少干过。而这种需求放到redis里就很好处理,DBA仅仅对容量进行预估即可。

未来的OLTP系统应该是kv和关系型的紧密结合。

转自:http://www.hoterran.info/redis_kv_design

Create=1

Read=2

Update=4

Delete=8

YourPermissions=(Create|Delete)你的权限 这里是增加删除

判断增加权限 Create==(YourPermissions&Create)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
尼玛程序员也有不是秃顶好不好!!!!!
头发稀疏的也是有的好不好!!!
我没有在小黑屋里工作!!!!!
人家在明亮的办公室里工作!!!
根本就没有一边扒薯片一边敲着键盘!!!!

看看人家外国的程序员!!
Spring的作者Rod Johnson拥有音乐学的博士学位的!!!!!!
也是你们伤不起的那一型耶!!!!!!!!!!!
怎么到了中国程序员都成猪了!!!!!!!!!!!

什么?!
你说Rod Johnson是秃顶?!
我擦嘞!!!!!!!!!!
你们学中文的没有秃顶啊!!!!!
你们学法语的没有秃顶啊!!!!!
你们学音乐的没有秃顶啊!!!!!
你们学体育的没有秃顶啊!!!!!
你们学金融的没有秃顶啊!!!!!
你们学新闻的没有秃顶啊!!!!!
学什么的没有秃顶啊!!!!!!!
为什么你们就都伤不起!!!!!!
就他妈程序员一定秃顶啊!!!!!

为什么我咆哮的这么整齐!!!!!
因为写程序要注意缩进的!!!!!
很整齐有没有!!!!!!!!!!
彬彬有礼有没有!!!!!!!!!
逻辑关系很明了有没有!!!!!!
不要再诽谤程序员秃顶了好不好!!

"那个人是做技术的,很冷血……"
冷你妹啊!!!!!!!!!!!!
不能用天然呆形容啊!!!!!!!
程序员又不是杀手!!!!!!!!
这个程序员不太冷!!!!!!!!
只是他妈的折翼了啊!!!!!!!
折翼了啊!!!!!!!!!!!!

学什么毕业了还要啃书的啊!!!!
尼玛程序员的技术参考书每周都出新的啊!!!!
卓越都买成VIP了啊!!!!!!!!!!
地铁上啃书不是为了装逼啊!!!!!!
400多页尼玛我乐意啊!!!!!!!!
ant你大爷啊!!!!!!
spring你大爷啊!!!!!
ibatis你大爷啊!!!!!!
hibernate你大爷啊!!!!!

你丫豆瓣100颗小豆牛逼啥啊!!!!!
一行代码啊!!!!!!!!!!!!!
你丫开心农场100级牛逼啥啊!!!!!
一行代码啊!!!!!!!!!!!!!
你丫wow拿橙装牛逼啥啊!!!!!!!
一行代码啊!!!!!!!!!!!!!
你丫QQ到了太阳牛逼啥啊!!!!!!
一行代码啊!!!!!!!!!!!!!
服务器硬盘坏了不就都傻逼了!!!
傻逼了啊!!!!!!!!!!!!
我们程序员也帮不了了啊!!!!!
程序员不会修硬盘啊!!!!!!!
接网线什么的也不会啊!!!!!!
我们不是网管啊!!!!!!!!!
我们只会Thinking in java啊!!!!
in java欧叶!!!!!!!!!!!

计算机专业的修电脑的孩纸你们也伤不起啊!!!!!
上辈子没事手贱折别人翅膀玩这辈子得报应啊!!!!
上学的时候别系姑娘专门让你修电脑啊!!!!!!!
自己还没摸几天电脑就要给人修电脑啊!!!!!!!
连盲打都没学会问你为什么音箱没声音啊!!!!!!
音箱没声音你不知道自己去后面看洞有没有插对啊!!
绿插头插到红插孔里怎么会给你出声音啊!!!!!!
买新硬盘设跳线也来问你啊!!!!
自己不会看说明书啊!!!!!!!
拿个磁盘拷进去一百多个mp3的快捷方式没法听也要说是计算机坏了啊!!!!!!
计算机好冤枉的啊!!!!!!

电脑可能出的问题至少八百多万种,有没有!!!!!!有没有!!!!!!
每种故障至少二十种导致原因,有没有!!!!!!有没有!!!!!!
课本教的东西跟这八百多万种问题一毛钱关系都没有啊!!!!!!
每次都要自己上网搜索解决方法啊!!!!!!
人家硬生生被逼成修电脑的好人啊!!!!!!
中的木马跟病毒都够给杀毒软件公司当资料库了啊!!!!!!

修电脑也罢了装电脑来问我价钱跟牌子算什么事啊!!!!!!
我又不是电脑城给人装机的小弟把所有牌子的特点跟价格都背得一清二楚啊!!!!!!

当年就不该选计算机专业啊!!!!!!
现在每天坐在电脑前面一坐就是8个小时啊!!!!!!
腰椎也突了啊!!!!!!
颈椎也歪了啊!!!!!!
腱鞘也畸形了啊!!!!!!
胳膊肘也生老茧了啊!!!!!!
脸也黄了啊!!!!!!
斑也长了啊!!!!!!
肥减不掉了啊!!!!!!
黑眼圈褪不掉啊!!!!!
这么苦逼的人森神马时候到尽头啊!!!!!!

好吧~我只好转了~

ipwry.dat相比qqwry.dat占用空间更小,我们可以将纯真IP数据库(qqwry.dat)转换成最新的IP数据库格式(ipwry.dat),两种格式都是CNSS大神发明。

下面是具体的转换方法:

1
2
3
4
5
6
7
8
9
10
11
ipwry 0.2.2c
使用说明:
-i [ --input ] arg (=qqwry.dat)  : 输入文件
-o [ --output ] arg (=ipwry.dat) : 输出文件
-l [ --lzma ]                    : 使用LZMA压缩数据
-d [ --discard ] arg             : 要丢弃的地理地址
-f [ --ipsearcher ]              : 生成与此程序版本相配的ipsearcher.dll
-g [ --gbwry ]                   : 生成gbwry.dat文件
-t [ --text ]                    : 生成文本文件
-v [ --ver ]                     : 显示版本
-h [ --help ]                    : 帮助

制作方法:

1、将下面的代码保存为 *.bat 后缀的文件,如“IPwry.bat”

1
2
3
4
5
6
@Echo Off
title qqwry.dat转换为ipwry.dat
IPwry -i QQwry.dat -o IPwry.dat -l
Echo IPwry.dat更新生成完毕
pause
Exit

2、将“QQwry.dat”及“IPwry.exe”等放在同一文件夹下。

3、执行“IPwry.bat”便可以生成CNSS格式的“IPwry.dat”IP文件了。

相关文件下载地址:

1、ipwry 0.2.2c

在下载页面选择“ipwry_0_2_2c.zip”这个

http://cosoft.org.cn/projects/ipwry/

附带“IPwry.bat”的ipwry_0_2_2c.zip压缩包

2、纯真数据库

http://www.cz88.net/

附带源码:IPwry0_2_2

处理类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
public class Handshake {
private byte crlf13 = (byte) 13; // '\r'

private byte crlf10 = (byte) 10; // '\n'

private InputStream input;

Map http = new HashMap();

/**
*
* @param input
* 输入流
*/
public Handshake(InputStream input) {
this.input = input;
}

public byte[] getResponse() throws IOException {
StringBuffer header = new StringBuffer();
byte[] content = new byte[8];
byte[] crlf = new byte[1];
int crlfNum = 0; // 已经连接的回车换行数 crlfNum=4为头部结束

// 读取头部
while (input.read(crlf) != -1) {
if (crlf[0] == crlf13 || crlf[0] == crlf10) {
crlfNum++;
} else {
crlfNum = 0;
} // 不是则清
header.append(new String(crlf, 0, 1)); // byte数组相+
if (crlfNum == 4) {
input.read(content); // 读取内容
break;
}
}

String[] hhh = header.toString().split("\r\n");
http.put("Method", hhh[0].split(" ")[0]);
http.put("Path", hhh[0].split(" ")[1]);
http.put("Http-Protocol", hhh[0].split(" ")[2]);

http.put("Upgrade", header.substring(header.indexOf("Upgrade: ") + 9)
.split("\r\n")[0]);
http.put("Connection", header.substring(
header.indexOf("Connection: ") + 12).split("\r\n")[0]);
http.put("Host", header.substring(header.indexOf("Host: ") + 6).split(
"\r\n")[0]);
http.put("Origin", header.substring(header.indexOf("Origin: ") + 8 )
.split("\r\n")[0]);
http.put("Sec-WebSocket-Key1", header.substring(
header.indexOf("Sec-WebSocket-Key1: ") + 20).split("\r\n")[0]);
http.put("Sec-WebSocket-Key2", header.substring(
header.indexOf("Sec-WebSocket-Key2: ") + 20).split("\r\n")[0]);
http.put("Content", new String(content));

String key1 = http.get("Sec-WebSocket-Key1");
String key2 = http.get("Sec-WebSocket-Key2");
// 数字/空格数
long a = Long.parseLong(filterNonNumeric(key1))
/ filterNonSpace(key1).length();
long b = Long.parseLong(filterNonNumeric(key2))
/ filterNonSpace(key2).length();
// 转换为十六进制字符串
String ekey1 = Long.toHexString(a).toUpperCase();
String ekey2 = Long.toHexString(b).toUpperCase();
String ekey3 = bytes2HexStr(content);

// 补零
while (ekey1.length() < 8 )
ekey1 = "0" + ekey1;
while (ekey2.length() < 8 )
ekey2 = "0" + ekey2;
while (ekey3.length() < 8 )
ekey3 = "0" + ekey3;

byte[] bb = hexStr2Bytes(ekey1 + ekey2 + ekey3);
byte[] challenge = null;
try {
challenge = MessageDigest.getInstance("MD5").digest(bb);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}

StringBuffer sb = new StringBuffer();
sb.append("HTTP/1.1 101 WebSocket Protocol Handshake\r\n");
sb.append("Upgrade: WebSocket\r\n");
sb.append("Connection: Upgrade\r\n");
sb.append("Sec-WebSocket-Origin: " + http.get("Origin") + "\r\n");
sb.append("Sec-WebSocket-Location: ws://" + http.get("Host")
+ http.get("Path") + "\r\n\r\n");

return addByte(sb.toString().getBytes(), challenge);
}

public byte[] getMsg() throws IOException {
byte[] bbbb = null;
byte[] crlf = new byte[1];
while (input.read(crlf) != -1) {
// 处理任务
if (crlf[0] == (byte) 0) {
bbbb = new byte[] {};
}
bbbb = addByte(bbbb, crlf);
if (crlf[0] == (byte) 255) {
break;
}
}
return bbbb;
}
}

工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
public class Utils {

/**
* 数组相+
*
* @param target
* 目标数组
* @param src
* 加入数组
* @return 相加后的结果
*/
public static byte[] addByte(byte[] target, byte[] src) {
byte[] a = new byte[target.length + src.length];
for (int i = 0; i < target.length; i++) {
a[i] = target[i];
}
for (int j = target.length; j < a.length; j++) {
a[j] = src[j - target.length];
}
return a;
}

/**
* 过滤掉非数字的字符
* 例如:str="uis sdj13 e8 kj*ks90ao",则返回"13890"
*
* @param str
* @return 过滤后的字符串.如果str为空,则直接返回str
*/
public static String filterNonNumeric(String str) {
if (str == null || str == "") {
return str;
}
StringBuffer sb = new StringBuffer();
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if (Character.isDigit(c)) {
sb.append(c);
}
}
return sb.toString();
}

/**
* 过滤掉非空格的字符
* 例如:str="uis sdj13 e8 kj*ks90ao",则返回" "
*
* @param str
* @return 过滤后的字符串.如果str为空,则直接返回str
*/
public static String filterNonSpace(String str) {
if (str == null || str == "") {
return str;
}
StringBuffer sb = new StringBuffer();
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if (" ".equals(String.valueOf(c))) {
sb.append(c);
}
}
return sb.toString();
}

/**
* byte[]转换成十六进制字符串
* 例如:b=new byte[]{0,(byte) 255},则返回"00FF"
*
* @param b
* byte数组
* @return 大写十六进制字符串
*/
public static String bytes2HexStr(byte[] b) {
StringBuffer hs = new StringBuffer();
String stmp = "";
for (byte n : b) {
stmp = Integer.toHexString(n & 0XFF);
hs.append((stmp.length() == 1) ? "0" + stmp : stmp);
}
return hs.toString().toUpperCase();
}

/**
* 十六进制字符串转换成byte[]
* 例如:src="00FF",则返回new byte[]{0,(byte) 255}
*
* @param src
* 大写十六进制字符串
* @return byte数组
*/
public static byte[] hexStr2Bytes(String src) {
int l = src.length() / 2;
byte[] ret = new byte[l];
for (int i = 0; i < l; i++) {
ret[i] = (byte) Integer.parseInt(src.substring(i * 2, i * 2 + 2),
16);
}

return ret;
}
}

完成了协议分析的工作。

如果我们在Hibernate中需要查询多个表的不同字段,那么如何来获取Hibernate多表查询的结果呢?有两种方式:

1、 对各个字段分别转化成对应类型,如下:

Java代码:

1
2
3
4
5
6
7
8
9
10
Query q = session.createQuery(" select members, classInfo.className "
+ " from Members members, ClassInfo classInfo "
+ " where members.level = classInfo.classCode ");
List result = q.list();
Iterator it = result.iterator();
while (it.hasNext()) {
Object[] tuple = (Object[]) it.next();
Members members = (Members) tuple[0];
String className = (String) tuple[1];
}

这是获取Hibernate多表查询的结果的最常用的方式。

2、构造自己的复合类型,如下:

Java代码:

1
2
3
4
Query q = session
.createQuery(" select new NewMembers(members, classInfo.className) "
+ " from Members members, ClassInfo classInfo "
+ " where members.level = classInfo.classCode ");

当然我们需要有一个NewMembers类和相应的构造方式。以上便是两种用于获取Hibernate多表查询的结果的方法以及其相应的代码。

我胡汉三又回来了!!


La Isla Bonita - Alizee

艾莉婕Alizee小档案:
生日: 8月21日
年龄:1984生
星座: 狮子座
出生地: 法国南
兴趣: 绘画、舞蹈

出生于法国南部的海港-阿亚丘Ajaccio、柯西嘉岛上一个被地中海的阳光晒得刺眼却美得令人震慑的城市,1984年8月21日生日、狮子座的Alizee从小就在这里长大。也许是这儿的一切过于美好,她那过人的艺术天份与浑然天成的美,不自觉地就在她体内发酵、成熟、蕴生。

几百年一遇的日全食,出现在中国。很幸运的我就在日全食可以看到的杭州。

记忆最深的是钻石戒指般的那个不知叫啥的

附上照片

 http://picasaweb.google.com/jumkey/tTwyrH

照片没有拍好。没有专业的工具。

实际景象绝对比照片震撼!

成绩查询结果
准考证号: 911533030032 姓名: 陈XX
出生年月: 19880820 性别: 报考级别: 软件设计师
上午成绩: 49 下午成绩: 41
注:其中 -1为缺考,-2为作弊。