使用Quartz处理定时任务

通常开发中提到定时任务,大多是说异步定期执行的批处理任务,比如夜里低峰时段的备份、统计,或者是每周、每月对数据库表进行整理(比如从每日流水表导入到周表、月表)

如果只有少量的定时任务,使用系统的crontab就足够了,这也是90%情况下的首选方案。但是当任务数量非常大,而且任务与任务之间有因果关系、先后顺序、竞争条件的话,crontab就无法满足需求了。据说,淘宝的量子统计平台组,内部就开发一套叫做“天网”的任务调度系统,看看这名字多霸气,电影《终结者》系列里的最终BOSS……听名字也可以想见,淘宝量子统计这种用户行为分析统计平台,该有多少异步任务、该有多复杂。

这种类型的定时任务模型的特点是,每个任务都有不同的业务流程,有不同的触发逻辑,但整体上,任务数量是有限的。另外有一种定时任务模型与此正好相反,“超时检测”,任务的执行流程相似,触发逻辑也相似,但数量非常巨大。举例来说,memcached是一个Key-Value的缓存,每一组值都可以设定超时时间,超时后值失效,但是memcached不需要做“超时检测”,它可以在客户端访问某个Key时查看是否过期,这是一种“被动超时检测”,而如果业务上需要某个Key过期后马上执行一段业务流程,就需要一种“主动超时检测”机制。这需要同时管理大量的定时器,而我目前项目在做的,正是这样一种模型,我选择了使用Quartz框架来进行辅助开发。

Quartz是一个使用Java开发的,用于处理定时任务的框架,对定时任务这样一个典型开发场景进行了抽象和封装,为开发者提供了简洁的API。

核心概念

1.Job:Job是任务执行的流程,是一个类

2.JobDetail:JobDetail是Job是实例,是一个对象,包含了该实例的执行计划和所需要的数据

3.Trigger:Trigger是定时器,决定任务何时执行

4.Scheduler:调度器,调度器接受一组JobDetail+Trigger即可安排一个任务,其中一个JobDetail可以关联多个Trigger

实例

1.初始化:

Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
scheduler.start();

当程序退出时,应该主动调用shutdown()方法关闭该调度器。

2.Job:

一个Job类需要实现org.quartz.Job接口,这个接口值需要实现一个方法即可:

void execute(JobExecutionContext context) throws JobExecutionException

context是重要的上下文,可以访问到关联的JobDetail对象和本次触发的Trigger对象,以及在此之上设定的数据。

3.JobDetail:

可以使用JobBuilder来构建一个JobDetail对象:

JobDetail job = JobBuilder.newJob(MyJob.class) // MyJob是我实现的Job类
	.withIdentity("myjob") // 可以给该JobDetail起一个id,便于之后的检索
	.requestRecovery() // 执行中应用发生故障,需要重新执行
	.storeDurably() // 即使没有Trigger关联时,也不需要删除该JobDetail
	.usingJobData("key1", "value1")
	.usingJobData("key2", "value2") // 以Key-Value形式关联数据
	.build();

Quartz因为考虑到有些任务不是幂等的,不可以多次重复执行,所以默认没有开启“requestRecovery”。当确认业务中允许一次任务执行两次的情况下,可以开启该选项,则任务肯定不会因为应用停止而漏调用,但缺点就是,有可能会重复调用。

每个JobDetail内都有一个Map,包含了关联到这个Job的数据,在Job类中,可以通过context取出该数据,进行业务流程处理。

4.Trigger:

可以使用TriggerBuilder来构建一个Trigger对象:

Trigger trigger = TriggerBuilder.newTrigger()
	.forJob("myjob") // 关联上述的JobDetail
	.withIdentity("myjob-trigger1") // 给该Trigger起一个id
	.startAt(DateBuilder.futureDate(20, IntervalUnit.SECOND)) // 延迟20秒开始
	.withSchedule(SimpleScheduleBuilder.repeatMinutelyForever()) // 每分钟触发一次,无限循环
	.usingJobData("key3", "value3")
	.usingJobData("key4", "value4") // 以Key-Value形式关联数据
	.build();

5.设定:

