文档章节

缓存的那些事

qiujiayu
 qiujiayu
发布于 2017/09/12 10:31
字数 2666
阅读 136
收藏 0

缓存可以说是无处不在,比如:PC电脑中的内存、CPU中有二级缓存、http协议中的缓存控制、CDN加速技术 无不都是使用了缓存的思想来解决性能问题。

缓存是用于解决高并发场景下系统的性能及稳定性问题的银弹。

本文主要是讨论我们经常使用的分布式缓存Redis在开发过程中需要考虑的问题。

1. 如何将业务逻辑与缓存之间进行解耦?

大部分情况,大家都是把缓存操作和业务逻辑之间的代码交织在一起的,比如:

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);
    }
}

从上面的代码可以看出以下几个问题:

  1. 缓存操作非常繁琐,产生非常多的重复代码;
  2. 缓存操作与业务逻辑耦合度非常高,不利于后期的维护;
  3. 当业务数据为null时,无法确定是否已经缓存,会造成缓存无法命中;
  4. 开发阶段,为了排查问题,经常需要来回开关缓存功能,使用上面的代码是无法做到很方便地开关缓存功能;
  5. 当业务越来越复杂时,使用缓存的地方越来越多时,很难定位哪些数据要进行主动删除;
  6. 如果想用别的缓存技术代替Redis,那就要哭了。。。

因为高耦合带来的问题还很多,就不一一列举了。接下来介绍笔者开源的一个缓存管理框架:AutoLoadCache是如何帮助我们来解决上述问题的。

借鉴于Spring cache的思想使用AOP + Annotation 等技术实现缓存与业务逻辑的解耦。我们再用AutoLoadCache 来重构上面的代码,进行对比:

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);
    }
}

2. 如何提升缓存key生成表达式性能?

使用Annotation解决缓存与业务之间的耦合后,我们最主要的工作就是如何来设计缓存KEY了,缓存KEY设计的粒度越小,缓存的复用性也就越好。

上面例子中我们是使用Spring EL表达式来生成缓存KEY,有些人估计会担心Spring EL表达式的性能不好,或者不想用Spring的情况该怎么办?

框架中为了满足这些需求,支持扩展表达式解析器:继承com.jarvis.cache.script. AbstractScriptParser后就可以任你扩展。

框架现在除了支持Spring EL表达式外,还支持Ognl,javascript表达式。对于性能要求非常高的人,可以使用Ognl,它的性能非常接近原生代码。

3. 如何解决缓存Key冲突问题?

在实际情况中,可能有多个模块共用一个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;
    }
}

4. 压缩缓存数据及提升序列化与反序列化性能

我们希望缓存数据包越小越好,能减少内存占用,以及减轻带宽压力;同时也要考虑序列化与反序列化的性能。

AutoLoadCache为了满足不同用户的需要,已经实现了基于JDK、Hessian、JacksonJson、Fastjson、JacksonMsgpack等技术序列化及反序列工具。也可以通过实现com.jarvis.cache.serializer.ISerializer 接口自行扩展。

JDK自带的序列化与反序列化工具产生的数据包非常大,而且性能也非常差,不建议大家使用;JacksonJson 和 Fastjson 是基于JSON的,所有用到缓存的函数的参数及返回值都必须是具体类型的,不能是不确定类型的(不能是Object, List<?>等),另外有些数据转成Json是其一些属性是会被忽略,存在这种情况时,也不能使用Json; 而Hessian 则是非常不错的选择,技术非常成熟,稳定性非常好。阿里的dubbo和HSF两个RPC框架都是使用了Hessian进行序列化和返序列化。

5. 如何减少回源并发数?

当缓存未命中时,都需要回到数据源去取数据,如果这时有100个并发来请求同一个数据,这100个请求同时去数据源取数据,并写缓存,造成资源极大的浪费,也可能造成数据源负载过高而无法服务。

AutoLoadCache有两种机制可以解决这个问题:

  1. 拿来主义机制

    拿来主交机制,指的是当有多个用户请求同一个数据时,会选举出一个用户去数据源加载数据,其它用户则等待其拿到的数据。

  2. 自动加载机制

    自动加载机制,将用户请求及缓存时间等信息放到一个队列中,后台使用线程池定期扫这个队列,发现缓存缓存快要过期,则去数据源加载最新的数据放到缓存中。这样可以把用户的不可预期的并发请求,转成可固定的请求数量。

    自动加载机制设计之初是为了解决以下问题:

    1. 使用非常频繁的数据,长期缓存在内存中;
    2. 解决耗时业务;

往缓存里写数据性能相对来说要比读请求慢一些,所以通过上面两种机制,也能减少写缓存的并发,提升缓存服务的性能和吞吐量。

6. 异步刷新

当缓存过期后,请求穿透到数据源中,可能会造成系统不稳定。

AutoLoadCache 会在缓存快过期之前发起一个异步请求,去数据源加载数据,来减少这方面的风险。

7. 批量删除缓存

在很多时候,数据查询条件是比较复杂,我们无法获取或还原要删除的缓存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) ;
    
}

