Lostinet

Lostinet
随笔 - 17, 文章 - 0, 评论 - 283, 引用 - 4
数据加载中……

介绍 RuntimeEntity 的缓存模式,和分页方案

传统背景:

程序员要编写使用缓存的代码, 通常的写法是这样的:

string key=....根据情况生成缓存的key
object data=Cache[key];
if(data==null)
{
    data=....这里编写SQL操作.
    Cache[key]=data;
}
return data;

这种写法看上去没什么问题.
但是如果每个获取数据的方法, 都写上这么一段代码,
那么就会非常无聊和耗时间. 代码也不够简洁和直接.
而且程序换很难去维护这个缓存的及时性. 很容易产生混乱的情况.


RuntimeEntity提供了2种缓存, 用于释放程序员的编码工作.

一 , 对象缓存.

对象缓存即针对单个的对象逐个进行缓存.
RuntimeEntity的对象缓存是透明的,内置的.不需要编写任何代码, 就能享受到对象缓存的好处.

例如
Product p1=Product.LoadRow(1);
Product p2=Product.LoadRow(1);

这2行代码都去读取同一个Product.
第一行p1会去执行一条SQL语句. 对象会被缓存起来. 然后第二行p2就不用再重新执行SQL语句了.

但是RuntimeEntity有一个很好的feature,在上面例子中,
虽然p2是来自于缓存, 但是p1与p2引用的并不是同一个对象.

为什么会这样设计呢?
这是防止一段代码调用 p1.ProductName="Xxx" 修改对象中,提交修改之前,
另外一段代码取得 p2 , 并且执行 textbox.Text=p2.ProductName 可能发生的错误.

现在p1不等于p2, 无论p1怎样修改它的属性, 只要p1没有Save, 那么p1修改的值就不会影响到p2.

下面的代码,
Product p1=Product.LoadRow(1);
Product p2=Product.LoadRow(1);
p1.ProductName="NewName";
p1.Save();
在执行p1.Save()之后, p2.ProductName也会跟着变成"NewName".

很神奇吧! 目前其他的ORM并无法做到这一点!

RuntimeEntity会自动维护了缓存的对象的最新版本.
使缓存是高效的同时, 确保应用程序保持正确的行为.
如果一个对象更新了, 那么其他同一个ID的对象也会是最新的值.

查询例子代码:
Order[] orders1=RuntimeEntity.All<Order>().Where("ShipVia=1 AND EmployeeID<>1");
Order[] orders2=RuntimeEntity.All<Order>().Where("ShipVia=1");
在上述代码中, 使用了RuntimeEntityQuery来查询符合条件的Order.
orders1中并不包含EmployeeID为1的Order 而order2则包含.
因为某些对象已经在orders1被缓存, 所以查询orders2的时候,Employee<>1的部分将不会被加载!
这个设计最大化地应用了对象缓存,大大地提高了加载数据的性能!

无论是LoadRow,还是通过RuntimeEntityQuery一次过返回多个对象,
都会把对象加进缓存里面. 直到内存紧张时, 没有被引用的对象就会被释放.


二 , 查询缓存.

对象缓存只是节省了加载对象的过程. 但是如果使用RuntimeEntityQuery去查询对象,
即使所有符合条件的对象都已经在缓存中了, RuntimeEntityQuery依然会执行一句SQL.
为什么会这样的呢? 原因是RuntimeEntityQuery是分为2步的:

步骤1. SELECT OrderID FROM Orders WHERE ShipVia=1 AND EmployeeID<>1
这样,就得到了所有符合条件的Order的OrderID
RuntimeEntity在内存中,逐一寻找符合OrderID的对象缓存. 如果没有找到,那么就会执行语句:
步骤2. SELECT * FROM Orders WHERE OrderID IN (11111,11112,11113)

即使所有符合条件的Order都在对象缓存中, 步骤1 是必须执行的.

所以,下面代码,执行了3次SQL:
Order[] orders1=RuntimeEntity.All<Order>().Where("ShipVia=1");
Order[] orders2=RuntimeEntity.All<Order>().Where("ShipVia=1");
order1执行了步骤1和步骤2. 然后所有的对象都被缓存起来.
所以返回order2的时候, 仍然会执行步骤1,而跳过了步骤2..