因为上述的Trigger已经关联了JobDetail,可以使用

scheduler.scheduleJob(trigger);

把这一组JobDetail和Trigger加载到调度器上,接下来就会按照计划执行Job任务。

6.配置文件:

配置文件不是必须的,Quartz对配置项都是有默认值的,当需要自定义的时候,可以在classpath路径下放一个quartz.properties文件,Quartz的StdSchedulerFactory在启动时会自动加载该配置文件。

比较值得关注的是这两个配置项:

org.quartz.threadPool.threadCount=50
org.quartz.scheduler.batchTriggerAcquisitionMaxCount=50

第一个配置项是线程池里的线程数,默认值是10,当执行任务会并发执行多个耗时任务时,要根据业务特点选择线程池的大小。

第二个配置是,当检查某个Trigger应该触发时,默认每次只Acquire一个Trigger,(为什么要有Acquire的过程呢?是为了防止多线程访问的情况下,同一个Trigger被不同的线程多次触发)。尤其是使用JDBC JobStore时,一次Acquire就是一个update语句,尽可能一次性的多获取几个Trigger,一起触发,当定时器数量非常大的时候,这是个非常有效的优化。当定时器数量比较少时,触发不是极为频繁时,这个优化的意义就不大了。

持久化

如果定时器在业务中属于关键数据,需要在故障重启后恢复状态,则需要把Quartz配置为持久化模式。默认情况下,所有定时任务和数据都保存在内存中,在应用重启后状态会消失。

JobStore决定了Quartz如何存储任务和触发器,默认值是org.quartz.simpl.RAMJobStore,我们需要把它配置为org.quartz.impl.jdbcjobstore.JobStoreTX,即可以使用JDBC数据源,把状态持久化到关系型数据库中。

用H2数据库进行举例,配置文件如下:

org.quartz.dataSource.DATA_SOURCE_NAME.driver=org.h2.Driver
org.quartz.dataSource.DATA_SOURCE_NAME.URL=jdbc:h2:quartz;MVCC=TRUE
org.quartz.dataSource.DATA_SOURCE_NAME.user=sa
org.quartz.dataSource.DATA_SOURCE_NAME.password=
org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.dataSource=DATA_SOURCE_NAME
# org.quartz.jobStore.useDBLocks=true

Quartz为了保证多线程下的一致性,使用了锁,但是使用了使用Lock表里的一行记录来模拟锁的形式来实现的,其实性能很糟糕,还不如使用默认的基于Java的Monitor锁的效果好,所以我注释掉了这个配置项。

之后,打开quartz-x.x.x.tar.gz包内的docs/dbTables文件夹内有各种数据库下的建表语句,直接在数据库中执行,把表先建好。

配置文件中增加上述对JobStore的设定后,代码不用修改一行,所有的任务和触发器已经是持久化的了,当应用停机重启后,错过的(misfire)任务和上次正在执行的(recovery)任务都会回复状态,如果JobDetail指定了requestRecovery,上次执行中,但没有执行完毕的任务会重新执行一遍。

MongoDB介绍

好久没写博客了,一晃半年多了,写文章这种事情还是需要灵感需要情绪需要思考储备的,故迟迟未能动笔。最近因工作需要,有一个子系统要使用MongoDB做存储,所以把官网上的文档完整看了一遍。话说自己胆子也不小,仅仅看了一上午文档,就开始和客户讨论、推动系统的设计了,并成功说服客户接受这个MongoDB的方案。

确实MongoDB和我在做的东西在业务上非常契合,我主要看重的是这几个特性:

  • 面向文档,无模式

首先谈一下什么是面向文档(Document-oriented),MongoDB里的“文档”概念,基本可以对照MySQL里的“行”,是一个基本存储单元。但不同的是,在MySQL里要表现“一个用户有若干个订单”这种“一对多”模型时,标准做法是一张user表,一张order表,order表有一列外键指向user的主键,表示“belong to”这个含义。而这种传统RDBMS的做法,其实是逆思维的,我们通常想表达“一对多”关系时,更希望从“一”的这端来持有管理权。MongoDB里的文档就顺应了正常人的思维,每一个存储单元是一个JSON对象(或者准确的说,是BSON),每个属性就相当于MySQL里的“列”,不同的是,每一列里既可以存单值,也可以存数组,甚至可以存嵌套的JSON对象。像上面的例子中,MongoDB下应该是有一个user集合(Collection),每一个对象都形似:{name:”zhangsan”,orders:[{id:201204063478},{id:201203289834}]},再有一个order集合,保存着每一个订单的详细信息。