8. 双写不一致问题

先来看下面的代码:

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,等事务提交后再去执行缓存删除操作。其实不能说是“解决不一致问题”,而是缓解而已。

缓存数据双写不一致的问题是很难解决的,即使我们只用数据库(单写的情况)也会存在数据不一致的情况(当从数据库中取数据时,同时又被更新了),我们只能是减少不一致情况的发生。对于一些比较重要的数据,我们不能直接使用缓存中的数据进行计算并回写的数据库中,比如扣库存,需要对数据增加版本信息,并通过乐观锁等技术来避免数据不一致问题。

9. 支持多种缓存操作

大部分情况下,我们都是对缓存进行读与写操作,可有时,我们只需要从缓存中读取数据,或者只写数据,那么可以通过 @Cache 的 opType 指定缓存操作类型。现支持以下几种操作类型:

  1. READ_WRITE:读写缓存操:如果缓存中有数据,则使用缓存中的数据,如果缓存中没有数据,则加载数据,并写入缓存。默认是READ_WRITE;
  2. WRITE:从数据源中加载最新的数据,并写入缓存。对数据源和缓存数据进行同步;
  3. READ_ONLY: 只从缓存中读取,并不会去数据源加载数据。用于异地读写缓存的场景;
  4. LOAD :只从数据源加载数据,不读取缓存中的数据,也不写入缓存。

另外在@Cache中只能静态指写缓存操作类型,如果想在运行时调整操作类型,需要通过CacheHelper.setCacheOpType()方法来进行调整。

最后欢迎大家到github对AutoLoadCache开源项目Star和Fork进行支持。

© 著作权归作者所有

共有 人打赏支持
qiujiayu
粉丝 51
博文 29
码字总数 12610
作品 1
东城
架构师
私信 提问
缓存那些事

首先我画了一个粗糙的图形,用来表示我们使用web请求所经历的过程。大家将就看下 从图可以看出,当用户发起一个网络请求。首先会经过浏览器的缓存,然后经过静态资源的CDN缓存,然后是Nginx...

-鹏
2016/03/11
123
0
CDN缓存那些事

CDN是什么? 谈到CDN的作用,可以用8年买火车piao的经历来形象比喻: 8年前,还没有火车piao代售点一说,12306.cn更是无从说起。那时候火车piao还只能在火车站的售piao大厅购买,而我所住的小...

浮世微凉
2015/11/27
517
0
手写启动一个本地服务器的命令行工具

本篇主要将 实现一个自己的命令行工具,web缓存,以及web文件压缩融合到一起,如果有部分不是太清楚的地方可以查看文末的链接。 实现本地服务器命令行工具 xl-server 我们知道实现在本地命令行...

xlei1123
07/26
0
0
一款轻量级的 iOS 图像缓存

我们的 iOS 应用都包含了大量的图像。创建富有吸引力的视图,主要依赖于大量的装饰图片,所有这些首先必须从远程服务器获取。如果每次打开应用都要从服务器一次又一次的获取每个图像,那么用...

oschina
2014/07/16
8.7K
2
CPU与内存的那些事

本文以一个现代的、实际的个人电脑为对象,分析其中CPU(Intel Core 2 Duo 3.0GHz)以及各类子系统的运行速度——延迟和数据吞吐量。通过粗略的估算PC各个组件的相对运行速度,希望能给大家留...

浮躁的码农
2015/08/11
175
0

没有更多内容

加载失败,请刷新页面

加载更多

传播正能量——《海南英才》阅读的读后感2200字

传播正能量——《海南英才》阅读的读后感2200字: 新华社记者12月8日从公安部获悉,针对自媒体“网络水军”敲诈勒索等违法犯罪活动突出问题,今年以来,公安部组织各地公安机关依法深入开展侦...

原创小博客
17分钟前
2
0
Confluence 6 对一个空间进行归档后产生的影响

空间 如果一个空间被归档: 将不会在查找结果中显示,除非你选择 在归档空间中查找(Search archived spaces)。如果没有归档空间的话,这个功能是隐藏的。 页面和内容将不会在 Confluence 的...

honeymose
19分钟前
2
0
java框架学习日志-2

上篇文章(java框架学习日志-1)虽然跟着写了例子,也理解为什么这么写,但是有个疑问,为什么叫控制反转?控制的是什么?反转又是什么? 控制其实就是控制对象的创建。 反转与正转对应,正转...

白话
今天
5
0
Integer使用双等号比较会发生什么

话不多说,根据以下程序运行,打印的结果为什么不同? Integer a = 100;Integer b = 100;System.out.println(a == b);//print : trueInteger a = 200;Integer b = 200;System.out.pr...

兜兜毛毛
昨天
10
0
CockroachDB

百度云上的CockroachDB 云数据库 帮助文档 > 产品文档 > CockroachDB 云数据库 > 产品描述 开源NewSQL – CockroachDB在百度内部的应用与实践 嘉宾演讲视频及PPT回顾:http://suo.im/5bnORh ...

miaojiangmin
昨天
6
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部