我们先从一个相对简单的用例开始吧:一个增量计数器,可显示某网站受到多少次点击。spring data redis 有两个适用于这一实用程序的类:<code>redisatomicinteger</code> 和 <code>redisatomiclong</code>。和 java 并发包中的 <code>atomicinteger</code> 和 <code>atomiclong</code> 不同的是,这些 spring 类能在多个 jvm 中发挥作用。
列表 3:全局唯一增量计数器
请注意整型溢出并谨记,在这两个类上进行操作需要付出相对较高的代价。
时不时的,用户就得应对服务器集群的争用。假设你从一个服务器集群运行一个预定作业。在没有全局锁的情况下,集群中的节点会发起冗余作业实例。假设某个聊天室分区可容纳 50 人。如果聊天室已满,就需要创建新的聊天室实例来容纳另外 50 人。
列表4:全局悲观锁
如果使用关系数据库,一旦最先生成锁的程序意外退出,锁就可能永远得不到释放。redis 的 <code>expire</code> 设置可确保在任何情况下释放锁。
假设 web 客户端需要轮询一台 web 服务器,针对某个数据库中的多个表查询客户指定更新内容。如果盲目地查询所有相应的表以寻找潜在更新,成本较高。为了避免这一做法,可以尝试在 redis 中给每个客户端保存一个整型作为脏指标,整型的每个数位表示一个表。该表中存在客户所需更新时,设置数位。轮询期间,不会触发对表的查询,除非设置了相应数位。就获取并将这样的位屏蔽设置为 <code>string</code> 而言,redis 非常高效。
借助出色的速度和处理能力,redis 极好地融合了布隆过滤器。搜索 github,就能发现很多 redis 布隆过滤器项目,其中一些还支持可调谐精度。
redis 发布/订阅渠道的工作方式类似于一个扇出消息传递系统,或 jms 语义中的一个主题。jms 主题和 redis 发布/订阅渠道的一个区别是,通过 redis 发布的消息并不持久。消息被推送给所有相连的客户端后,redis 上就会删除这一消息。换句话说,订阅者必须一直在线才能接收新消息。redis 发布/订阅渠道的典型用例包括实时配置分布、简单的聊天服务器等。
在 web 服务器集群中,每个节点都可以是 redis 发布/订阅渠道的一个订阅者。发布到渠道上的消息也会被即时推送到所有相连节点。这一消息可以是某种配置更改,也可以是针对所有在线用户的全局通知。和恒定轮询相比,这种推送沟通模式显然极为高效。
redis 非常强大,但也可以从整体上和根据特定编程场景做出进一步优化。可以考虑以下技巧。
所有 redis 数据结构都具备存活时间 (ttl) 属性。当你设置这一属性时,数据结构会在过期后自动删除。充分利用这一功能,可以让 redis 保持较低的内存损耗。
在一条请求中向 redis 发送多个命令,这种方法叫做管道技术。这一技术节省了网络往返的成本,这一点非常重要,因为网络延迟可能比 redis 延迟要高上好几个量级。但这里存在一个陷阱:管道中的 redis 命令列表必须预先确定,并且应当彼此独立。如果一个命令的参数是由先前命令的结果计算得出,管道技术就不起作用。列表 5 给出了 redis 管道技术的一个示例。
列表 5:管道技术
redis 并不像关系数据库管理系统那样能支持全面的 acid 事务,但其自有的事务也非常有效。从本质上来说,redis 事务是管道、乐观锁、确定提交和回滚的结合。其思想是执行一个管道中的一个命令列表,然后观察某一关键记录的潜在更新(乐观锁)。根据所观察的记录是否会被另一个进程更新,该命令列表或整体确定提交,或完全回滚。
下面以某个拍卖网站上的卖方库存为例。买方试图从卖方处购买某件商品时,你负责观察 redis 事务内的卖方库存变化。同时,你要从同一个库存中删除此商品。事务关闭前,如果库存被一个以上进程触及(例如,如果两个买方同时购买了同一件商品),事务将回滚,否则事务会确定提交。回滚后可开始重试。
在运行一个 <code>monitor</code> 命令后,我的团队发现,在进行 redis 操作或 <code>rediscallback</code> 后,spring 并没有自动关闭 redis 连接,而事实上它是应该关闭的。如果再次使用未关闭的连接,可能会从意想不到的 redis 密钥返回垃圾数据。有意思的是,如果在 <code>redistemplate</code> 中把事务支持设为 false,这一问题就不会出现了。
我们发现,我们可以先在 spring 语境里配置一个 <code>platformtransactionmanager</code>(例如 <code>datasourcetransactionmanager</code>),然后再用 <code>@transactional</code> 注释来声明 redis 事务的范围,让 spring 自动关闭 redis 连接。
根据这一经验,我们相信,在 spring 语境里配置两个单独的 <code>redistemplate</code> 是很好的做法:其中一个 redistemplates 的事务设为 false,用于大多数 redis 操作,另一个 redistemplates 的事务已激活,仅用于 redis 事务。当然必须要声明 <code>platformtransactionmanager</code> 和 <code>@transactional</code>,以防返回垃圾数值。
另外,我们还发现了 redis 事务和关系数据库事务(在本例中,即 jdbc)相结合的不利之处。混合型事务的表现和预想的不太一样。
我希望通过这篇文章向其他 java 企业开发师介绍 redis 的强大之处,尤其是将 redis 用作远程数据缓存和用于易挥发数据时。在这里我介绍了 redis 的六个有效用例,分享了一些性能优化技巧,还说明了我的 glu mobile 团队怎样解决了 spring data redis 事务配置不当造成的垃圾数据问题。我希望这篇文章能够激发你对 redis nosql 的好奇心,让你能够受到启发,在自己的 java 企业版系统里创造出一番天地。