缓存能够说是无处不在,好比:PC电脑中的内存、CPU中有二级缓存、http协议中的缓存控制、CDN加速技术 无不都是使用了缓存的思想来解决性能问题。javascript
缓存是用于解决高并发场景下系统的性能及稳定性问题的银弹。java
本文主要是讨论咱们常用的分布式缓存Redis在开发过程当中须要考虑的问题。git
大部分状况,你们都是把缓存操做和业务逻辑之间的代码交织在一块儿的,好比:github
public UserServiceImpl implements UserService { @Autowired private RedisTemplate<String, User> redisTemplate; @Autowired private UserMapper userMapper; public User getUserById(Long userId) { String cacheKey = "user_" + userId; User user = redisTemplate.opsForValue().get(cacheKey); if(null != user) { return user; } user = userMapper.getUserById(userId); redisTemplate.opsForValue().set(cacheKey, user); // 若是user 为null时,缓存就没有意义了 return user; } public void deleteUserById(Long userId) { userMapper.deleteUserById(userId); String cacheKey = "user_" + userId; redisTemplate.opsForValue().del(cacheKey); } }
从上面的代码能够看出如下几个问题:redis
由于高耦合带来的问题还不少,就不一一列举了。接下来介绍笔者开源的一个缓存管理框架:AutoLoadCache是如何帮助咱们来解决上述问题的。数据库
借鉴于Spring cache的思想使用AOP + Annotation 等技术实现缓存与业务逻辑的解耦。咱们再用AutoLoadCache 来重构上面的代码,进行对比:json
public interface UserMapper { @Cache(expire = 120, key = "'user_' + #args[0]") User getUserById(Long userId); @CacheDelete({ @CacheDeleteKey(value = "'user' + #args[0].id") }) void updateUser(User user); } public UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; public User getUserById(Long userId) { return userMapper.getUserById(userId); } @Transactional(rollbackFor=Throwable.class) public void updateUser(User user) { userMapper.updateUser(user); } }
使用Annotation解决缓存与业务之间的耦合后,咱们最主要的工做就是如何来设计缓存KEY了,缓存KEY设计的粒度越小,缓存的复用性也就越好。缓存
上面例子中咱们是使用Spring EL表达式来生成缓存KEY,有些人估计会担忧Spring EL表达式的性能很差,或者不想用Spring的状况该怎么办?服务器
框架中为了知足这些需求,支持扩展表达式解析器:继承com.jarvis.cache.script. AbstractScriptParser后就能够任你扩展。并发
框架如今除了支持Spring EL表达式外,还支持Ognl,javascript表达式。对于性能要求很是高的人,可使用Ognl,它的性能很是接近原生代码。
在实际状况中,可能有多个模块共用一个Redis服务器或是一个Redis集群的状况,那么有可能形成缓存key冲突了。
为了解决这个问题AutoLoadCache,增长了namespace。若是设置了namespace就会在每一个缓存Key最前面增长namespace:
public final class CacheKeyTO implements Serializable { private final String namespace; private final String key;// 缓存Key private final String hfield;// 设置哈希表中的字段,若是设置此项,则用哈希表进行存储 public String getCacheKey() { // 生成缓存Key方法 if(null != this.namespace && this.namespace.length() > 0) { return new StringBuilder(this.namespace).append(":").append(this.key).toString(); } return this.key; } }
咱们但愿缓存数据包越小越好,能减小内存占用,以及减轻带宽压力;同时也要考虑序列化与反序列化的性能。
AutoLoadCache为了知足不一样用户的须要,已经实现了基于JDK、Hessian、JacksonJson、Fastjson、JacksonMsgpack等技术序列化及反序列工具。也能够经过实现com.jarvis.cache.serializer.ISerializer 接口自行扩展。
JDK自带的序列化与反序列化工具产生的数据包很是大,并且性能也很是差,不建议你们使用;JacksonJson 和 Fastjson 是基于JSON的,全部用到缓存的函数的参数及返回值都必须是具体类型的,不能是不肯定类型的(不能是Object, List<?>等),另外有些数据转成Json是其一些属性是会被忽略,存在这种状况时,也不能使用Json; 而Hessian 则是很是不错的选择,技术很是成熟,稳定性很是好。阿里的dubbo和HSF两个RPC框架都是使用了Hessian进行序列化和返序列化。
当缓存未命中时,都须要回到数据源去取数据,若是这时有100个并发来请求同一个数据,这100个请求同时去数据源取数据,并写缓存,形成资源极大的浪费,也可能形成数据源负载太高而没法服务。
AutoLoadCache有两种机制能够解决这个问题:
拿来主义机制
拿来主交机制,指的是当有多个用户请求同一个数据时,会选举出一个用户去数据源加载数据,其它用户则等待其拿到的数据。
自动加载机制
自动加载机制,将用户请求及缓存时间等信息放到一个队列中,后台使用线程池按期扫这个队列,发现缓存缓存快要过时,则去数据源加载最新的数据放到缓存中。这样能够把用户的不可预期的并发请求,转成可固定的请求数量。
自动加载机制设计之初是为了解决如下问题:
往缓存里写数据性能相对来讲要比读请求慢一些,因此经过上面两种机制,也能减小写缓存的并发,提高缓存服务的性能和吞吐量。
当缓存过时后,请求穿透到数据源中,可能会形成系统不稳定。
AutoLoadCache 会在缓存快过时以前发起一个异步请求,去数据源加载数据,来减小这方面的风险。
在不少时候,数据查询条件是比较复杂,咱们没法获取或还原要删除的缓存key。
AutoLoadCache 为了解决这个问题,使用Redis的hash表来管理这部分的缓存。把须要批量删除的缓存放在同一个hash表中,若是须要须要批量删除这些缓存时,直接把这个hash表删除便可。这时只要设计合理粒度的缓存key便可。
经过@Cache的hfield设置hash表的key。
咱们举个商品评论的场景:
public interface ProuductCommentMapper { @Cache(expire=600, key="'prouduct_comment_list_'+#args[0]", hfield = "#args[1]+'_'+#args[2]") // 例如:prouductId=1, pageNo=2, pageSize=3 时至关于Redis命令:HSET prouduct_comment_list_1 2_3 List<Long> public List<Long> getCommentListByProuductId(Long prouductId, int pageNo, int pageSize); @CacheDelete({@CacheDeleteKey(value="'prouduct_comment_list_'+#args[0].prouductId")}) // 例如:#args[0].prouductId = 1时,至关于Redis命令: DEL prouduct_comment_list_1 public void addComment(ProuductComment comment) ; }
若是添加评论时,咱们只须要主动删除前3页的评论:
public interface ProuductCommentMapper { @Cache(expire=600, key="'prouduct_comment_list_'+#args[0]+'_'+#args[1]", hfield = "#args[2]") public List<Long> getCommentListByProuductId(Long prouductId, int pageNo, int pageSize); @CacheDelete({ @CacheDeleteKey(value="'prouduct_comment_list_'+#args[0].prouductId+'_1'"), @CacheDeleteKey(value="'prouduct_comment_list_'+#args[0].prouductId+'_2'"), @CacheDeleteKey(value="'prouduct_comment_list_'+#args[0].prouductId+'_3'") }) public void addComment(ProuductComment comment) ; }
先来看下面的代码:
public interface UserMapper { @Cache(expire = 120, key = "'user_' + #args[0]") User getUserById(Long userId); @CacheDelete({ @CacheDeleteKey(value = "'user' + #args[0].id") }) void updateUser(User user); } public UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; public User getUserById(Long userId) { return userMapper.getUserById(userId); } @Transactional(rollbackFor=Throwable.class) public void updateUser(User user) { userMapper.updateUser(user); } }
使用updateUser方法更新用户信息时, 同时会主动删除缓存中的数据。 若是在事务还没提交以前又有一个请求去加载用户数据,这时就会把数据库中旧数据缓存起来,在下次主动删除缓存或缓存过时以前的这一段时间内,缓存中的数据与数据库中的数据是不一致的。AutoloadCache框架为了解决这个问题,引入了一个新的注解:@CacheDeleteTransactional:
public UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; public User getUserById(Long userId) { return userMapper.getUserById(userId); } @Transactional(rollbackFor=Throwable.class) @CacheDeleteTransactional public void updateUser(User user) { userMapper.updateUser(user); } }
使用@CacheDeleteTransactional注解后,AutoloadCache 会先使用ThreadLocal缓存要删除缓存KEY,等事务提交后再去执行缓存删除操做。其实不能说是“解决不一致问题”,而是缓解而已。
缓存数据双写不一致的问题是很难解决的,即便咱们只用数据库(单写的状况)也会存在数据不一致的状况(当从数据库中取数据时,同时又被更新了),咱们只能是减小不一致状况的发生。对于一些比较重要的数据,咱们不能直接使用缓存中的数据进行计算并回写的数据库中,好比扣库存,须要对数据增长版本信息,并经过乐观锁等技术来避免数据不一致问题。
大部分状况下,咱们都是对缓存进行读与写操做,可有时,咱们只须要从缓存中读取数据,或者只写数据,那么能够经过 @Cache 的 opType 指定缓存操做类型。现支持如下几种操做类型:
另外在@Cache中只能静态指写缓存操做类型,若是想在运行时调整操做类型,须要经过CacheHelper.setCacheOpType()方法来进行调整。
最后欢迎你们到github对AutoLoadCache开源项目Star和Fork进行支持。