建立在这种面向文档的结构基础之上,MongoDB是一个无模式(Schema Free)的数据库,不需要像MySQL那样提前定义表结构(列名、数据类型、约束……),而是可以插入任意的JSON对象,不做强制约束。当业务需求中不能在开发期确定属性的数据模型中,这个特性非常有用。

  • 原子更新操作

原子更新,最简单的例子就是递增计数,i++。严格意义上讲,i++操作分3步:读取i,i+1,把新值写回去。而在并发访问下,这3步会被打散,造成更新丢失,故必须加锁,或者进行事务控制。而MongoDB是采用的无事务、无锁的设计,没有显式的startTransaction、commit、rollback。而是提供了原子操作的原语,如$inc、$addToSet等,以及findAndModify指令来进行复杂条件的原子查询+更新操作。

  • Replicate Set,高可用

为了保证系统的高可用,数据库都会提供一些复制的机制,如Oracle的RAC,MySQL的主从复制。拿MySQL举例,开启了二进制日志,所有对主节点的写操作都会传递给从节点,实现数据的复制。一方面,是基于数据备份的目的,来保证数据的安全;另一方面,互联网应用基本都是多读少写,可以通过把读请求交由从节点处理,来扩展读的性能。虽然不能保证实时的事务级一致性,但可以保证最终一致性。

MongoDB是更先进的复制机制,同一个Replicate Set集群中,并不明确区分主节点和从节点,MongoDB会自发的进行选举,同时只会出现一个Primary节点,其他都是Secondary节点。之间会有定时的心跳检测来进行健康检查,当某台服务器故障失活后,集群会自动切换,保证任意一个节点宕机都不影响业务的正常执行。哪怕主节点宕机,其余的从节点也会自动重新选举出新的主节点,所有读写操作还可以顺利执行,只会有几秒钟时间的暂停服务。而所有的这些切换过程,都对客户端应用代码是透明的,驱动程序会自动找到哪台机器是现在的主节点。

MongoDB不但实现了数据的安全性,更是极大的减轻了运维团队的负担,很多以前人工执行的操作都由MongoDB服务端自动接管了。

  • Auto Sharding,水平扩展

随着业务增长,数据量也会随之上涨,总有一天单机会过载,需要通过加机器来水平扩展。传统RDBMS的水平扩展大体分为两种类型:应用层代码切分、数据库切分。前者就是通常说的“分库分表”,把不同的业务表放到不同的MySQL实例上,或者根据id、时间、地理区域等业务字段来分片,切分到不同的MySQL上,这种方式在应用层写代码会比较繁琐,而且涉及跨库事务,跨库查询时会更麻烦。后者就是使用数据库的分区(partition),指定分区字段和区间,由数据库来处理。或者使用Amoeba这类的mysql proxy来实现切分的功能也是一种选择。

MongoDB提供了更先进的机制:Auto Sharding,和MySQL的分区类似,也需要指定一个分区字段,但不需要指定区间。MongoDB的数据存储单元是块(Chunk),默认是64M,MongoDB会根据数据的分布情况来自动选择切分区段,把数据平均的分布到多台机器去。当运行一段时间,数据分布发生变化时,MongoDB会自动判断出这种情况,并以Chunk为单位迁移数据,把数据从一台过载的机器转移到另一台负载较轻的机器,以保证集群的负载平衡。整个这个过程对应用层代码都是透明的,不需要程序员去主动干预。

在这种机制下,程序员的负担大大的解放,扩展性能只靠加机器就可以无限扩展,而不需要改动任何一行代码。


