给jfinal提几个小建议

今天在公司里有个大佬给我一个缓存的方向,就是用mybatis的Cache接口类似于这种

package com.hua.redis;

import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import org.apache.ibatis.cache.Cache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.util.StringUtils;

public class RedisCache implements Cache {

	private static final Logger LOG = LoggerFactory.getLogger(RedisCache.class);

	private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
	private final String id; // cache instance id
	private RedisTemplate<?, ?> redisTemplate;

	public RedisCache(String id) {
		if (id == null) {
			throw new IllegalArgumentException("Cache instances require an ID");
		}
		this.id = id;
	}

	/**
	 * @return string
	 */
	@Override
	public String getId() {
		return id;
	}

	/**
	 * @return int
	 */
	@Override
	public int getSize() {
		RedisTemplate<?, ?> redisTemplate = getRedisTemplate();
		redisTemplate.setKeySerializer(redisTemplate.getStringSerializer());
		HashOperations hashOperations = redisTemplate.opsForHash();
		Map map = hashOperations.entries(id);
		LOG.error("RedisCache getSize>>>id:>>" + id);
		if(map != null && !map.isEmpty()) {
			return map.size();
		} else {
			return 0;
		}
	}

	/**
	 * Put query result to redis
	 *
	 * @param key
	 * @param value
	 */
	@Override
	public void putObject(Object key, Object value) {
		RedisTemplate<?, ?> redisTemplate = getRedisTemplate();
		redisTemplate.setKeySerializer(redisTemplate.getStringSerializer());
		HashOperations hashOperations = redisTemplate.opsForHash();
//		ValueOperations opsForValue = redisTemplate.opsForValue();
		if (key == null) {
			LOG.error("RedisCache putObject>>>id:>>" + id+">key>>"+key);
		}
		try {
			//增加空判断,如果查询结果为空,不插入缓存
			if(value == null || (value instanceof String && StringUtils.isEmpty((String) value))) {
				LOG.error("RedisCache putObject>>>id:>>" + id+">key>>"+key);
				return;
			}
			LOG.info("RedisCache putObject>>>id:>>" + id+">key>>"+key);
			hashOperations.put(id, key, value);
		} catch (Exception e) {
			LOG.error(e.getMessage());
		}
	}

	/**
	 * Get cached query result from redis
	 *
	 * @param key
	 * @return
	 */
	@Override
	public Object getObject(Object key) {
		RedisTemplate<?, ?> redisTemplate = getRedisTemplate();
		redisTemplate.setKeySerializer(redisTemplate.getStringSerializer());
//		ValueOperations<?, ?> opsForValue = redisTemplate.opsForValue();
		HashOperations hashOperations = redisTemplate.opsForHash();
		LOG.info("Get cached query result from redis id>>"+id+">key>>"+key);
//		return opsForValue.get(key);
		return hashOperations.get(id, key);
	}

	/**
	 * Remove cached query result from redis
	 *
	 * @param key
	 * @return
	 */
	@Override
	public Object removeObject(Object key) {
		RedisTemplate redisTemplate = getRedisTemplate();
		redisTemplate.setKeySerializer(redisTemplate.getStringSerializer());
		HashOperations hashOperations = redisTemplate.opsForHash();
		if (key == null) {
			LOG.error("RedisCache removeObject>>>id:>>" + id+">key>>"+key);
		}
		hashOperations.delete(id, key);
		LOG.info("Remove cached query result from redis>>"+id+">>"+key);
		return null;
	}

	/**
	 * Clears this cache instance
	 */
	@Override
	public void clear() {
		RedisTemplate<?, ?> redisTemplate = getRedisTemplate();
		redisTemplate.setKeySerializer(redisTemplate.getStringSerializer());
		final RedisSerializer<String> redisSerializer = redisTemplate.getStringSerializer(); 
		redisTemplate.execute(new RedisCallback<String>() {
			public String doInRedis(RedisConnection conn) {
				byte[] keyByte = redisSerializer.serialize(id);
				conn.del(keyByte);
				LOG.info("clear cached query result from redis"+id);
				return "successful";
			}
		});
	}

	/**
	 * @return ReadWriteLock
	 */
	@Override
	public ReadWriteLock getReadWriteLock() {
		return readWriteLock;
	}

	/**
	 * @return RedisTemplate<String, String>
	 */
	private RedisTemplate<?, ?> getRedisTemplate() {
		if (redisTemplate == null) {
			redisTemplate = ApplicationContextHolder.getBean("redisTemplate");
		}
		return redisTemplate;
	}
}

大致的意思就是在mybatis中

<select>标签会去缓存sql的结构,以namespace+sql+param的为key有就拿缓存没有就读db放缓存

<update><delete>这种的清除缓存

不用人为的去干预缓存的管理,避免造成一方去cache了但是有业务update忘记去清除缓存而有脏数据的尴尬

