常用缓存框架及redis应用
一.缓存简介
互联网高速发展的今天,缓存技术已经成为大型网站的关键技术之一,其设计的好坏直接关系到用户体验,在考虑对服务性能表现进行提升时,缓存机制也往往是解决问题的重要起点--缓存技术被认为是减轻服务器负载、降低网络拥塞,减少访问延迟的有效途径之一.
多级缓存:缓存按不同位置和使用场景分类如下图
浏览器缓存:是最靠近用户的缓存,用户在访问同一个页面时,将不再从服务器下载页面,而是从本机的缓存目录中读取页面
网关或代理服务器缓存:是将网页缓存在网关服务器上,多用户访问同一个页面时,将直接从网关服务器把页面传送给用户,eg.Nginx.
页面缓存:是将动态页面直接生成静态的页面放在服务器端,用户调取相同页面时,直接下载到客户端,除了整个页面的缓存,还有一种技术叫做”网页片段缓存技术”,将页面的部分而不是全部进行缓存,代表作有ESIcache.
数据库缓存:数据库的缓存一般由数据库或提供,可以对表建立高速缓存,提高对经常访问的数据的访问速度
ORM框架:Hibernate的一级缓存(session级别)、二级缓存(sessionFactory级别)以及查询缓存.
数据缓存:详见2
二.三种缓存框架
常用数据缓存框架&技术有Ehcache和Memcached,Redis等
2.1 Ecache
号称使用最广泛的轻量级缓存框架,基于java开发,用于大型分布式应用的各个节点中.
特点:高性能,简单可靠,易扩展
- 广泛的运用于其他的开源项目比如:hibernate
- 发布版本最大不会超过2M,一般只有几百K
- 核心程序仅仅依赖slf4j这一个包,没有之一!
- 提供LRU、LFU、FIFO等淘汰算法,支持热配置、支持插件
2.2 Memcached
Memcached最初是由BradFitzpatrick于2003年开发而成,并在逐步发展为现代Web应用程序的构建基石.Memcache致力于一种高性能、分布式对象缓存系统,可以把它想象成一个大的内存HashTable,最初设计用于缓解动态网站数据库加载数据的延迟.
- C语言编写,高性能(最初实现方式为Perl语言),理论上性能的瓶颈落在网卡上
- 分布式
- 多线程机制
- 当前开发工作主要关注运行稳定性及效果优化,不再积极为打造更多功能
2.3 Redis
Redis则由SalvatoreSanfilippo于2009年创建,时至今日Sanfilippo仍然担任着该项目的首席开发者以及惟一维护者的角色.redis是一款先进的Key/Value存储系统,具有丰富的数据类型,它与Memcached类似,本质是缓存技术和数据库技术的一种融合产物.Redis有时候会被人们称为”强化版的Memcached”.
- 丰富的数据类型和功能
- 简单事务支持
- 持久化
- 具备数据库特征
- 提供主从复制功能
2.4三种缓存框架比较
Ecache
Ecache主要用于进程内缓存管理,虽然通过RMI,JGroups,JMS三种方式提供了分布式缓存同步机制,但比较低级,不适合分布式环境下的应用.
Memcached的优点
- 1.单实例更快:使用预分配内存池的方式,可以省去申请/释放内存的开销,并且能减小内存碎片产生.多线程实现可以更充分利用cpu.
- 2.第三方支持更加成熟稳定:Memcached历史更加悠久,redis协议本身比Memcached复杂
Redis的优点
Redis在很多方面具备数据库的特征,或者说就是一个数据库系统,而Memcached只是简单的K/V缓存.Redis被大部分技术人员视为首选目标.
- 1.支持持久化:redis的本地持久化支持两种方式:RDB和AOF.RDB配置持久化触发器,AOF指的是redis每增加一条记录都会保存到持久化文件中.在灾难恢复和数据备份上有天然的优势
- 2.丰富的数据类型:redis支持String、Lists、sets、sortedsets、hashes多种数据类型
- 3.功能丰富,更新快
- 4.高性能
在别人发了一个memcache性能比redis好很多的博客后,redis作者发表了一篇博文,主要是说到如何给redis和memcache做压力测试,作者在相同的运行环境做了三次测试的取值,得到的结果如下图:
需要指出的是此次测试是单核处理过程,memcache是支持多核多线程操作的(默认没开),其实没有比较的意义.作者从2.2版本后专注rediscluster的方向开发,通过纵向扩展的方式来缓解其性能上的弊端.
何时使用Memcached
有两类特殊场景仍然是Memcached的天下.首先就是对小型静态数据进行缓存处理,最具代表性的例子就是HTML代码片段.Memcached在处理元数据时所消耗的内存资源相对更少.除此之外,Memcached在横向扩展方面也比Redis更具优势.由于其在设计上的思路倾向以及相对更为简单的功能设置,Memcached在实现扩展时的难度比Redis低得多.
何时使用Redis
除非大家需要考虑某种限定性条件(例如处理传统应用程序)对于Memcached的特殊依赖性,或者实际用例属于前面提到的两类场景中的一种,请直接选择Redis.
可能的结论
Redis与Memcached作为两款广受赞誉而且久经考验的解决方案,成为完成缓存机制的两大首选技术成果.Redis更多场景是作为Memcached的替代者来使用,从功能多样性以及设计先进性的角度出发,Redis几乎在缓存管理工作中的每一个侧面都表现出显而易见的优越性,显然更适合作为通用首选方案.
三.redis的事务
redis支持简单的事务,简单的意思指的是它不支持’事务’或者ACID原则.
Redis事务就是把几个命令合并,一次性执行全部命令——不支持回滚,只保证命令依次被执行,即使出错也会继续往下执行,本质上是客户端可以一次性执行多条命令.
3.1 multi / exec
执行过程:执行multi命令开始事务--输入需要执行的命令--输入exec执行事务.
过程分析:redis服务器收到multi命令后,会将对应的client的状态设置为REDIS_MULTI,表示client处于事务阶段,并在client的multiState结构体里面保持事务的命令具体信息(当然首先也会检查命令是否能否识别,错误的命令不会保存),即命令的个数和具体的各个命令,当收到exec命令后,redis会顺序执行multiState里面保存的命令,然后保存每个命令的返回值,当有命令发生错误是否继续执行需要区分版本和场景—— 语法错误Redis 2.6.5之前的版本会忽略错误的命令,执行其他正确的命令,2.6.5之后的版本会忽略这个事务中的所有命令,都不执行.运行错误表示命令在执行过程中出现错误,这种错误在命令执行之前Redis是无法发现的,所以在事务里这样的命令会被Redis接受并执行。如果事务里有一条命令执行错误,其他命令依旧会执行
3.2 watch / unwatch
执行过程:redis收到exec命令后,事务的执行过程是不会被打断的,但是在用户逐个输入事务的命令的时候,期间可能已经有别的客户修改了事务里面用到的数据,所以redis还提供了watch命令,用户可以在输入multi之前,执行watch命令,指定需要观察的数据,这样如果在exec之前,有其他的客户端修改了这些被watch的数据,则exec执行失败,提示数据已经dirty.
如何实现:每一个redisdb中还有一个dict watched_keys,dictentry的key是被watch的key,而value则是一个list,里面存储的是watch它的client.每个client也有一个watched_keys,里面保存的是这个client当前watch的key.在执行watch的时候,redis在对应的数据库的watched_keys中找到这个key(如果没有,则新建一个dictentry),然后在它的客户列表中加入这个client,同时,往这个client的watched_keys中加入这个key.当有客户执行一个命令修改数据的时候,redis首先在watched_keys中找这个key,遍历所有watch它的client,将这些client设置为REDIS_DIRTY_CAS.当client执行的事务的时候,首先会检查是否被设置了REDIS_DIRTY_CAS,如果是,则表明数据dirty了,事务无法执行,会立即返回错误. 执行exec后,该client的所有watch的key都会被清除,同时db中该key的client列表也会清除该client,即使exec没有执行成功也是一样).
如果不想执行事务中的命令,也可以使用UNWATCH命令来取消监控
四.reids应用及线上事故分析
项目中应用了缓存框架:Spring+Ecache+redis, 其中Ecache作单机缓存,redis作集中缓存处理,使用了Spring框架的 Spring-Cache和Spring-Data-Redis模块(Spring-Data-Redis核心是对jedis进一步的封装.)
4.1 redis配置
在Spring项目中配置reids应用相对简单,贴图如下
Spring-data-redis和jedis依赖
注:上图省去了redis连接参数的配置
RedisTemplate事务应用.待补充
1. 启用redisTemalate事务支持
配置文件中配置enableTransactionSupport=true.或者template.setEnableTransactionSupport(true);
2. 注意!!!:启用Spring事务注解@Transactional,否则会导致redis连接不释放。详见5参考资料
3. 实现方式,
(1)直接使用redis命令.没有绑定同一个连接,可能有坑
- redisTemplate.watch(key)
- redisTemplate.multi ()
- redisTemplate.exec ()
- redisTemplate.unwatch()
(2)回调方式.保持同一连接,推荐!!!
redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations operations) {
operations.watch(key);
String value = operations.opsForValue().get(key);
operations.multi();
operations.opsForValue().set(key,value );
Object rs = operations.exec();
return rs;
}});
4.2 线上事故分析.待补充
事故表现:
- 日志显示大量空指针异常.
- 上线前QA多次回归功能,未发现问题.
- 线下调试发现该问题概率性出现
l 通过堆栈信息定位到抛空指针的代码,redisTemplate.size返回NULL,确认是redis使用问题
l 上线功能涉及redis的改动:redis事务支持,spring-data-session-redis.连接池配置
进一步定位:
l Spring-data-session-redis属于框架使用,不涉及业务,出问题概率较低.
l 由于使用场景增加,上线前出现无法获取连接的问题,更改redis连接池配置后上线,属于重点怀疑对象.初期反复调试更改最大最小连接数,超时时间等后,失败概率有所降低,但无法完全根治.问题可能和连接池配置有一定关系.待详解
l 源码定位<待补充>:
下载Spring-data-redis源码
1. 定位size操作—DefaultSetOperations.
可以看出size函数最终使用的是redisConnectin的sCard命令,接着跟踪
2. 定位sCard操作---JedisConnection
jedisConnection.sCard,spring-data-redis的底层还是使用的jedis,在执行scard命令前,分别经过两个步骤判断isPipelined和isQueueing,inspect发现isQueueing=true!!!
3. isInMulti !!!
加上transaction这个诡异的名字,大概可以窥出端倪
4. BinaryClient中唯一处将client置为isinmulti=true的操作只有一个命令multi,大坑!!
问题已经比较明显,multi命令将client isInMulti标志位置为true,导致请求被放到队列内而本次调用返回null.
由于代码中使用到multi的地方只有一处,很容易就定位到了问题代码,随后下线redis事务得到解决,略坑!
Question:redisTempate执行事务期间,仍能从连接池中获取到同一个client!?
Why?
定位源码.redisTemplate.excute – redisConnectionUtils从连接池中获取connection
逻辑稍复杂,有时间再详细分析,先给出结论如下:
4.3 结论
1.Redis很好很强大.
2.RedisTempalte各种坑, 慎用
五.参考资料
http://tech.it168.com/a2014/1016/1674/000001674122.shtml
http://www.cnblogs.com/xiaoluo501395377/p/3377604.html
https://msdn.microsoft.com/zh-cn/library/ff648482.aspx
https://segmentfault.com/a/1190000004393573
http://www.blogways.net/blog/2013/06/02/jedis-demo.html