MongoDB是一个NoSQL数据库,但相比Memcached和Redis这种缓存性质的内存数据库,要提供更多的功能,包括复杂查询、嵌套数据结构和服务端支持的复制+分片。从功能上讲,MongoDB更接近于MySQL,但从扩展性上讲,则更接近于NoSQL,可以说融合了二者的优点。它既有“非常SQL”的查询语言,where、order by、group by、limit、distinct,也有“非常KEY-VALUE”的文档模型,嵌套、数组、集合。

缺点呢,就是非常耗内存、吃硬盘,为了保证性能足够好,必须要在物理内存中可以容纳得下热点数据,否则频繁内存换页,性能会下降到冰点。另外,MongoDB还在快速发展中,很多特性还不是特别稳定,而且中文资料很少,很多问题需要去stackoverflow和quora上去挖。比如说Auto-Sharding机制,从资料上来看非常完美非常强大,但也有foursquare的失败案例在前,长达11小时的宕机。

目前项目中,为了保险期间,生产环境暂时不使用Auto-Sharding机制,而采用Pre-Splitting的切分方案,手工指定划分区间,指定MongoDB不进行自动平衡和迁移。

下面我要总结一下搭建Pre-Splitting分片模式的MongoDB集群的过程,介绍这个的中文文章不多,基本都是自己对着文档,查着stackoverflow搭出来的。

架构图:

生产环境的MongoDB集群,应该是如上的架构,shard1~4是4组Replicate Set,每组的3台机器数据是一样的,由Replicate机制来保持之间的同步,4片shard数据各不相同,分片规则有config server来进行持久化,mongos来进行路由和自动平衡。config server保存有非常重要的数据,包括分片节点ip和端口、分片区段规则、数据块分布等集群的元信息,其本身的负载极小,因为极少发生变化。但又由于它极为重要,容不得半点闪失,故要有3台冗余。mongos是路由节点,对于应用层代码来说,它就是一个代理,所有读写请求都发给一个mongos,它会自动把请求转发给对应的数据节点,如果查询操作没有指定分片条件的话,它还会自动Map/Reduce,把请求分散给所有节点并行处理,并汇总结果。应用层代码的视角来看,mongos就是一个独立的MongoDB节点,所有集群操作都透明的由它处理了。

为简化起见,我只配置两片shard、一台config、一台mongos,而且shard也是单机,不使用Replicate Set,都配置在同一台物理机器上,使用不同的数据目录、日志路径和监听端口。

启动2片shard、1个config、1个mongos:

mongod --shardsvr --port 27101 --dbpath /path/to/data/shard1 --logpath /path/to/logs/shard1.log
mongod --shardsvr --port 27102 --dbpath /path/to/data/shard2 --logpath /path/to/logs/shard2.log
mongod --configsvr --port 27103 --dbpath /path/to/data/config --logpath /path/to/logs/config.log
mongos --port 27104 --configdb localhost:27103 --logpath /path/to/logs/mongos.log

使用mongo客户端登录mongos,并连接admin数据库:

MongoDB shell version: 2.0.2
connecting to: localhost:27104/test
mongos> use admin
switched to db admin

添加两片shard:

mongos> db.runCommand({addShard:"localhost:27101",name:"shard1"})
{ "shardAdded" : "shard1", "ok" : 1 }
mongos> db.runCommand({addShard:"localhost:27102",name:"shard2"})
{ "shardAdded" : "shard2", "ok" : 1 }
mongos> db.runCommand({listShards:1})
{
        "shards" : [
                {
                        "_id" : "shard1",
                        "host" : "localhost:27101"
                },
                {
                        "_id" : "shard2",
                        "host" : "localhost:27102"
                }
        ],
        "ok" : 1
}

接下来要在test数据库的user集合上,启用Sharding机制,按userid字段切分:

mongos> db.runCommand({enableSharding:"test"})
{ "ok" : 1 }
mongos> db.runCommand({shardCollection:"test.user",key:{userid:1}})
{ "collectionsharded" : "test.user", "ok" : 1 }
mongos> db.printShardingStatus()
--- Sharding Status --- 
  sharding version: { "_id" : 1, "version" : 3 }
  shards:
        {  "_id" : "shard1",  "host" : "localhost:27101" }
        {  "_id" : "shard2",  "host" : "localhost:27102" }
  databases:
        {  "_id" : "admin",  "partitioned" : false,  "primary" : "config" }
        {  "_id" : "test",  "partitioned" : true,  "primary" : "shard1" }
                test.user chunks:
                                shard1  1
                        { "userid" : { $minKey : 1 } } -->> { "userid" : { $maxKey : 1 } } on : shard1 { "t" : 1000, "i" : 0 }

可以看到,user集合已经启用了Sharding,但分片规则还没有配置,当前所有新的数据会发送给shard1。

如果是希望配置为Auto-Sharding机制,交由MongoDB自动平衡数据,到这里就可以了。之后可以尝试一下循环插入几万条数据,再重新db.printShardingStatus()一下,可以看到MongoDB自动添加了若干条分片规则,shard1和shard2的chunks数基本差不多。

如果是需要配置为Pre-Splitting模式,禁用MongoDB自动平衡器,并手动指定规则还需要进行:

mongos> db.runCommand({split:"test.user",middle:{userid:100}})
{ "ok" : 1 }
mongos> db.runCommand({moveChunk:"test.user",find:{userid:100},to:"shard2"})
{ "millis" : 4019, "ok" : 1 }
mongos> db.printShardingStatus()
--- Sharding Status --- 
  sharding version: { "_id" : 1, "version" : 3 }
  shards:
        {  "_id" : "shard1",  "host" : "localhost:27101" }
        {  "_id" : "shard2",  "host" : "localhost:27102" }
  databases:
        {  "_id" : "admin",  "partitioned" : false,  "primary" : "config" }
        {  "_id" : "test",  "partitioned" : true,  "primary" : "shard1" }
                test.user chunks:
                                shard1  1
                                shard2  1
                        { "userid" : { $minKey : 1 } } -->> { "userid" : 100 } on : shard1 { "t" : 2000, "i" : 1 }
                        { "userid" : 100 } -->> { "userid" : { $maxKey : 1 } } on : shard2 { "t" : 2000, "i" : 0 }

可以看到,以及以userid 100为界限,所有userid小于100的用户都会被分配到shard1上,大于等于100的用户都会被分配到shard2上。

接下来一步也很重要,必须停止掉MongoDB内置的自动平衡器:

mongos> use config
switched to db config
mongos> db.settings.update({_id:"balancer"},{$set:{stopped:true}},true)
mongos> db.settings.find()
{ "_id" : "chunksize", "value" : 64 }
{ "_id" : "balancer", "stopped" : true }

好了,现在可以尝试一下插入userid为1~200的用户:

mongos> use test
switched to db test
mongos> for(var i=1;i<=200;i++) db.user.insert({userid:i})
mongos> db.user.count()
200

分别连接到shard1和shard2两个数据节点上验证一下,可以看到,1~99的用户分配到了shard1上,100~200的用户分配到了shard2上,符合之前配置的规则。

所有通过mongos的查询请求,如果where条件中包含分片字段(如userid),则mongos可以直接定位到对应shard数据节点,属于Targeted类型的查询;如果where条件中没有指定分片字段,则mongos会Map/Reduce,把请求分布到所有shard节点,并汇总返回数据,这属于Global类型的查询。具体描述可以参考这里的文档。

ProcessingJS介绍

最近由于工作的关系,好好研究了一下ProcessingJS。Processing是一门可视化编程语言,ProcessingJS是它的JavaScript实现,使用HTML5的canvas,配合现代浏览器来实现web客户端的可视化技术。(不得不说,John Resig真是个高产的作者,jQuery、Sizzle、ProcessingJS)

ProcessingJS做了这两件事:

1.把基于Java的Processing表述,转化为JavaScript代码

2.利用HTML5的canvas api,在浏览器上执行

下面是一个3D绘图的例子,如果你使用Chrome、Firefox、Safari(版本别太老就行,IE是无论什么版本都不行),就可以看到效果:(可以拖拽)

在没有HTML5之前,要实现类似的效果就必须要Flash或者Java Applet这样的浏览器插件来实现,而现在只要书写出基于Web标准的代码,就可以跨平台跨浏览器的实现非常优秀的用户体验和界面效果。

