`
ch_space
  • 浏览: 111396 次
  • 性别: Icon_minigender_1
  • 来自: 农村进城务工人员
社区版块
存档分类
最新评论

分页查询总行数缓存策略

    博客分类:
  • Java
阅读更多
文章有点长。。。

以前看到的分页模型大同小异,都是一个POJO结合各类视图技术实现的,但对于每次查询,都要计算总页数(统计记录总行数),对于记录数较少、并发不高的系统来讲,这似乎没有什么问题,但对于高并发,记录行数很多(千万级)的情况,每次的统计行数就要花费不少时间。我这里尝试着设计了一个行数缓存和一个简单的分页POJO(跟传统的POJO大同小异),请大家批评讨论,并提出一些建议。分享才能进步!

1、在哪里缓存
可以在客户端(使用Cookie),也可以在服务器端(设计一个Cache)。服务器端可以灵活定义缓存时间、刷新策略,这里仅讨论服务器端缓存(大家也可以提提客户端缓存的优缺点)。

2、使用什么作为缓存的Key
缓存当然要有Key-Value,什么作为Key合适呢?
对于每次查询,查询条件是不一样的,尤其对于复杂的多条件动态查询,即同一个Service方法可能会有不同的查询条件,这样每次的记录行数是不确定的。所以,可以唯一标示一个查询的就是请求调用的方法(Controler里分发)和对应的查询参数,如:/listUsers.do?name=xxx&curPage=1 。那么就需要在服务器端获取这个url,然后把&curPage=1 这个条件去掉。需要注意的是:对于form的提交,需要以get方式,参数才可以用request.getQueryString()获取。

确定使用url作为缓存的key后,就要设计Page模型和缓存模型了。一般情况下,在Controler里调用Service,需要传入一个Page对象,在Dao中需要根据url进行缓存查询,决定是否要统计行数,将url直接作为Dao层中方法的参数是很不优雅的。这里我将url设计成Page的一个属性,在Dao中可以方便的使用page.getUrl()获取url了。

3、缓存策略
我们以<url,RowCount>的key-value形式在HashMap中缓存一个行数记录。关于缓存策略,可以有很多种,这里分析一下。

1)每个url具有独立的缓存策略
就是说,每个url可以有不同的缓存时间、刷新策略。缓存时间可以根据这个url对应的预计并发情况、统计耗时确定。
刷新可以访问后无论缓存中是否有对应的行数记录立刻重置计时,也可以只在缓存过期后才刷新,而缓存有效时不进行刷新。

2)统一定义每个url的缓存策略
这种情况下只需要每隔一段时间重置所有缓存中的记录的计时器即可,是最简单的一种。然而间隔时间多少不好估计。

3)是否需要换出内存
一个url按100个字符计算,加上RowCount(含2个int,一个long)本身的内存占用大概30byte,一个记录大概230byte,如果一个系统有10000个需要分页的查询(若查询参数不同,数目远不止这个),缓存占用约2Mb。还是占用了不少的内存,因此需要设计内存换出策略。可以采用最近最少使用原则LRU(Least Recently Used)、最不常用原则LFU(Least Frequently Used)等。前者简单,后者貌似更公平。我们简单采用LRU的一个简化版本:当缓存条目达到限制时,将最近最久未访问的缓存记录换出(LRU是计数,这里是计时)。

4、示例代码
Page分页模型:
/**
 * A pagination tool,default pageSize:20
 * @author chen
 */
public class Page {
	private int totalRow;
	private int totalPage;
	private int curPage;
	private int pageSize;
	private String url;
	private static final int DEFAULT_PAGESIZE=20;
	/**
	 * Default page size is 20
	 * @param url A string in the address field of the browser
	 * @param curPage current page index
	 */
	public Page(String url,int curPage) {
		this.url=url.replaceAll("&?curPage=\\d*", "");
		this.curPage = curPage < 1 ? 1 : curPage;
		this.pageSize = DEFAULT_PAGESIZE;
	}

	/**
	 * @param url A string in the address field of the browser
	 * @param curPage Current page index
	 * @param pageSize Size of the page
	 */
	public Page(String url,int curPage, int pageSize) {
		this.url=url.replaceAll("&?curPage=\\d*", "");
		this.curPage = curPage < 1 ? 1 : curPage;
		this.pageSize = pageSize;
	}
	/**
	 * Set total row of the pager
	 */
	public void setTotalRow(int totalRow) {
		this.totalRow =totalRow;
		this.totalPage=this.totalRow < 1 ? 0 : (this.totalRow - 1) / pageSize + 1;
		//invalid state
		if(curPage>this.totalPage || curPage<1){
			this.totalPage=0;
			this.totalRow=0;
			this.curPage=1;
		}
	}
	......
}

缓存模型:
public class RowCountCache {
	private RowCountCache() {
	}
	private Map<String, RowCount> m = new HashMap<String, RowCount>();
	private static RowCountCache cache = new RowCountCache();
	private static final int MAXSIZE=10000;
	private static Calendar c=Calendar.getInstance();
	/**
	 * An object of this cache.
	 * @return this
	 */
	public static RowCountCache getInstance() {
		return cache;
	}
	/**
	 * Cache state of the object: In the cache and is valid.
	 */
	public static final int CACHESTATE_VALID=1;
	/**
	 * Cache state of the object: Cache is expired
	 */
	public static final int CACHESTATE_EXPIRED=-1;
	/**
	 * Cache state of the object: Not in the cache.
	 */
	public static final int CACHESTATE_UNCACHED=-2;
	
	/**
	 * Get the row-count number from the cache of the given url.
	 * @return A row-count number,-1 if was not cached.
	 */
	public int get(String url) {
		RowCount r = m.get(url);
		return r==null ? -1 : r.getTotalRow();
	}
	
	/**
	 * Put or refresh cached row-count corresponding given url with default cache time.
	 * @param totalRow A row-count number corresponding a specify url.
	 */
	public void putOrRefresh(String url,int totalRow) {
		int cacheState=RowCountCache.getInstance().getCacheState(url);
		if(cacheState==RowCountCache.CACHESTATE_UNCACHED){
			this.put(url, totalRow);
		}else{
			this.refresh(url, totalRow);
		}
	}
	/**
	 * Put or refresh cached row-count corresponding given url with custom cache time.
	 * @param totalRow A row-count number corresponding a specify url.
	 * @param cacheTime Time the totalRow will be cached, in seconds.
	 */
	public void putOrRefresh(String url,int totalRow,int cacheTime) {
		int cacheState=RowCountCache.getInstance().getCacheState(url);
		if(cacheState==RowCountCache.CACHESTATE_UNCACHED){
			this.put(url, totalRow,cacheTime);
		}
		if(cacheState==RowCountCache.CACHESTATE_EXPIRED){
			this.refresh(url, totalRow);
		}
	}
	private void put(String url,int totalRow) {
		if(m.size()>=MAXSIZE){
			Set<Map.Entry<String, RowCount>> set = m.entrySet();
			c.setTime(new Date());
			long max_interval=-1;
			String key="";
			//find the farthest unused RowCount record
			for(Iterator<Map.Entry<String, RowCount>> iter=set.iterator();iter.hasNext();){
				Map.Entry<String, RowCount> e = iter.next();
				RowCount r=e.getValue();
				long interval=c.getTimeInMillis()-r.getLastVisit();
				if(max_interval<interval){
					max_interval=interval;
					key=e.getKey();
				}
			}
			m.remove(key);
		}
		m.put(url, new RowCount(totalRow));
	}
	private void put(String url,int totalRow,int cacheTime) {
		m.put(url, new RowCount(totalRow,cacheTime));
	}
	private void refresh(String url,int totalRow) {
		RowCount r =m.get(url);
		r.refresh(totalRow);
	}
	/**
	 * Get the cache state of RowCount corresponding the given url
	 * @return cache state
	 */
	public int getCacheState(String url) {
		RowCount r=m.get(url);
		if(r==null){
			return RowCountCache.CACHESTATE_UNCACHED;
		}else if(r.isExpired()){
			return RowCountCache.CACHESTATE_EXPIRED;
		}else{
			return RowCountCache.CACHESTATE_VALID;
		}
	}
}
/**
 * An object in the cache corresponding a specify url.
 * @author chen
 */
class RowCount{
	//Default cached time,5sec.
	private static final int DEFAULT_CACHE_TIME=5;
	private static Calendar c=Calendar.getInstance();
	private int totalRow;
	private int cacheTime;
	private long lastVisit;
	/**
	 * Construct RowCount with default cached time,5sec.
	 * @param totalRow A row count number corresponding a specify url.
	 */
	public RowCount(int totalRow){
		this.totalRow=totalRow;
		this.cacheTime=DEFAULT_CACHE_TIME;
		c.setTime(new Date());
		this.lastVisit=c.getTimeInMillis();
	}
	/**
	 * Construct RowCount with custom cached time,5sec.
	 * @param totalRow A row count number corresponding a specify url.
	 * @param cacheTime Time of the object will be cached,in seconds.
	 */
	public RowCount(int totalRow,int cacheTime){
		this.totalRow=totalRow;
		this.cacheTime=cacheTime;
		c.setTime(new Date());
		this.lastVisit=c.getTimeInMillis();;
	}
	/**
	 * Get the value of the row count.
	 * @return the value of the row count.Return -1 if the cache is expired.
	 */
	public int getTotalRow(){
		if(!isExpired()){
			return this.totalRow;
		}
		return -1;
	}
	/**
	 * Refresh this RowCount object in the cache.
	 * @param row A new row count number.
	 */
	protected void refresh(int row){
		this.totalRow=row;
		c.setTime(new Date());
		this.lastVisit=c.getTimeInMillis();;
	}
	/**
	 * Check whether the cache is expired
	 * @return true,expired; false,unexpired
	 */
	public boolean isExpired(){
		c.setTime(new Date());
		long t=c.getTimeInMillis();
		return t > this.lastVisit + this.cacheTime*1000;
	}
	/**
	 * Get the last visit date of the object in milliseconds.
	 */
	protected long getLastVisit(){
		return this.lastVisit;
	}
}


下面是一个使用的Demo(部分代码)
一个请求发送到Controler(为简化,省略了Service层)
...
Page p=new Page(HttpUtil.getUrl(),HttpUtil.getInteger(request, "curPage"));
request.setAttribute("all", userDao.find(cond,p));
request.setAttribute("page", p);
return mapping.findForward("find.do");


Dao:
...
int cacheState=RowCountCache.getInstance().getCacheState(p.getUrl());
if(cacheState==RowCountCache.CACHESTATE_VALID){
     p.setTotalRow(RowCountCache.getInstance().get(p.getUrl()));
}else{
     p.setTotalRow(this.countRow(cond, p));
}
//Whatever the cache is valid,refresh it.You can aslo refresh only when the cache is expired
RowCountCache.getInstance().putOrRefresh(p.getUrl(), p.getTotalRow());
List<Users> all=ct.setFirstResult(p.getFirstRow()).setMaxResults(p.getPageSize()).list();
return all;


终于写完了,请大家多多提意见,共同进步。~~

分享到:
评论
2 楼 WenBin_Zhou 2009-11-02  
LZ的写的很好,但我想请教几个问题:
    1.在内存中用map缓存用户的请求参数,如果是同一个用户在一段时间内发同一请求,但不同的curPage参数,LZ的做法是多进行了缓存,却没有想覆盖,这样是否是浪费而没意义的呢
    2.List<Users> all=ct.setFirstResult(p.getFirstRow()).setMaxResults(p.getPageSize()).list(); Lz使用hibernate吧,那hibernate的一二级缓存为何不考虑使用呢?
    3.分页吧,是Web应用吧?那服务器的session来缓存应该是最妥当的,缓存用户会话期间的请求参数,会话结束,即可以自动在设置的时间内销毁。
    4.在页面用数据岛缓存,但这需要js控制,也是不错的选择
最后 Lz代码写得很棒,学习了!
1 楼 qiren83 2009-11-02  
多谢楼主 很详细的代码和注释

看了一半 有点头大 先顶下

请问下楼主做JAVA开发几年了 哪年的 呵呵

最近写代码感觉没前途似的 以前出来的几个朋友都做其它的去了 一半以上开外贸公司 多的月10万 去年开始的

相关推荐

    千万级数据分页查询存储过程SQLServer

    4. **缓存策略**:对于经常访问的页面,可以考虑使用缓存技术,如Redis,减少数据库压力。 总的来说,利用SQL Server的存储过程进行千万级数据分页查询,结合合理的索引策略和优化技巧,能够有效提升查询性能,降低...

    oracle分页程序的实现

    在这个例子中,我们首先计算出总行数和起始行号、结束行号,然后通过比较rowid(Oracle的逻辑地址)来选取指定范围内的数据。 在实际开发中,为了提高性能,通常还会对查询进行优化,如合理设计索引、减少不必要的...

    前端 elementUI 穿梭框大量数据分页处理

    4. **数据缓存**:对于已经加载过的数据,我们可以将其存储在缓存中,以避免重复请求。在用户翻页时,优先从缓存中获取数据,只有在缓存为空时才向服务器发起请求。 5. **优化DOM操作**:减少不必要的DOM操作,例如...

    各种分页方法

    10. **最佳实践**:对于大型数据集,考虑使用缓存策略,如Redis,将部分数据预加载到内存,或者使用前端分页,只在用户滚动时加载更多数据,这种被称为“无限滚动”。 以上就是关于MySQL数据库中分页方法的一些核心...

    asp高效分页 效率很高

    ' 这里是获取总行数的逻辑 End Function Public Function GetPagingSQL() ' 这里是生成分页SQL的逻辑 End Function Public Function GetPageLinks() ' 这里是生成分页链接的逻辑 End Function End Class `...

    ListView高效分页

    在实现分页时,需要设置数据源的分页属性,例如`PageIndex`(当前页索引)、`PageSize`(每页条目数)和`TotalRowCount`(总行数)。 3. **事件处理**:ListView提供了`PageIndexChanging`事件,当用户切换页面时...

    asp.net分页

    - 预估总行数:避免每次分页都计算总行数,可以使用缓存或预估值。 六、总结 在ASP.NET中,分页是一个重要的功能,通过控件分页和自定义分页可以满足不同场景的需求。理解并熟练掌握这些方法,可以有效地提升Web...

    目前用到的两个分页存储过程代码

    首先,我们来看第一个存储过程`[sq8reyoung].[fenye_num]`,它的主要目的是计算满足特定条件的数据总行数。这个存储过程接受两个参数:`@TableNames`(要查询的表名)和`@Filter`(查询条件)。如果`@Filter`为空,...

    hibernate分页例子

    在IT行业中,数据库操作是必不可少的一部分,特别是在Java领域,Hibernate作为一种强大的对象关系映射(ORM)框架,极大地简化了...同时,为了提升性能,还可以考虑缓存策略、索引优化等手段,以提高分页查询的效率。

    PHP用于文件内容操作的分页类.zip

    1. **总长度计算**:为了实现分页,需要知道文件的总长度或总行数。这个类可能提供了读取文件并计算总行数的方法,这一步是确定可以有多少页的基础。 2. **参数管理**:在网页请求中,分页通常通过URL参数如`page`...

    AspNetPager72Samples

    - 使用缓存策略,减少数据库访问次数。 - 考虑用户界面的友好性,提供清晰的分页指示,如“共XX页,当前第XX页”。 - 考虑到搜索引擎优化(SEO),提供链接形式的分页,便于爬虫抓取。 通过深入学习和实践`...

    PHP分页函数代码(简单实用型)

    分页功能的优化还包括缓存策略、负载均衡和响应式设计等。例如,可以使用缓存技术(如Memcached或Redis)来存储分页结果,减少数据库查询次数。对于高并发场景,可以考虑使用负载均衡器来分散请求。对于移动设备和...

    JS代码实现table数据分页效果

    // 获取数据总行数 var sumPages = Math.ceil(sumRows / pageSize); // 得到总页数 var $pager = $('&lt;div class="page"&gt;&lt;/div&gt;'); for (var pageIndex = 1; pageIndex ; pageIndex++) { $('&lt;a href="#"&gt;&lt;span&gt;...

    C#原生报表操作--设置每页打印30行实例源码

    2. **分页逻辑**:在C#中实现分页,需要计算数据源的总行数并根据每页的行数(这里是30行)进行分割。可以使用数据集合(如DataTable或List)的分页方法,或者手动遍历数据并按每30行划分到不同的页中。分页后的数据...

    mysql获取group by的总记录行数另类方法

    需要注意的是,`SQL_CALC_FOUND_ROWS`和`FOUND_ROWS()`不适用于所有的SQL优化策略,例如,它们可能不会在查询缓存中工作。因此,在性能至关重要的情况下,可能需要寻找其他解决方案,如预先计算并存储在数据库中的...

    kdtable的虚模式加载数据

    `KDTable1.setRowCount(list.size())`设置了表格的总行数,这一步可选,如果没有提供,kdtable会在第一次请求数据时自动计算。 接下来,添加了一个`KDTDataRequestListener`,这个监听器会在用户滚动表格(或触发...

    SqlServer 2005 T-SQL Query 学习笔记(3)

    例如,如果我们设置NTILE(3),则结果集会被分为3个组,如果总行数不能均分为3组,多余的行将分配到前两个组。NTILE()在数据分区和分组分析中非常有用,可以方便地将数据划分为低、中、高三档,或者根据需求进行...

    PHP 翻页 实例代码

    让我们深入了解一下这个代码的工作原理。 首先,我们定义了一个名为`Page`的类,它包含了以下几个私有属性: 1. `$pageSize`:表示每一页显示的数据...同时,对于大型网站,可能需要考虑缓存策略以减少数据库查询。

Global site tag (gtag.js) - Google Analytics