Spring Data Redis 让 NoSQL 快如闪电 (1)
【编者按】本文作者为 Xinyu Liu,详细介绍了 Redis 的特性,并辅之以丰富的用例。在本文的第一部分,将重点概述 Redis 的方方面面。文章系国内 ITOM 管理平台 OneAPM 编译呈现。
建立在 Java 企业版之上的多层体系结构是强大的服务器端编程解决方案。作为一名从业多年的 Java 企业版开发人员,我最满意的就是三层企业开发法:最下方是 JPA/Hibernate 持久层,中间是 Spring 或 EJB 应用层,最上方则是 web 层。对于较为复杂的用例,我用 BPM(业务流程管理)、一个类似于 Drools 的规则引擎和一个集成框架(例如 Camel)集成了一个工作流驱动的解决方案。
但是,笔者最近接到一个任务,要设计一个拥有亚秒级响应延迟并能支持成千上万名并发用户的系统。我立即发现了自己常用的 Java 企业版栈区的局限性。基于关系数据库管理系统的传统型 web 应用程序,包括在 Hibernate/JPA 之上构建的应用程序,都有二阶延迟,扩展效果不佳。传统的 Java 企业版持久性体系结构无法满足我当时设计的系统的性能和处理能力要求。然后我转而尝试 NoSQL,最后发现了 Redis。
作为一种内存键值数据库,Redis 打破了数据库的传统定义(将数据保存在硬盘上)。反之,使用 Redis 时可结合持久性的 NoSQL 数据库,比如 MongoDB、HBase、Cassandra 或 DynamoDB。Redis 以远程缓存服务器见长,对易挥发数据来说是极快型数据库。
在本文中,笔者会介绍一些有关 Redis 的简单用例和进阶用例以及性能调优情况。当然,我还会做个简单概述,但我相信各位基本都了解 NoSQL 及其各种解决方案。
Spring Data Redis
Redis 几乎拥有针对所有编程语言的各种客户端库,其中就包括 Java。Jedis 可能是最受欢迎的 Java 客户端库了。本文中的示例都基于 Spring Data Redis,我把它作为一个较高层次的包装程序 API。Spring Data Redis 不仅配置方便,而且拥有各种友好的 API 和实用插件。
Redis 概述
和大多数 NoSQL 数据库一样,Redis 舍弃了表格、行列的关系概念。而事实上,Redis 是一种键值数据库,利用独特的字符串键值来存储和检索每条记录。Redis 支持把以下内置数据结构作为所有记录的值:
-
STRING
保有单个字符串值。 -
LIST
、SET
和HASH
从语义上来说与 Java 中的相同数据结构相一致。 -
ZSET
是由浮点分数安排的字符串列表,类似于 Java 中的PriorityQueue
。
不同于关系数据库管理系统中的表,Redis 数据结构是即时实例化的。如果用户查询的内容不存在于 Redis 中,系统只会返回空值。虽然 Redis 不允许嵌套结构,但用户可以执行自定义的 Java 或 JSON 串行器/解串器,从而将 POJO 映射到字符串。通过这种方式,就可以把任意 Java bean 保存为 STRING
,或者将其放置在 LIST
、SET
中,等等。
性能和可扩展性
对于 Redis,人们注意到的第一个特点可能就是它的速度极快。根据记录的大小和连接的数量,性能基准会有所不同,但延迟通常为单数位毫秒。在大多数用例中,Redis 每秒最多可支持 50000 次请求。如果用户使用较高端的硬件,处理能力更可高达每秒 700000 次请求(但这一数值可能会被网卡带宽扼制)。
作为一种内存数据库,Redis 的存储容量有限; AWS EC2 中的最大实例为 r3.8xlarge,内存 244 GB。由于数据结构的索引和性能都经过优化,Redis 消耗的内存比所存储的数据量大得多。切分 Redis 有助于克服这一局限性。要把内存数据备份到硬盘上,可以在预定作业中进行时间点转储,也可以根据需要运行 dump
命令。
用 Spring 进行远程数据缓存
要想提升应用程序服务器的性能,数据缓存可能是性价比最高的办法了。利用 Spring 的缓存抽象注释(@Cacheable
、@CachePut
、@CacheEvict
、@Caching
和 @CacheConfig
)可以毫不费力地启用数据缓存。在 Spring 配置下,用户还可以把 Ehcache、Memcached 或 Redis 当作基本缓存服务器。
Encache 通常被配置成本地缓存层,具有嵌套结构,在应用的 JVM 上运行。 Memcached 和 Redis 都能作为独立的缓存服务器运行。要想把 Redis 缓存集成到基于 Spring 的应用中,需要使用 Spring Data Redis 的 RedisTemplate 和 RedisCacheManager。
在 Redis 中访问已缓存的对象,耗时通常不到数毫秒,和关系数据库查询相比,这大幅提升了应用程序的性能。
延迟和收益
亚马逊公司在很大程度上依赖缓存服务器来最大程度地减少其零售网站的延迟,该公司甚至曾经发布过一份案例分析,其中记录了延迟和收益之间的关系。
本地缓存与远程缓存
在没有网络开销的系统中,本地缓存快于远程缓存。本地缓存的缺点是,同一个对象的多个拷贝在服务器集群中的各个不同节点之中会同步得更快。正因如此,本地缓存仅适用于静态数据,例如可容忍短期滞后和不一致现象的系统级设置。如果为易挥发的业务数据(例如用户数据和交易数据)使用本地缓存,很有可能会以运行应用程序服务器的单个实例而告终。
远程缓存服务器就没有这一局限性。在同一个键的情况下,可保证缓存服务器上的对象只有一个拷贝。只要用户让缓存中的对象及其数据库值彼此保持同步,就无需处理过期数据。
列表 1 给出了一个 Spring 数据缓存的示例。
列表 1:在基于 Spring 的应用中启用缓存
@Cacheable(value="User_CACHE_REPOSITORY", key = "#id")
public User get(Long id) {
return em.find(User.class, id);
}
@Caching(put = {@CachePut(value="USER_CACHE_REPOSITORY", key = "#user.getId()")})
public User update(User user) {
em.merge(user);
return user;
}
@Caching(evict = {@CacheEvict(value="USER_CACHE_REPOSITORY", key = "#user.getId()")}) public void delete(User user) {
em.remove(user);
}
@Caching(evict = {@CacheEvict(value="USER_CACHE_REPOSITORY", key = "#user.getId()")}) public void evictCache(User user) {
}
这里的读取操作被 Spring 的 @Cacheable
注释围绕,作为 AOP 幕僚而执行。Spring 中的存活时间设置也规定了这些对象可在缓存中停留的时间。调用 get()
方法后,Spring 就会试着先从远程缓存读取和返回对象。如果未找到对象,Spring 会执行方法主体,然后将数据库结果放在远程缓存中,之后再返回结果。
但如果另一个过程(例如另一个服务器节点)甚至同一个 JVM 中的另一个线程在数据库中更新了同一个对象,又会怎样呢?如果只运用 @Cacheable
注释,你可能会从远程缓存服务器收到过期拷贝。
为了防止发生这种情况,可以给所有数据库更新操作添加一个 @CachePut
注释。每次调用这些方法时,返回值就会替换掉远程缓存中原先的对象。在数据库读取和写入上都更新缓存,可以让缓存服务器和后台数据之间的记录保持同步。
容错
听起来简直完美,对吧?事实当然不是这样。利用列表 1 中的配置,负载较低时可能不会遇到任何问题,但随着服务器集群上的负载逐渐增加,远程缓存上就会出现过期数据。要做好准备应对服务器节点争用甚至更糟的情况。即使成功写入数据库,最后也可能会因为网络故障而使得缓存服务器 PUT
以失败告终。另外,NoSQL 通常不支持在关系数据库中存在完整事务语义,因为这会导致部分提交。为了让代码容错,可以考虑给数据模型增加版本号,实现乐观锁。
在收到 OptimisticLockingFailureException
或 CurrentModificationException
(具体取决于持久性解决方案)时,可以调用带有 @CacheEvict
注释的方法,从缓存中清除过期拷贝,然后重试同一个操作:
列表 2:解决缓存中的过期对象
try{
User user = userDao.get(id); // user fetched in cache server
userDao.update(user, oldname, newname);
}catch(ConcurrentModificationException ex) { // cached user object may be stale
userDao.evictCache(user);
user = userDao.get(id); // refresh user object
userDao.update(user, oldname, newname); // retry the same operation. Note it may still throw legitimate ConcurrentModificationException.}
结合 Elasticache 使用 Redis
Amazon Elasticache 是一款内存缓存服务,可结合 Memcached 或 Redis 作为缓存服务器使用。虽然 Elasticache 不在本文介绍范围内,但笔者还是想给各位开发人员介绍一个结合 Redis 使用 Elasticache 的技巧。对于大多数 Redis 参数,使用其默认值并无大碍,但
tcp-keepalive
和timeout
的默认 Redis 设置并不会移除已无效的客户连接,最后还会耗尽缓存服务器上的套接口。结合 Elasticache 使用 Redis 时,务必每次都明确设置这两个值。
在本文的第二部分,将介绍 Redis 的6大用例,敬请期待。
本文系 OneAPM 工程师编译整理。OneAPM 能为您提供端到端的 Java 应用性能解决方案,我们支持所有常见的 Java 框架及应用服务器,助您快速发现系统瓶颈,定位异常根本原因。分钟级部署,即刻体验,Java 监控从来没有如此简单。想阅读更多技术文章,请访问 OneAPM 官方技术博客。
本文转自 OneAPM 官方博客