而直接书写Canvas API,又过于繁琐过于底层了,Processing可视化语言创建之初的目的就是,希望在软件开发工程师和艺术家、数据建模者之间建立起桥梁,让非技术专家也可以利用计算机和程序语言来建立模型和动画。(提高程序员的审美水平是否也是创建Processing语言的目的之一呢? – -b)

使用Processing来描述可视化数据或者动画,是非常方便和易于理解的,只要定义两个函数即可。

void setup() { … } 负责初始化绘图面板

void draw() { … } 定义绘图逻辑,每一帧画面的重绘都会调用这个函数

Processing API提供了方便的绘图支持:图形类的包括:rect()、ellipse()等基本图形,bezier()等曲线,box()、sphere()等3D图形。交互类的包括,对鼠标、键盘的事件处理。坐标定义的包括,translate()坐标转换,rotate()旋转,光源、视角的转换、位移。颜色处理、图片渲染、字体渲染。以及常用的数学计算支持、三角函数。

结合代码看一个2D绘图的例子:

int i = 0;
void setup() {
	size(200, 200);
	frameRate(24);
	background(255);
	strokeWeight(15);
	smooth();
}
void draw() {
	stroke(random(50), random(255), random(255), 100);
	line(i++, 0, random(0, width), height);
	if (i >= width) i = 0;
}
void mousePressed() {
	background(255);
	redraw();
}

结合代码再看一个3D绘图的例子:

float ang = 0, ang2 = 0, ang3 = 0, ang4 = 0;
float px = 0, py = 0, pz = 0;
float flapSpeed = 0.2;
void setup(){
	size(200, 200, P3D);
	frameRate(30);
	noStroke();
}
void draw(){
	background(0);
	camera();
	// Flight
	px = sin(radians(ang3)) * 170;
	py = cos(radians(ang3)) * 300;
	pz = sin(radians(ang4)) * 500;
	translate(width/2+px, height/2+py, -700+pz);
	rotateX(sin(radians(ang2)) * 120);
	rotateY(sin(radians(ang2)) * 50);
	rotateZ(sin(radians(ang2)) * 65);
	// Body
	fill(153);
	box(20, 100, 20);
	// Left wing
	fill(204);
	pushMatrix();
	rotateY(sin(radians(ang)) * -20);
	rect(-75, -50, 75, 100);
	popMatrix();
	// Right wing
	pushMatrix();
	rotateY(sin(radians(ang)) * 20);
	rect(0, -50, 75, 100);
	popMatrix();
	// Wing flap
	ang += flapSpeed;
	if (ang > 3 || ang < -3) flapSpeed *= -1;
	// Increment angles
	ang2 += 0.02;
	ang3 += 4.0;
	ang4 += 1.50;
}

呵呵,功能很强大,API表述能力很强。

文章的最后,放上一个我用ProcessingJS实现的常见排序算法图形化演示,包括了冒泡、选择、插入、希尔、归并、堆排、快排算法的实现。

传送门

Redis介绍


Redis是一个基于key-value的高速缓存系统,类似于memcached,但是支持更复杂的数据结构List、Set、Sorted Set,并且有持久化的功能。

由于近期工作很多地方都用到了它,所以花了不少时间来阅读文章、编码实验,了解一下Redis都能做些什么,能有什么样的性能表现。

首先遇到的第一个问题就是,Redis究竟是什么?

这个问题看似可笑,其实不然,我很赞同Timyang的观点,架构者对Redis的理解不同、定位也不同,决定了Redis在整个系统结构中会扮演什么样的角色。我总结一下,主流有3种理解:

1.key value store.是一个以key-value形式存储的数据库,定位直指MySQL,用来作为唯一的存储系统。

2.memory cache.是一个把数据存储在内存中的高速缓存,用来在应用和数据库间提供缓冲,替代memcachd。

3.data structrue server.把它支持对复杂数据结构的高速操作作为卖点,提供某些特殊业务场景的计算和展现需求。比如排行榜应用,Top 10之类的。

