作者:Lightning@小宝
发布时间:October 21, 2009
分类:互联网系统架构
1.1. LVS介绍
LVS 是LINUX VIRTUL SERVER的简称,是由章文嵩博士主持的著名开放源码项目,一个实现“三高”系统的解决方案。LVS旨在解决高速发展的Web商务中日益凸现的问题:如 何在有限资金投入的情况下,最大幅度的提高Web站点的潜在服务性能。核心就是通过一组服务器来进行负载均衡,通过前端的负载调度器(Load Balancer),无缝地将网络请求调度到真实服务器上,从而使得服务器集群的结构对客户是透明的,客户访问集群系统提供的网络服务就像访问一台高性 能、高可用的服务器一样。客户程序不受服务器集群的影响不需作任何修改。
系统的伸缩性通过在服务机群中透明地加入和删除一个节点来达到,通过检测节点或服务进程故障和正确地重置系统达到高可用性。由于我们的负载调度技术是在Linux内核中实现的,我们称之为Linux虚拟服务器(Linux Virtual Server)。
LVS的设计根据透明性、性能、高可用性、可管理性和可编程性的指导思想实现的。
我们把前面那台负载均衡机器叫做:director server(DR)。后面的实际服务机器叫做:real server(RS)
1.2. LVS的三种负载均衡模式
调度器的实现技术中,IP负载均衡技术是效率最高的,IP虚拟服务器软件(IPVS)是在linux内核中实现的。
1.2.1. NAT模式
NAT 用法本来是因为网络IP地址不足而把内部保留IP地址通过映射转换成公网地址的一种上网方式(原地址NAT)。如果把NAT的过程稍微变化,就可以成为负 载均衡的一种方式。原理其实就是把从客户端发来的IP包的IP头目的地址在DR上换成其中一台REAL SERVER的IP地址并发至此REAL SERVER,而REAL SERVER则在处理完成后把数据经过DR主机发回给客户端,DR在这个时候再把数据包的原IP地址改为DR接口上的IP地址即可。期间,无论是进来的流 量,还是出去的流量,都必须经过DR。
1.2.2. IP隧道模式
隧道模式则类似于VPN的方式,使用网络分层的原理,在从客户端发来的数据包的基础上,封装一个新的IP头标记(不完整的IP头,只有目的IP部)发给REAL SERVER,REAL SERVER收到后,先把DR发过来的数据包的头给解开,还原其数据包原样,处理后,直接返回给客户端,而不需要再经过DR。需要注意的是,由于REAL SERVER需要对DR发过来的数据包进行还原,也就是说必须支持IP TUNNEL协议。所以,在REAL SERVER的内核中,必须编译支持IP TUNNEL这个选项。IP TUNNEL也在Networking options里面,如下图所示。
1.2.3. 直接路由模式
直接路由模式比较特别,很难说和什么方面相似,前2种模式基本上都是工作在网络层上(三层),而直接路由模式则应该是工作在数据链路层上(二层)。其原理为,DR和REAL SERVER都使用同一个IP对外服务。但只有DR对ARP请求进行响应,所有REAL SERVER对本身这个IP的ARP请求保持静默。也就是说,网关会把对这个服务IP的请求全部定向给DR,而DR收到数据包后根据调度算法,找出对应的REAL SERVER,把目的MAC地址改为REAL SERVER的MAC并发给这台REAL SERVER。这时REAL SERVER收到这个数据包,则等于直接从客户端收到这个数据包无异,处理后直接返回给客户端。由于DR要对二层包头进行改换,所以DR和REAL SERVER之间必须在一个广播域,也可以简单的理解为在同一台交换机上。
1.3. LVS的安装和配置
LVS的安装在其官方网站上有详细的说明文档。网址是:http://www.austintek.com/LVS/LVS-HOWTO/HOWTO/ 。不过一般说来以下网址http://www.austintek.com/LVS/LVS-HOWTO/mini-HOWTO/LVS-mini-HOWTO.html就足够了。
1.3.1. 让DR内核支持 IPVS
2.6及以上内核中已经支持了IPVS功能。至于2.4的内核则看版本,2.4.23及以上的内核已经有了ipvs,所以不必且不能打包,如果打了就会编 译出错。对于2.4.23以下内核则必须打IPVS补丁。2.4.23/24的内核有问题,所以不建议使用这两种内核。
IPVS只对DR需要,而RS则不需要。
Ipvs补丁和ipvsadm的下载地址:http://www.linuxvirtualserver.org/software/ipvs.html.
1.3.1.1. 打补丁来支持ipvs
对于2.4.23以下内核需要这个步骤,否则请按1.3.1.2步骤。
到http://www.linuxvirtualserver.org/software/ipvs.html下载对应的补丁。假设你使用的内核是2.4.18, 那么下载Version 1.0.10,包为ipvs-1.0.10.tar.gz。解包后阅读README文件,按中其中的方法打包。不过我对这里的说明有点不明白。下面我说说我的方法:
cd
make patchkernel
make installsource
或者
cd /usr/src/linux
cp /linux_kernel_ksyms_c.diff ./
cp /linux_net_netsyms_c.diff ./
patch –p1 < linux_kernel_ksyms_c.diff
patch –p1 < linux_net_netsyms_c.diff
然后cd /usr/src/linux
make dep; make bzImage;
接着重新copy新的内核文件,然后lilo,然后reboot
1.3.1.2. 重新配置内核选项来支持IPVS
对于2.4.23及以上的内核已经有了ipvs,所以你可以直接在内核中配置来支持IPVS。
cd /usr/src/linux
make menuconfig
对于不同版本的内核来说,各个配置项的路径是不一致的。对于2.6.8.1菜单路径是:Device Drivers->Networking support -> Networking options->IP: Virtual Server Configuration。而在2.4.30的内核中Networking options就在第一层路径下面。请到目的菜单Networking options下面,如下图所示:
如果你需要IPVS支持IP隧道模式,那么请选择下面这两个选项:
然后再选择:IP: Virtual Server Configuration这个选项。这时出现下图:
请选中所有的选项。
要 关注的是IPVS connection table size,这个东西意思是每个连接的hash table的大小,大家知道通常hash表越大,一次命中的机会越大。这里取值范围是2-20,2的2次方到2的20次方。有的资料建议用20,个人认为 默认值就足够了。由于每个连接需要128BYTE,每个HASH位需要8BYTE,所以(128 + 8 ) * 2^20 = 128M + 8 M = 136M,对于我们的系统没问题。
最 后的是IPVS application helper,这种是专门针对多次连接的情况的(可以理解为session),大家都知道一次FTP通讯需要2次连接,所以必须保证这两次都在一台服务器 上,否则将连接失败。这是个明显针对协议,或者说针对应用层的分类。有点七层交换的味道,希望这个功能尽快把HTTP的SESSION给加上,到时就不用 使用后台同步SESSION或者对原地址做HASH定向处理了。
这里负载调度算法的意思请具体参考:http://www.linuxvirtualserver.org/zh/lvs4.html
make dep; make bzImage;
接着重新copy新的内核文件,然后lilo,然后reboot。启动时可以看到ipvs提示信息。
1.3.2. RS的NOARP问题解决
直接路由模式肯定要处理NOARP的问题,官方文档《LVS-mini-HOWTO》3.7节指出在如下情况下必须处理:
IF( (you are using LVS-DR or LVS-Tun on the director)
AND
(you are running a Linux 2.2.x, 2.4.x, 2.6.x kernel on a realserver)
AND
(
the VIP on the realserver is on an ethernet device eg lo:0, tunl0:0
i.e. packets to the VIP are not being accepted by transparent proxy
)
AND
(
the realservers can answer arp requests from
the client/router (the realservers are on the same
piece of wire|network|segment as the director)
) )
THEN { YOU MUST HANDLE THE ARP PROBLEM }
FI
对于这断话的,我不太理解。下面引用其它的文档说明。
在 使用LVS中的DR与IP Tunnel的时候,会需要有一块网络卡要设定两个IP的情形,但是Linux在2.2.14之后,就将eth0:1的-NOARP这个FLAG关闭。也 就是说在kernel 2.2.14以后,eth0:1就视为eth0的别名,任何对eth0:1的设定也同样作用在eth0,换句话说,我对eth0:1下-NOARP,同样 也会对eth0有作用,这样会使得整张网络卡收不到封包。
在 直接路由、IP隧道模式下,因为我所有的机器都放在同一个网段,当该网段的Router接收到客户端(Client)对虚拟IP(Virtual IP)的TCP connection要求时,会先在网段中利用Arp request询问谁有VIP的地址,而包含Director与RealServers上所有的interface(不管Primary还是 Subinterface),只要他有那个ip,都会发送arp reply回去,造成网段内所有拥有Virtual IP的interface都会reply给Router,最后结果就是看谁的速度快,Router就将该封包送给谁,如此会造成LVS的Server并无 法发挥其效果,因此需要利用hidden这个pattch,将Subinterface上的Virtual IP给隐藏起来,如此他就不会对Arp Request进行Reply,如此就可以解决ARP的问题,而这个NOARP的问题,kernel发展小组认为不重要,所以以后都不会修改,要用请自行 编译。
所以从http://www.ssi.bg/~ja/#hidden下载对应的补丁,对于2.6.8.1的内核我选择hidden-2.6.4-1.diff。
cp hidden-2.6.4-1.diff /usr/src/linux
cd /usr/src/linux
patch –p1 < hidden-2.6.4-1.diff
make dep;make bzImage;
copy新的内核文件到相应的启动目录。再lilo。最后reboot。
当然解决noarp的问题,不止这一种。譬如leofan在内核中通过以下四个命令解决,前提条件是有这些设备:
echo "1">/proc/sys/net/ipv4/conf/lo/arp_ignore
echo "2">/proc/sys/net/ipv4/conf/lo/arp_announce
echo "1">/proc/sys/net/ipv4/conf/all/arp_ignore
echo "2">/proc/sys/net/ipv4/conf/all/arp_announce
不过我在2.6.8.1的内核中试了一下好像不行。Lvs把包固定发给了rs1这台机器,估计rs1arp reply响应最快。
1.3.3. 安装ipvsadm
这是一个IPVS的管理工具,提供了一个和IPVS打交道的接口,包括配置IPVS和连接信息统计。到地址:http: //www.linuxvirtualserver.org/software/ipvs.html去下载相应的版本。对于2.6.8.1内核,下了 ipvsadm-1.24-5.src.rpm。
rpm ipvsadm的存放路径/ipvsadm-1.24-5.src.rpm
cd /usr/src/rpm/SOURCE/
tar –zxvf ipvsadm-1.24.tar.gz
cd ipvsadm-1.24
make ; make install
然后:
ipvsadm,如果出现以下提示说明已经安装成功。
IP Virtual Server version 1.2.0 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
ipvsadm的具体用法可参考 man ipvsadm.
1.3.4. Ipvsadm的配置
根据http://www.austintek.com/LVS/LVS-HOWTO/mini-HOWTO/LVS-mini-HOWTO.html4 节和5结节的说明,Ipvsadm的配置可以用官方的配置脚本配置,配置脚本可在http://www.austintek.com/LVS/下载,也可 以手工敲入命令配置。
官方的脚本使用perl写的,配置脚本的命令为包中文件名为cofigure的文件, 包中的lvs_dr.conf.one_NIC_one_network等文件是一些配置信息,作为configure的参数传入.下面这个命令对应两台real server的配置, 需要自己修改lvs_dr.conf.one_NIC_two_network中的信息.
./configure lvs_dr.conf.one_NIC_two_network
不过本人试了一下自动配置没有成功,就手工配置了.读者有兴趣可自行研究如何使用官方配置脚本配置。
1.3.4.1. DR的配置
对于前台负载均衡服务器Direcrt Server(DR)的配置,我自己做一个配置脚本 config_vs.sh。里面的参数请自己替换。
#/bin/sh
#file: config_vs.sh 这里默认使用网卡eth0,如果想用其它网卡请自行替换即可。
VIP=172.30.31.200 #DR的虚拟服务IP
DIP=172.30.31.68 #DR的eth0真实地址
RS1=172.30.31.66 #real server1的真实地址
RS2=172.30.31.67 #real server2的真实地址
GW=172.30.31.1 #DR使用的网关或路由的地址
SERVICE=22 #你的服务端口
cat /proc/sys/net/ipv4/ip_forward #脚本调试信息
echo "0" >/proc/sys/net/ipv4/ip_forward
echo "1" >/proc/sys/net/ipv4/conf/all/send_redirects #脚本调试信息
cat /proc/sys/net/ipv4/conf/all/send_redirects #脚本调试信息
echo "1" >/proc/sys/net/ipv4/conf/default/send_redirects
cat /proc/sys/net/ipv4/conf/default/send_redirects#脚本调试信息
echo "1" >/proc/sys/net/ipv4/conf/eth0/send_redirects
cat /proc/sys/net/ipv4/conf/eth0/send_redirects #脚本调试信息
/sbin/ifconfig eth0 ${DIP} #配置所使用的网卡IP
#配置虚拟服务IP
/sbin/ifconfig eth0:0 ${VIP} broadcast ${VIP} netmask 255.255.255.255
/sbin/route add -host ${VIP} dev eth0:0 #添加虚拟IP的路由
/sbin/route add default gw ${GW} #添加本地路由
/sbin/ifconfig eth0:0 #脚本调试信息, 显示虚拟IP配置
#/bin/ping -b ${VIP} #脚本调试信息, 看看虚拟IP是否可以ping通
/sbin/route -n #脚本调试信息,显示路由信息
/sbin/ipvsadm –C #清除虚拟服务器表,也就是清除IPVS的原配置
#添加服务及轮调调度算法,-t:tcp服务,-A:添加服务; -s rr:指定调度算法为轮调
/sbin/ipvsadm -A -t ${VIP}:${SERVICE} -s rr
#添加real server, -a:添加一台RS, -t:tcp服务,-r: RS的地址,-g:直接路由模式。
#这里语句个数随real server的个数而定
/sbin/ipvsadm -a -t ${VIP}:${SERVICE} -r ${RS1} -g
/sbin/ipvsadm -a -t ${VIP}:${SERVICE} -r ${RS2} -g
ping -c 1 ${RS1} #脚本调试信息
ping -c 1 ${RS2} #脚本调试信息
/sbin/ipvsadm #脚本调试信息
#end of config_vs.sh
补:由于DR是个单点,为了保证高可用幸,我们使用了开源包Heartbeat来保证。所以有两台DR,一台为主,一台为辅。各自都可用这个脚本来配置,不过DIP要该成各自的真实地址,其它选项必须完全相同。
1.3.4.2. RS的配置
对于real server(RS)的配置,我自己做一个配置脚本 config_rs.sh。里面的参数请自己替换。
#/bin/sh
#file: config_rs.sh这里默认使用网卡eth0,如果想用其它网卡请自行替换即可。
VIP=172.30.31.200 #DR的虚拟服务IP
#DR的eth0真实地址,作用是在配置时测试DR的IP是否有效。但是DR切换后,DIP变了,所以这项取消了。
#DIP=172.30.31.68
LOCALIP=172.30.31.66 #real server1的真实地址
GW=172.30.31.1 #real server使用的网关或路由的地址
/sbin/ifconfig eth0 ${LOCALIP} #配置eth0的本地IP
/sbin/route add default gw ${GW} #添加默认路由
/bin/netstat -rn #脚本调试信息
ping -c 1 ${GW} #脚本调试信息,是否可以ping通网关
echo "0" >/proc/sys/net/ipv4/ip_forward
cat /proc/sys/net/ipv4/ip_forward #脚本调试信息,ip_forward是否配置正确
ping -c 1 ${DIP} #脚本调试信息, 是否可以ping通DR的真实地址
ping –c 1 ${VIP} #是否可以ping通虚拟DR的IP
#按照real server的虚拟IP
/sbin/ifconfig lo:0 ${VIP} broadcast ${VIP} netmask 0xffffffff up
#配置出口
/sbin/ifconfig lo:0
#在lo:0上添加去虚拟IP的路由
/sbin/route add -host ${VIP} dev lo:0
/bin/netstat -rn #脚本调试信息
echo "1" >/proc/sys/net/ipv4/conf/all/hidden
cat /proc/sys/net/ipv4/conf/all/hidden #脚本调试信息
echo "1" >/proc/sys/net/ipv4/conf/lo/hidden
cat /proc/sys/net/ipv4/conf/lo/hidden #脚本调试信息
#end of config_rs.sh
不同real server配置时只要改变LOCALIP选项就可以了。我只添加了172.30.31.66(real server 1)和172.30.31.67(real server 2)两台机器。DR的主节点名称为heatbeat1,副节点名称为heartbeat2
1.4. 试运行结果
安装上面的说明走完所有的步骤后,我用ssh测试了一下。奇数次去了172.30.31.66这台机器,偶数次去了172.30.31.67这台机器
来源:http://blog.csdn.net/harbor1981/archive/2006/10/31/1358925.aspx
作者:Lightning@小宝
发布时间:September 2, 2009
分类:互联网系统架构
上篇对疯狂代码缓存配置进行了概要的设计,可能说的有点模糊了,有几个朋友发了几个问题探讨了下,这里有必要先澄清一个问题,和常见的缓存策略不同,我们的缓存策略将重点放在更新策略而不是只读策略上。只读缓存以及共性缓存策略性质实现的难度并不大,我们要解决的是非共性缓存,并发更新缓存,可扩充性缓存,分布式缓存更新运算的问题,而对于共性的东西的话我们可以很轻松的实现,而不必做太多的运算。
试想一个问题,对于一个多用户的并发的系统,如果对每个用户都维护一份缓存策略还要保证更新的及时性以及处理的必要性来说的话,我们很难想到一个有效的处理机制来维护每份(每用户)缓存的副本的,缓存的存储性质也决定了做分布式缓存策略处理的难度和分布式通讯更新的的难度,我们也很难尝试对于一些访问量很小且少有共性的页面实现有效的缓存命中率,比如某某用户的博客。
简单的总结了一下关于缓存策略讨论的重点
A. 基于海量非共性数据的缓存策略
B. 基于数据缓存级别并发更新的缓存策略
C. 基于数据并发存储的缓存策略
D. 基于分布式的缓存策略
E.基于搜索的缓存策略
我们这里不再赘谈关于页面静态化以及类似的问题,静态化的情况非常适合在系统初期,用户的基数并不算很大的情况下实现,而在涉及集群的情况下,静态化的实现成本,IO成本,维护成本,扩充成本以及更新成本会远远的超出缓存策略的成本,当然我们也会有一套建立在缓存基础上的静态化处理方案,这些放在以后再谈。我们的目的是要建立一个可伸缩,便于维护扩展的缓存策略,下面就具体问题进行分析。
对于问题A:
常见的博客系统就是一个最好的例子,每个用户的首页都是相对个性的数据,共性的地方不多,以常见的处理方案来说的话,我们可能需要维护每个用户访问的缓存副本,而对于一些访问量极小的博客站点来说的话这种方式无疑会造成巨大的浪费。
对于大量非共性的数据缓存来说,几个处理方案:
1) 量化缓存目标并分配相应的缓存权值。(权值分级)
目的很简单,只缓存有效的数据。首先抽取活跃用户,以及高访问量用户,将数据进行分组分权制缓存(对于交友型的SNS系统来说,我们称之为美女效应)
2) 非连接持久性的缓存保持(临时的持久性)
珍惜并有效利用数据查询,将未被缓存命中时的查询或者无权值的数据持久化保存(序列化存贮静态存贮等),当缓存未被命中时优先取得持久化数据而非数据查询。可以理解为临时数据存贮,或者临时存贮于子服务器的某个位置。
3) 基于数据更新的缓存清除(一次性使用)
当持久性缓存保持失效(依赖数据发生修改),直接删除临时数据(缓存只在访问时被激活并储存,一旦修改或者失效,我们立刻抛弃)。
4)缓存更新代理规则
由另外的线程进行维护,并维护线程的有效性,最大限度的分离主程序对无效缓存以及临时持久性缓存数据的清理
对于问题B:
在小型缓存策略中,缓存处理对于整个应用程序对于每个请求来说都是唯一的,可操作的和非物理存储的。而在并发更新的过程中,一个小小的并发更新就会很现实的清空所有的缓存池,造成缓存命中率奇低而初始化率奇高而起不到缓存策略应有的作用。
在这种情况下,处理方案也和A.4中提到的方案是一样的,由独立的缓存更新进程来处理,对于应用程序中所有涉及缓存更新的请求由专门的更新代理来执行。这个处理方案相对简单,不再赘述。
对于问题C:
上篇已经提到关于并发数据更新会带来的问题也就是数据库的I/O响应,超时,死锁,以及线程的阻塞问题。我们用一个写入缓存来处理这个方案,其实这个并非传统意义上的读缓存,姑且命名为写缓存吧,我们可以形象的理解为类似硬盘缓冲区的问题。这里处理的操作稍微有点多了,还要涉及只读缓存的更新的问题了。
根据系统的不同,我们需要分析处理的角度也不同,我们以常见的webgame为例来简单介绍一下处理机制,这里有两种常见的情况
1) 对于 webgame的最终用户玩家来说,每个在线用户的数据是非共性的(问题A),而在一个战斗场景下,每组数据时刻都在变化之中,如果我们对数据的变化采用数据库日志记录的形式保存的情况显然对Database的压力很大,而我们需要记录的仅仅是战斗的结果,战斗的过程我们完全没有必要进行保存,这个时候我们就用写入缓存来执行相应的数据操作。这个处理很简单,用服务器变量的形式就能解决他。
2) 对于 webgame的服务器角色来说,如果战斗场景的用户量非常多,而数据更新非常大的情况下,我们采用方法1中的处理也可能力不从心,这个时候我们可以将缓存来进一步的抽象,在某个时间段内(比如3分钟),维护一个唯一的缓存对象,所有的数据操作都在这个时间段来被缓存进程来记录,来更新。而由另外的一个进程来进行异步的定时的数据保存操作。
对于问题D
这个是比较常见的分布式缓存服务器组了,而对缓存服务器来说其实要解决的问题就是服务器间之间互相通讯的问题,并保证数据一致性的问题。那么我们的有四个处理规则:
1) 数据缓存应该被有效的分组并索引
目标是实现数据耦合的成都降到最低,甚至没有耦合。比如以用户ID为分割的数据缓存分布,或者以文章分类为分割的缓存分布
2) 数据缓存应该被有效的更新
如果数据被有效的分组完成后,这个就是问题C.2的方案了,和C.2不同的是,因为缓存组可能未必在一组服务器中,可能涉及缓存和DATABASE数据通讯延迟的问题。这个时候要保证缓存服务器被即时的传递到databse,那么需要另外的一个缓存检测进程来完成这项工作(数据完整性检查,并备份两个缓存段的数据)
3) 缓存服务器间的数据完整性
对于无法分组的数据,比如时间段内的用户认证数据和资料数据,我们需要保证两组数据同步,最好的处理方法就是清除相应的缓存段,让它在下次使用的时候初始化
4) 缓存服务器间的连通性
这个取决于物理线路,如果缓存服务器在天南地北的话,我们还需要一个队列进程来进行同步和数据矫正,我们称之为缓存路由。
对于问题E
在分布式缓存的情况下,多条件搜索往往涉及多个缓存服务器,处理起来笔者尚未有一套完善的出来方案。笔者用的是敷衍原则和集成原则了
敷衍原则:
对于搜索型的数据来说,很多情况下并不是非常重要,我们的搜索结果完全可以晚一会提供给用户,允许搜索的数据有10分钟或者更长时间的延迟。
集成原则
将搜索字段和表整合出来,用独立的只读查询服务器来分担负荷
作者:Lightning@小宝
发布时间:September 2, 2009
分类:互联网系统架构
上篇以用户数据表为例介绍了基本的数据分割方案以及基本的配置方案。但是在2.0时代,这种简单的列表索引已经远远实现起来是问题的,多对多关系将是最常见的关系。现在我们针对web2.0数据中广泛存在的多对多关系进行阐述和具体行为判断,比如一个很简单的例子,在2.0时代,好友功能是最常被用到的,每个用户会有很多的好友,同时也会是很多人的好友,那么这个数据量将会是用户数的平方的级别。同样,对于文章标签,每个文章可以有多个标签,而每个标签又可以有多个文章,这又是一个几何乘积,数据量又会是个天文数字。
传统的处理方案有两种,一种是通过SEARCH的方法来实现,一种是通过另建一个索引表,存贮对应的ID以进行存贮。对于第一种方案,因为要涉及大量的LIKE查询,性能不敢恭维,第二种的情况下,数据库的行的数量也是惊人海量级别的,并且要跨表跨区查询,还要维护数据的唯一性,数据处理过程相当的复杂性能也就不言而喻了。
文入正题,下面对数据多对多关系举出来具体的解决方案,我们这里以标签和文章之间的多对多关系为例来讲解,大家可以举一反三的思考群组和用户之间,相册和被圈用户之间等等复杂的多对多关系。
首先滤清一下流程,我们以传统方案的第二种为例,在传统的数据库设计中我们是如下走的:当一篇博文发布的时候并插入标签的时候一般是三步走(也可以理解为四步,以为还要判断标签是否存在的问题),第一步插入文章数据库并获取文章的ID,第二步插入标签数据库同时查询标签是否存在,如果存在就取出标签的ID,否则的话插入新标签并取出ID,第三部,将文章的ID和标签的ID插入索引表来建立关联。如果这个时候在索引表上建立了索引的话就是灾难性的,特别是在数据量大的情况下,尽管它可以有效的提高查询速度,但是发布的速度可能就会让人无法忍受了。
我们处理的方法也是三部曲,对多对多关系进行进一步的处理。
用标签的时候,我们用的最多的就是查询标签下的文章和显示文章的标签,所以我们实现这例就成了。
第一步,抛弃索引表。
对文章做冗余字段,加一个TAG列,我们可以讲TAG的标签如下写[TagID,TagName]| [TagID,TagName]| [TagID,TagName] 同样 对于TAG表,我们做如下冗余加个Article字段,如下内容[ArticleID,Title]| [ArticleID, Title]| [ArticleID, Title],在需要增加的时候我们只要APPEND一下就可以了,至于ARTICLE的结构和TAG的结构可以参考我上一篇文章的介绍。其实根据需要还可以存贮更多。
有人会问,为什么要存贮TagName和ArticleTitle呢,其实是为了避免跨表查询和INNERJOIN查询来做的,In查询和跨表查询会造成全表遍历,所以我们在执行的时候In查询是必须要找到一个有效的替代方法的。
第二部:异步加载。
在设计模式下我们常思考的是单件模式,我们采用另类的单件模式来处理,也就是把文章和标签之间的索引作为专门的进程来做,异步的实现。
为了避免文章在发布的时候以为要检查TAG表而造成的线程拥堵,我们需要采取延迟加载的方案来做。服务器应该维护一个进程专业的对标签和文章地段的查询和索引,我们在发布文章的时候应该把标签同步这一块托管给另外的一个程序进行处理,并进行索引。
第三部:标签缓存索引:
对于频繁的判断标签去或者热门的标签我们还可以组织一套有效的索引,比如对于标签“疯狂代码”和”傲博知识库”,我们用树来把它表示出来。对于疯狂代码我们索引一个疯,其实用程序表达就是疯狂代码[0],同样傲博知识库就是傲博知识库[0]。而在数组”疯”中存贮以疯开头的标签组,以”傲”的数组中存贮以”傲”开头的标签。如果量更大的话还可以再做二级索引。
这涉及另外一个话题了就是分词,上面是一个简单的分词方案,大家在进行GOOGLE搜索的时候应该很输入它的Suggest方法吧,就是这个道理。最终讲标签有效的索引,并提取热门的作为一个全局静态变量,我们就可以绕过数据查询这一关,对第二部的单件模式又是一个进化。
以上是对多对多关系的一个简单的架构说明,肯定有人会问,如果这样做的话工作量不是太大了吗,分词处理什么的,对每个多对多关系进行处理。
OK,咱们可以进一步的把它来抽象化,我们用TableA 表示Article表,用TagbleT表示Tag表,我们可以讲字段抽象化出来,也就是一个ID,一个Tag的String 同理对于标签表也是如此。朋友们应该可以理解我的意思了。
对,就是做个代码生成器把对应的多对多关系给生成出来,这个很好写的,几个Append就可以搞定。如果想更方便的处理,那么把这个东西做成单件的模式抽象化出来,然后再违反一下原则,做成基类,其他关系继承这个基类。。。。。剩下的应该很简单了,具体实现大家思考吧。
请参照
第二篇的文章进行进一步优化设计来实现更高的负载性能
下章接着讲述数据分割和散列方面的内容