我用这个思路去找了下jfinal中有无类似的东西,发现activerecode中有一个ICache,但是这个接口稍微有点远离实际环境,原因有几点

  1. 在Model类中只去cache.put,在udpate和delete时候remove和removeAll并没有去调用(可能我看漏了,但是在Model中确实只有put没有清除缓存的相关逻辑)

  2. 在做findByCache的时候需要去传一个sql,这个我感觉完全可以做成拿activerecord帮我去生成的sql和我findByCache带的参数和我的Model class name来拼接一个Key,如果人为的去传sql虽然说更加自由,但是我感觉大多情况如果用activerecode的话就是不想写sql,传了sql等于去管理cache key, 一下子复杂度就上去了,我还要去管理我的key,我不知道当初这么设计是为了让开发更自由还是什么原因


我不知道以后jfianl能否提供一个和mybatis类似的,在我crud的时候一个统一的Cache接口,我只需要去关注我key value的存储到哪里去了,而不需要关心我什么时候去put我什么时候去remove

评论区

lyh061619

2018-04-08 16:53

JFinal这么设计的原因就,把更多的控制权交出来,让开发者或架构师去把控,如果封装的话存在三个问题,1、灵活性有损不能够很的控制;2、因为有时些框架自身的实现无法满足,但需求拓展的话,又显示多余;3、另外JFinal本身自己的依赖包是很少的,如果中间自动加一层缓存的话,(1)、必定会强制依赖ehcache等包,(2)、其次就是自动缓存据了解,在表级联查询是得不到很的控制,所以一般框架层缓存只能做到单表查询缓存,意义不大,(3)、且本身框架的代码复杂度有所增加,通常情况下JFinal ORM是 jdbc的极简封装,理论上是达到原生的速度,所以缓存在JFinal设计是是一种辅助的,不是非必须的,这就是JFinal的设计初衷,因为JFinal把握好核心,其他的都可以自行定制与拓展。

JFinal

2018-04-08 17:03

精细化的缓存 + 自动化缓存更新不是这么容易就做好的,你给的方案是针对于特定项目的,如果要做个 ORM 框架内部的 cache,容易出很多问题

做的话也不是没有可能,复杂度、性能都要做取舍,此外,缓存是做到业务层还是 ORM 层还有很多问题要考虑

暂时先不做这个

yjjdick1990

2018-04-08 17:16

@JFinal 如果把缓存做到DAO层的话有什么隐患吗,因为有些架构的多级缓存即使在ORM这层做的,比如hibernate 和 mybatis

yjjdick1990

2018-04-08 17:23

@JFinal 因为在service层做缓存有个难点在于,业务层中有很多DAO,如果管理不好就可能导致某些dao的缓存没有update或者清理掉导致有脏数据,缓存最大的难点也就在管理什么时候Put什么时候Delete的上面,只要解决这个问题缓存就能做的很好,像Mybatais那种我不敢说在性能方面最好但是在管理缓存方面我感觉是不会出现脏数据的,起码我的数据是正确的

杜福忠

2018-04-08 19:52

JFinal

2018-04-09 11:00

@yjjdick1990 我这里只说一个小的场景,可以思考一下怎么处理:
1:findByCache(key, sql, paras) 这个方法可以缓存查询到的 List 列表,假定 List 里头存放的是 Project 这个 Model
2:project.findById(123).setTitle("新title").update(); 更新某个 Project 对象的时候,应该如何去处理前面 findByCache 中缓存的 project,是让缓存失效,还是遍历 List 去更新
3:上面还只是假定一个 List,对于 Project 的缓存可能无处不在,jfinal 作为框架很难知道你用了什么 key 去缓存了什么 model

当然,也不是完全没有办法,可以将你缓存的 key 在底层自动给你添加上 model 的一些额外的 meta 信息,这样做复杂度提升了,而且你自己也无法通过 CacheKit.remove(...) 的方式很直白去移除 cache 了,性能也会因此带来比较大的影响

事务处理也是一个很大的问题,对于一般的类似于 BBS、Blog 类的项目还能还好点,但是 jfinal 作为框架要考虑的场景必然要包括企业级应用

总之,做是可以做的,但带来的问题也不少,目前 @玛雅牛 通过引入一个 BaseMode 来实现了一个比较好的自动化 cache 解决方案,也是有很多取舍的,可以参考一下

JFinal

2018-04-09 11:04

顺便提一下现在 jfinal 中的 findByCache 这类设计时的想法:
1:用户使用 findByCache 可以在底层用 ehcache 缓存
2:而 ehcache.xml 可以针对 cache 配置缓存失效、替换等策略,人为干预少
3:用户可以明确使用 CacheKit.remove(key) 随时移除 cache

对于读多写少的互联网项目,配置 ehcache.xml 来控制下失效,在 remove、update 等业务方法里头调用一下 CacheKit.remove(...) 即可,这种方式用起来最简单明了,对 jfinal 内部没有性能影响

yjjdick1990

2018-05-04 21:46

@杜福忠 这个我看了下好像也没做到细粒度的自动化缓存,他只不过用了j2cache做在了model上自动帮你去做model的cache,但是并不能帮我解决多表关联时能够根据我的sql来自动缓存或者失效sql中关联表的缓存内容

热门反馈

扫码入社