是的, 只有对象缓存是不足够的. 因为上面说的步骤1,本身就是一个非常耗时间的WHERE语句.
为了更加省去步骤1, RuntimeEntityQuery增加了一个新方法 UseCache() .
(这个方法并不包含在可下载的RuntimeEntityPreview中)

只要把上面的代码改成:
Order[] orders1=RuntimeEntity.All<Order>().UseCache().Where("ShipVia=1");
Order[] orders2=RuntimeEntity.All<Order>().UseCache().Where("ShipVia=1");

就可以享受查询缓存的好处.
这个代码的不同之处,只是在中间增加了一个 UserCache() 方法的调用而已.

就这样, RuntimeEntityQuery就会帮你自动分析生成缓存的Key,
并且自动把查询得到的OrderID缓存起来.
这样. 上面的代码在返回orders2的过程, 是完全不需要执行SQL语句了!

默认的情况下,查询缓存是30秒过期. UseCache方法还可以指定缓存的过期时间:
Order[] orders2=RuntimeEntity.All<Order>().UseCache(TimeSpan.FromSeconds(10)).Where("ShipVia=1");

之所以默认不使用查询缓存. 是因为RuntimeEntity无法单独在内存中判断查询缓存的正确性.
所以默认情况下都会查询数据库, 返回最新的ID列表.
而在某些场合, 查询的结果并不需要太实时. 程序员可以根据具体的情况去显式调用UseCache()

 

这是一个一劳永逸的方案!
只要使用RuntimeEntity,就能自动应用对象缓存. 只要使用UseCache()函数, 就能应用查询缓存.


三 , 下面简单地说一下如何使用RuntimeEntity来做超高性能的分页!!

RuntimeEntityQuery提供一个GetRange方法, 用于返回符合页数的对象:

int pageIndex=int.Parse(Request.QueryString["Page"])-1;
int pageIndex=0;
int pageSize=20;
int recordStart=pageIndex*pageSize;//计算记录的开始位置
int recordCount=pageSize;//计算返回多少记录
int totalCount;//将获取所有的记录数!

//执行一句GetRange,就能返回COUNT(*)的同时,返回符合页数的记录.
//完全不需要手动编写什么分页存储过程!
Product[] products = RuntimeEntity.All<Product>().GetRange(recordStart,recordCount,out totalCount);

//计算页数:
int pageCount=(int)Math.Ceiling((double)totalCount/(double)pageSize);
//RuntimeEntity可以绑定到ASP.NET的控件中去!
DataGrid1.DataSource=products;
DataGrid1.DataBind();
//使用totalCount,pageCount,可以设置分页控件:
...

上面的代码, 描述了GetRange的用法. 程序员还可以加上UseCache()用来缓存搜索的结果.

是的. 这是也是一个一劳永逸的方案! 写一次代码, 就能适应绝大部分分页场合的性能要求!!

假如数据库有1千万的记录那怎么办???
RuntimeEntity并不会去读取所有记录. 而是通过对ID进行分页的方式, 读取某段的ID对应的记录!
但是无论如何, 尝试去读取1千万个ID也不是好事.
所以RuntimeEntityQuery增加了一个 LimitTo 方法去限制返回的记录数:
(其原理就是简单的 SELECT TOP N ..)

RuntimeEntityQuery<BlogTopic> query=RuntimeEntity.All<BlogTopic>().UseCache();
if(pageIndex<100)
 query=query.LimitTo(pageSize*100);
query=query.OrderBySqlFieldDescending("TopicID");
BlogTopic[] topics=query.GetRange(recordStart,recordCount,out totalCount);
int pageCount=...
if(pageIndex<100&&pageCount>100)
    moreLink.Visible=true;

上面的代码 , 模拟了一个超大型的社区网站的Blog首页代码.
该网站有超多的用户, 而文章也疯狂地增加.

这段代码中, 用pageIndex来判断是否需要限制加载的ID的数量.
当翻有人到第100页的时候, 才选择加载全部的ID.

这种方案既能让用户能看到所有的记录 , 也在大部分情况下拥有更高的性能.


.. 总结

