JPress的ehcache缓存方案、以及踩过ehcache的坑

JPress之家发布 开发教程 2016-09-14 468

     感谢海哥分享,原文来自:http://www.yangfuhai.com/post/31.html 

     最近为了提高JPress的性能,减少数据查询的次数,JPress大量使用了ehcache缓存作为起内置缓存,同时session也是基于ehcache重新实现的支持分部署的session解决方案。

 

     因为JPress是基于JFinal快速开发框架,而JFinal又内置了ehcache的插件,使用起来及其简单。

 

        1、JFinal里配置ehcachePlugin插件。

public void configPlugin(Plugins me) {
        me.add(new EhCachePlugin());
		
	//添加其他插件
}

        2、在classPath下添加ehcache的配置文件ehcache.xml

 

        3、直接使用EhcacheKit操作缓存。

public void yourMethod() {
       Cachekit.put("cacheName","key","value")

}

 

        到此,一切很顺利的进行着,但随着JPress在大量的使用ehcache,ehcache的缓存数据操作与更新就变成了一个棘手的问题,更新数据库数据了,缓存若得不到及时更新,就会导致程序在运行的过程中有大量的bug、各种莫名其妙的问题。此时、缓存数据的更新,就需要一个良好更新的计划和方案。

 

         首先,是数据颗粒度的问题,我们在缓存数据的时候,可能是根据数据库的ID,对单个model(单条数据)进行缓存,这种缓存以model的ID作为key进行缓存,这种缓存的颗粒度极细。

 

       因此,我们在做数据的更新的时候非常简单,只需在model的更新和删除的时候从ehcache删除该ID即可。针对这一的问题,我们只需要重写Model的update和delete方法,删除其缓存。

 

        代码如下:

@Table(tableName = "content", primaryKey = "id")
public class Content extends BaseContent {

	private static final long serialVersionUID = 1L;

	@Override
	public boolean update() {
		removeCache(getId());//移除ehcache缓存
		return super.update();
	}

	@Override
	public boolean delete() {
		removeCache(getId());//移除ehcache缓存
		return super.delete();
	}

}

       通过这种方式,我们在通过ID来查询该数据的时候,不用担心缓存于数据库不同步的问题,因为我们在更新、删除的时候就已经把ehcache的缓存数据给清除掉了,当查询的时候发现ehcache里没有数据,自动会去数据库会获取,从而保证了ehcache的数据与数据库保持一致。

 

        但是,我们在缓存数据的时候,不只是对单个model进行缓存,在程序的各种业务场景中,大量会使用到列表的查询,因此我们在存储的时候,肯定也会多列表进行缓存。例如:

public List findByModule(final String module, final BigInteger parentId, String orderby) {
		final StringBuilder sqlBuilder = new StringBuilder("select * from content c");
		sqlBuilder.append(" sql ....");

		return DAO.getFromListCache("cacheName","key", new IDataLoader() {
			@Override
			public Object load() {
				return DAO.find(sqlBuilder.toString(), module,parentId);
			}
		});
		
	}

   

        但是,一旦缓存了列表,问题就来了?这个列表的数据什么时候会被更新呢?这个缓存到ehcache的某条数据可能会被随时更新或删除了,怎么来同步?

 

        一种粗糙的方案是:把所有缓存都缓存到同一个cacheName中,然后在model的update或delete的时候,对这个cacheName不管三七二十一直接全部清除,如下代码:

@Table(tableName = "content", primaryKey = "id")
public class Content extends BaseContent {

	private static final long serialVersionUID = 1L;

	@Override
	public boolean update() {
		removeCache(getId());//移除ehcache缓存
        removeAllListCache(); //移除所有保存列表数据的缓存
		return super.update();
	}

	@Override
	public boolean delete() {
		removeCache(getId());//移除ehcache缓存
         removeAllListCache(); //移除所有保存列表数据的缓存
		return super.delete(); 
	}

}

         

        虽然这是一种粗糙的方案,但是也是有效解决了列表数据不同步的问题;其粗糙的原因是,当我们清除数据的时候,把所有的列表都删除了,这样会导致很多没有没有该列表的数据也被清楚了...

 

        所以,更有效的解决方案应该是保留和该ID没有关系的数据,而只清除有关的数据。

 

        那问题来了,什么数据才是该ID有关的数据呢?

        1、列表有该ID的数据。

        2、列表的排序等会受到该ID影响的数据,比如谋条数据的orderby_number更新了,可能某个缓存的列表数据虽然没有该ID,但是该ID更新后,可能是orderby_number,由于缓存的列表数据是根据orderby_number来排序的,此时该数据应该出现在列表里。

        3、分页数据,比如某条数据被删除或更新了,可能分页的页码数据就会被改变。

 

         那如何才能找到该ID关联的数据呢?

        这是一个困难的问题,每个业务系统不一样,关联的数据肯定也不一样。在JPress里,每个content都有一个module字段,表示该数据所属的模型。

 

       因此,在JPress的内容分类里,JPress针对某种类型的数据,都按照一定的规则来建立这个存储的key,比如文章模型的列表在存储的时候,存储的key值大概为:module:article-xxx-xxx这样的key。

       当文章模型的数据被更新的时候,会去便利所有列表数据的key,如果发现key是以module:article开头,表示该数据是文章列表的缓存数据,应该清除。

 

       于是,就有了如下的代码:

public void removeAllListCache() {
		List