目前更多的人还是把它定位为一个memcached的升级版,提供更多的数据结构操作,仍然是一个cache。

传统的memcached在类似于SNS社区这样的业务场景下,有一些弊端。比如存储好友关系,不得不使用特殊字符分隔的长字符串来保存。在好友关系没有上限的业务需求下,操作性能低下,达不到缓存系统应有的性能水平。而且从数据库中的关系型结构映射到cache中的长字符串形式,很明显也是架构中很蹩脚的一个环节。

而Redis提供的List、Set和Sorted Set就可以很好的业务模型映射到相应的数据结构上,契合度很高。按我的理解,关系数据库理论几乎可以照搬到Redis的应用中来。

Redis官方教程中的仿Twitter案例就是一个非常好的入手点。用Set结构来存储follower和following,用List结构来保存每个人的所有post,再加上一些普通的key-value来存储用户基本信息,很直观和清晰。

我再来举一个好友关系的业务场景来描述一下我的理解,标准关系型数据库结构是怎么和Redis存储结构实现一一映射的。

数据库中有3张表:

1.用户表有两列:id、昵称

2.好友关系表有列:用户id、好友id、好友所属分组id、好友备注、添加好友时间

3.分组表:分组id、分组名称、所属用户id

增加、删除一个好友,就是在好友关系表里insert或delete一条记录。

获取某用户的所有好友分组,及分组内的好友数:

select g.gid, g.gname, count(f.fuid)
  from groups g left join friends f
    on g.gid=f.gid
 where g.uid=#uid#
 group by g.gid, g.gname

获取某用户某分组下的好友列表:

select f.fuid, u.nickname, f.remark, f.time
  from friends f left join users u
    on f.fuid=u.id
 where f.uid=#uid# and f.gid=#gid#
 order by f.time
 limit #start#, #count#

再来看看Redis如何实现类似的业务场景。

用户昵称:uid:xxxxx:nickname,以String结构存储,相当于user表

分组名称:gid:yyyyy:gname,以String结构存储

用户所有分组:uid:xxxxx:groups,以Set结构存储gid的集合

分组下好友:gid:yyyyy:friends,以Set结构存储,保存fuid的集合

好友:uid:xxxxx:fuid:zzzzz:gid、uid:xxxxx:fuid:zzzzz:remark、uid:xxxxx:fuid:zzzzz:time各自使用String结构存储,相当于friends表的每个字段

添加一个好友需要把uid:xxxxx:fuid:zzzzz:gid、uid:xxxxx:fuid:zzzzz:remark、uid:xxxxx:fuid:zzzzz:time这三个字段set好,再sadd gid:yyyyy:friends zzzzz,把好友加到这个组的集合内

获取某用户的所有好友分组,及分组内的好友数,需要用smembers获取uid:xxxxx:groups集合中的gid,再用这些gid来分别scard gid:yyyyy:friends获取该分组下有多少好友。

获取用户123456在分组1001下的好友列表:

sort gid:1001:friends
  by uid:123456:fuid:*:time
limit 0 10
 get #
 get uid:*:nickname
 get uid:123456:fuid:*:remark
 get uid:123456:fuid:*:time

很有意思是不是,很像sql语句,key中的*符号是个占位符,可以被sort出的结果替换,进而get到动态key里面的value。

我们可以总结一下,传统的关系型数据库,处理一对多的问题,需要把外键放在多的一端,因为RDBMS理论中没有集合这个直接概念。而使用Redis,我们可以很直觉的在一的一端来管理一对多的关系,使用Set。

这只是使用上的区别,而理论上,RDBMS的理论完全可以套用在Redis上,所有用关系型数据库理论可以描述的结构,用Redis的数据结构,都可以实现

最关键的是,如果使用MySQL,当数据规模非常大时,上面两个查询操作都需要借助表关联技术,而大表间的join在大型系统中是需要极力避免的操作。相反Redis的每个操作都会局限在一个较小的数据集范围内,而且key-value的存储形式,定位key只是一个复杂度为O(1)的操作。在very huge的数据量下,Redis性能效果非常优异,这就是NoSQL的优势所在!

第 1 页,共 6 页12345...最旧 »