简单, 总是程序员最希望的编程生活.
性能, 总是数据库开发中最让人头痛的事情.
RuntimeEntity不断尝试让常见的数据库操作变得更简单,同时,让程序拥有最佳的性能.

无论是高手 , 还是菜鸟 , 都能写出高质量的代码 !

希望体验RuntimeEntity的,请到http://www.cnblogs.com/lostinet/ 里寻找RuntimeEntityPreview .
(但是该版本, 并不包含上述提到的 UseCache() 和 LimitTo() 方法.)

 

posted on 2007-11-30 03:06 Lostinet 阅读(2329) 评论(21)  编辑 收藏

评论

#1楼    回复  引用  查看    

有想法,但对每次查询变成两次先取ID觉得这里有个性能问题。有些地方缓存几乎是没有命中的,这样反而性能下降了。
2007-11-30 07:43 | Ariel Y.      

#2楼    回复  引用  查看    

同意Ariel Y.的说法
2007-11-30 08:22 | 白菜园      

#3楼    回复  引用  查看    

在执行p1.Save()之后, p2.ProductName也会跟着变成"NewName".
很神奇吧! 目前其他的ORM并无法做到这一点!
真是手淫强身,意淫强国:)
2007-11-30 08:28 | henry      

#4楼    回复  引用  查看    

莫非是传说中很linq有一拼的延迟加载...
2007-11-30 08:33 | JesseZhao      

#5楼    回复  引用  查看    

缓存部分的设计有新意,到时候研究一下。这样的缓存会不是造成查询时产生比较高的额外消费呢?
2007-11-30 08:57 | 阿不      

#6楼    回复  引用  查看    

我一年前做的一个类似ORM的东西就带有你所说的缓存和超高性能的分页,呵呵。所以,请不要轻易说“目前其他的ORM并无法做到这一点!”,我的缓存机制,比NH当时还要好,比你文中所说的情况,功能也多多了。至于分页,不知道你这样的做法是否符合ODS的要求?我做的分页,屏蔽数据库差异、解决MSSQL没有top不能order,另外一个功能就是完全符合ODS的要求,这样才能使得数据绑定更方便。

一直在关注,衷心希望RuntimeEntity做得更好^_^
2007-11-30 09:17 | 大石头      

#7楼    回复  引用  查看    

Product p1=Product.LoadRow(1);
Product p2=Product.LoadRow(1);
p1.ProductName="NewName";
p1.Save();
在执行p1.Save()之后, p2.ProductName也会跟着变成"NewName".
---
我觉得这里根本就有逻辑上的问题
1
Product p1=Product.LoadRow(1);
Product p2=Product.LoadRow(1);
上面哪两句执行后,p1,p2里已填充了数据,我先不看下面的句子,哪么
p2.ProductName肯定是等于:"OldName"的
当:p1.ProductName="NewName";
p1.Save();
时,p1修改的是数据库里的数据还有缓存中的数据,而此时我再用:p2.ProductName得来的值竟然等于NewName???
p2应该是相当于脱机数据库一样吧,它此时如果没有再次去:Product.LoadRow(1);哪么它的值如何会等于NewName?
难道p2就是传说中的指针?指向缓存的指针?

2007-11-30 09:39 | 没剑      

#8楼    回复  引用  查看    

你所说的“目前其他的ORM并无法做到这一点!”的功能只是在修改缓存时进行貌似的事务处理,但是你有没有考虑过并发的问题,有没有在事务开始的时候就将指定的缓存进行锁定,锁定的级别是怎么样确定的。
2007-11-30 09:45 | Edward.Net      

#9楼    回复  引用  查看    

收藏党路过,已收藏
2007-11-30 10:03 | BoyLee      

#10楼 [楼主]   回复  引用  查看    

@Ariel Y.
这是一个比较折中的做法了。
即使是全部没有命中缓存,
由于WHERE OrderID IN (....)是CLUSTERED INDEX的查询,
并且第一句已经扫描过磁盘,第二句是非常快的。
如果有缓存的命中,那么带来性能的提升是非常高的。

@henry
不知道是褒义还是贬义。
如果有什么不妥,请指出。

@大石头
我所值的“其他ORM不能做到“,是指属性的同步的部分。
其他ORM做不到,是因为那些ORM针对的是POCO。
而我的RuntimeEntity不是ORM。Entity的属性的读写,是由RuntimeEntity来控制的。
所以RuntimeEntity能在不同的对象上返回最新的属性的值

@没剑
这个设计是经过充分的考虑的。
特别是针对Web程序。很多对象被加载使用后会立刻被抛弃。
一旦对象进入修改的状态,它的值就会复制一份,不会被同步的覆盖。
所以并发上不存在问题。
而这个设计更是考虑到相关对象的LazyLoad所产生的关联的时候。
例如
Product p=RuntimeEntity.LoadRow(1);
textBox1.Text=p.Category.Name;
这个。
p获取Category后,Category就会附加到p上。
如果其他地方更新了Category的值,那么p.Category也需要相应更新才行。


@Edward.Net
我这里没有介绍,并不代表没有处理。
实际上RuntimeEntity在后台处理的东西很多。
RuntimeEntity处理实物的时候,新修改的值是隔离的,完全模拟了SQL那边的行为。
所以如果是
using(RuntimeEntity.BeginTransaction())
{
Product p1=...
p1.ProductName="NewName";
p1.Save();

RuntimeEntity.CommitTransaction();
}
如果不到CommitTransaction这一步,p1修改的值,是不会影响其他地方的。

我现在很喜欢这个模式,
就是因为程序员只需要告诉RuntimeEntity他定义什么,执行了什么,
都是一些很简单的定义和操作方式。写一次代码不需要太多的改变。
因为现在定义的方案,和些代码的格式,都定下来了。
而很具体的操作,全部都是RuntimeEntity后台实现了。
这样即使有一些东西我没想到,我也可以以后加进去,自动去实现某些特征去保护程序的正确性。
2007-11-30 12:48 | Lostinet      

#11楼    回复  引用    

是不是要同步修改其它对象 最好是使用者决定 否则会头很大 其实很多时候并不需要同步, 不透明的修改对使用者来说是很痛苦的事

如果大量查询没有缓存命中,假如说有100条, 那么在in里的id是很可观的,比where 语句带来的开销要大很多

我的意见还是 尽量不要不透明地修改数据
2007-11-30 13:18 | .progame [未注册用户]

#12楼    回复  引用  查看    

Product p1=Product.LoadRow(1);
Product p2=Product.LoadRow(1);
p1和p2在同一个线程会话里本来就是一个对象,当然如果做到域范围又解决同步问题就更理想(但什么情况下的同步最理想又符合需求的估计没有人知道).
对于缓存我一直不倾向在数据持化组件中完成,不是太小就是太大;逻辑层面用户自定义的缓存方式最理想和最可优化的.
当然我不否认你考虑的很多东西,但很多时候这些考虑在应用时很多都体现不出任何优越.
2007-11-30 13:33 | henry      

#13楼    回复  引用  查看    

我觉得不就就会有方案让LINQ的性能会赶上RuntimeEntity的
2007-11-30 13:57 | C# hack      

#14楼    回复  引用  查看    

在执行p1.Save()之后, p2.ProductName也会跟着变成"NewName".
很神奇吧! 目前其他的ORM并无法做到这一点!

这一点ORM都可以做到,这是ORM的Identity Map模式,在同一个上下文中总是优先选择Cache对象
lz可以试试linq to sql
2007-11-30 14:03 | 紫色阴影      

#15楼 [楼主]   回复  引用  查看    

@progame:
同步带来的好处是很实在的.
当然这需要程序员明确地知道这件事.
RuntimeEntityPreview已经有方案去阻止某个对象进行自动的同步.
不过看来依然不够.我会在Transaction的范围上加一个锁,防止事务过程中的冲突.

@henry
p1和p2不是同一个reference.
无论是单线程,还是不同的线程,表现的行为是一致的.
更多的思考的原因,我会在下一篇文章,关于DomainModel的设计中提及.

@紫色阴影
这个和IdentityMap是两回事.
IdentityMap只不过是一种缓存.
而我的那个机制,是建立在,对象已经被加载,已经被其他对象引用了的基础上进行的.
IdentityMap要做到这一点,必须每次访问对象的时候都要Load一次.
而我的方案则只需要Load第一次.

2007-11-30 14:13 | Lostinet      

#16楼    回复  引用  查看    

/*
下面的代码,
Product p1=Product.LoadRow(1);
Product p2=Product.LoadRow(1);
p1.ProductName="NewName";
p1.Save();
在执行p1.Save()之后, p2.ProductName也会跟着变成"NewName".

很神奇吧! 目前其他的ORM并无法做到这一点!
*/
呵呵,一点儿都不神奇。如果能称之为ORM,必须能够做到这一点。
在我的Kanas.net Framework中,应该比你的更先进、更灵活。如果你不需要隔离,可以立即同步;如果需要隔离,也可以选择不同步。和你的同步不同的是:虽然是保持同一份引用,但是如果有一个线程准备修改,则立即生成一个新的“影子对象”,另外建一份实例是很浪费的,我的原则是需要时才重建实例。
2007-11-30 15:17 | 双鱼座      

#17楼    回复  引用  查看    

1. 在执行p1.Save()之后, p2.ProductName也会跟着变成"NewName".
这个未必是好事,也未必是坏事。也就是说应用层里面对象是有隔离概念的,但这个隔离不是彻底的,有东西会越过这个隔离来自动影响当前对象。
另外这样application层应当是不能做分布式了,否则p1.Save()之后要用分布式机制将这个消息通知其它server了

2. 查询缓存.
不建议这样做,这个效率肯定比不用缓存要低,因为查询出一批ID跟查询出这些实际数据没有多少差异(除非一种情况,查询出来的数据特别多,对于SQL Server这样要用bookmark lookup去取数据的)
用查询语句+参数做缓存的方案在目前是有充分的理由的
不过将查询结果缓存与实体缓存结合起来,这个也是我想要的结果。基于单实体的查询还是比较好实现,基于多实体,就不好做了,特别是有聚合(用group by)的情况几乎不大可能
2007-11-30 15:38 | RicCC      

#18楼    回复  引用  查看    

Product p1=Product.LoadRow(1);
Product p2=Product.LoadRow(1);
p1.ProductName="NewName";
p1.Save();
在执行p1.Save()之后, p2.ProductName也会跟着变成"NewName".

----
楼主,这个我怎么也想不通在WEB中可以这样子。。。
如:Product p1=Product.LoadRow(1);
Product p2=Product.LoadRow(1);
p1.ProductName="NewName";
p1.Save();
response.write(p2.ProductName);
楼主说说看这个p2.ProductName的值是什么

2007-11-30 17:28 | 没剑      

#19楼    回复  引用  查看    

lostinet大哥,如果能赶在现在提前与LINQ结合,与正式版本的LINQ同步推出,恐怕市场影响力会更大.
2007-12-01 01:57 | cnlamar      

#20楼    回复  引用  查看    

Product p1=Product.LoadRow(1);
Product p2=Product.LoadRow(1);
p1.ProductName="NewName";
p1.Save();
在执行p1.Save()之后, p2.ProductName也会跟着变成"NewName".


----

这其实会引起很多副作用的。我的对象被你的ORM莫名奇妙的改变了,而这个我完全不知情!!!如果p1 p2 在不同的事物中,你这样做无意破坏了事物的隔离!!
2007-12-01 21:47 | quitgame      

#21楼 [楼主]   回复  引用  查看    

@双鱼座
当需要的时候,才生成Shadow,已经太晚了.
加入有两个对象同时引用一个Entity.
如果某个对象对该Entity进行操作,并且失败了.
而该Entity的状态没有被恢复,
那么另外一个Entity再操作它的时候就会发生不可预料的错误.

@RicCC
关于分布式的实现, 请参考我那篇高级特征的文章.

@没剑
NewName

@cnlamar
RuntimeEntity是支持LINQ的语法查询的.

@quitgame
是自动改变成最新的值.能反应数据库的最新情况
而任何Dirty的值,都不会互相影响.

2007-12-07 18:57 | Lostinet      

标题  
姓名  
主页
Email (博主才能看到) 
验证码 *  看不清,换一张 [登录][注册]
内容(请不要发表任何与政治相关的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      
"五向定位"职业成长路线公开课(上海、南京、大连)
Google站内搜索


相关链接: