zl程序教程

您现在的位置是:首页 >  数据库

当前栏目

Spring Data Redis 让 NoSQL 快如闪电(2)

2023-09-14 09:04:36 时间

【编者按】本文作者为 Xinyu Liu,文章的第一部分重点概述了 Redis 方方面面的特性。在第二部分,将介绍详细的用例。文章系国内 ITOM 管理平台 OneAPM 编译呈现。

把 Redis 当作数据库的用例

现在我们来看看在服务器端 Java 企业版系统中把 Redis 当作数据库的各种用法吧。无论用例的简繁,Redis 都能帮助用户优化性能、处理能力和延迟,让常规 Java 企业版技术栈望而却步。

1. 全局唯一增量计数器

我们先从一个相对简单的用例开始吧:一个增量计数器,可显示某网站受到多少次点击。Spring Data Redis 有两个适用于这一实用程序的类:RedisAtomicInteger 和 RedisAtomicLong。和 Java 并发包中的 AtomicInteger 和 AtomicLong 不同的是,这些 Spring 类能在多个 JVM 中发挥作用。

列表 3:全局唯一增量计数器

RedisAtomicLong counter = 

 new RedisAtomicLong("UNIQUE_COUNTER_NAME", redisTemplate.getConnectionFactory()); 

Long myCounter = counter.incrementAndGet();// return the incremented value

请注意整型溢出并谨记,在这两个类上进行操作需要付出相对较高的代价。

2. 全局悲观锁

时不时的,用户就得应对服务器集群的争用。假设你从一个服务器集群运行一个预定作业。在没有全局锁的情况下,集群中的节点会发起冗余作业实例。假设某个聊天室分区可容纳 50 人。如果聊天室已满,就需要创建新的聊天室实例来容纳另外 50 人。

如果检测到聊天室已满但没有全局锁,集群中的各个节点就会创建自有的聊天室实例,为整个系统带来不可预知的因素。列表 4 介绍了应当如何充分利用 SETNX(SET if Not eXists:如果不存在,则设置)这一 Redis 命令来执行全局悲观锁。

列表4:全局悲观锁

public String aquirePessimisticLockWithTimeout(String lockName, int acquireTimeout, int lockTimeout) { 

 if (StringUtils.isBlank(lockName) || lockTimeout = 0) 

 return null; 

 final String lockKey = lockName;

 String identifier = UUID.randomUUID().toString(); 

 Calendar atoCal = Calendar.getInstance();

 atoCal.add(Calendar.SECOND, acquireTimeout);

 Date atoTime = atoCal.getTime(); 

 while (true) { 

 // try to acquire the lock 

 if (redisTemplate.execute(new RedisCallback Boolean () { @Override 

 public Boolean doInRedis(RedisConnection connection) throws DataAccessException { 

 return connection.setNX(

redisTemplate.getStringSerializer().serialize(lockKey), redisTemplate.getStringSerializer().serialize(identifier));

 })) { // successfully acquired the lock, set expiration of the lock

 redisTemplate.execute(new RedisCallback Boolean () { @Override 

 public Boolean doInRedis(RedisConnection connection) throws DataAccessException { 

 return connection.expire(redisTemplate

 .getStringSerializer().serialize(lockKey),

 lockTimeout);

 }); 

 return identifier;

 } else { // fail to acquire the lock 

 // set expiration of the lock in case ttl is not set yet. if (null == redisTemplate.execute(new RedisCallback Long () { @Override 

 public Long 

 doInRedis(RedisConnection connection) 

 throws DataAccessException 

 return connection.ttl(redisTemplate

 .getStringSerializer().serialize(lockKey));

 })) { // set expiration of the lock

 redisTemplate.execute(new RedisCallback Boolean () 

 @Override 

 public Boolean 

 doInRedis(RedisConnection connection) throws DataAccessException { 

 return connection.expire(redisTemplate

 .getStringSerializer().serialize(lockKey),

 lockTimeout);

 }); 

} if (acquireTimeout 0) // no wait 

 return null; 

 else { 

 try {

 Thread.sleep(100l); // wait 100 milliseconds before retry

 } catch (InterruptedException ex) {

 } if (new Date().after(atoTime)) break;

 } return null;


public void releasePessimisticLockWithTimeout(String lockName, String identifier) { if (StringUtils.isBlank(lockName) || StringUtils.isBlank(identifier)) return; final String lockKey = lockName; redisTemplate.execute(new RedisCallback Void () { @Override public Void doInRedis(RedisConnection connection) throws DataAccessException { byte[] ctn = connection.get(redisTemplate .getStringSerializer().serialize(lockKey)); if(ctn!=null identifier.equals(redisTemplate.getStringSerializer().deserialize(ctn))) connection.del(redisTemplate.getStringSerializer().serialize(lockKey)); return null;

如果使用关系数据库,一旦最先生成锁的程序意外退出,锁就可能永远得不到释放。Redis 的 EXPIRE 设置可确保在任何情况下释放锁。

3. 位屏蔽(Bit Mask)

假设 web 客户端需要轮询一台 web 服务器,针对某个数据库中的多个表查询客户指定更新内容。如果盲目地查询所有相应的表以寻找潜在更新,成本较高。为了避免这一做法,可以尝试在 Redis 中给每个客户端保存一个整型作为脏指标,整型的每个数位表示一个表。该表中存在客户所需更新时,设置数位。轮询期间,不会触发对表的查询,除非设置了相应数位。就获取并将这样的位屏蔽设置为 STRING 而言,Redis 非常高效。

4. 排行榜(Leaderboard)

Redis 的 ZSET 数据结构为游戏玩家排行榜提供了简洁的解决方案。ZSET 的工作方式有些类似于 Java 中的 PriorityQueue,各个对象均为经过排序的数据结构,井井有条。可以按照分数排出游戏玩家在排行榜上的位置。Redis 的 ZSET 定义了一份内容丰富的命令列表,支持灵活有效的查询。例如,ZRANGE(包括 ZREVRANGE)可返回有序集内的指定范围要素。

你可以使用这一命令列出排行榜前 100 名玩家。ZRANGEBYSCORE 返回指定分数范围内的要素(例如列出得分为 1000 至 2000 之间的玩家),ZRNK 则返回有序集内的要素的排名,诸如此类。

5. 布隆(Bloom)过滤器

[布隆过滤器]11 是一种空间利用率较高的概率数据结构,用来测试某元素是否某个集的一员。可能会出现误报匹配,但不会漏报。查询可返回“可能在集内”或“肯定不在集内”。

就在线服务和离线服务包括大数据分析等方面,布隆过滤器数据结构都能派上很多用场。Facebook 利用布隆过滤器进行输入提示搜索,为用户输入的查询提取朋友和朋友的朋友。Apache HBase 则利用布隆过滤器过滤掉不包含特殊行或列的 HFile 块磁盘读取,使读取速度得到明显提升。Bitly 用布隆过滤器来避免将用户重定向到恶意网站,而 Quara 则在订阅后端执行了一个切分的布隆过滤器,用来过滤掉之前查看过的内容。在我自己的项目里,我用布隆过滤器追踪用户对各个主题的投票情况。

借助出色的速度和处理能力,Redis 极好地融合了布隆过滤器。搜索 GitHub,就能发现很多 Redis 布隆过滤器项目,其中一些还支持可调谐精度。

6. 高效的全局通知:发布/订阅渠道

Redis 发布/订阅渠道的工作方式类似于一个扇出消息传递系统,或 JMS 语义中的一个主题。JMS 主题和 Redis 发布/订阅渠道的一个区别是,通过 Redis 发布的消息并不持久。消息被推送给所有相连的客户端后,Redis 上就会删除这一消息。换句话说,订阅者必须一直在线才能接收新消息。Redis 发布/订阅渠道的典型用例包括实时配置分布、简单的聊天服务器等。

在 web 服务器集群中,每个节点都可以是 Redis 发布/订阅渠道的一个订阅者。发布到渠道上的消息也会被即时推送到所有相连节点。这一消息可以是某种配置更改,也可以是针对所有在线用户的全局通知。和恒定轮询相比,这种推送沟通模式显然极为高效。

Redis 性能优化

Redis 非常强大,但也可以从整体上和根据特定编程场景做出进一步优化。可以考虑以下技巧。

所有 Redis 数据结构都具备存活时间 (TTL) 属性。当你设置这一属性时,数据结构会在过期后自动删除。充分利用这一功能,可以让 Redis 保持较低的内存损耗。

在一条请求中向 Redis 发送多个命令,这种方法叫做管道技术。这一技术节省了网络往返的成本,这一点非常重要,因为网络延迟可能比 Redis 延迟要高上好几个量级。但这里存在一个陷阱:管道中的 Redis 命令列表必须预先确定,并且应当彼此独立。如果一个命令的参数是由先前命令的结果计算得出,管道技术就不起作用。列表 5 给出了 Redis 管道技术的一个示例。

列表 5:管道技术

@Override

public List LeaderboardEntry fetchLeaderboard(String key, String... playerIds) { 

 final List LeaderboardEntry entries = new ArrayList ();

 redisTemplate.executePipelined(new RedisCallback Object () { // enable Redis Pipeline 

 @Override 

 public Object doInRedis(RedisConnection connection) throws DataAccessException { 

 for(String playerId : playerIds) {

 Long rank = connection.zRevRank(key.getBytes(), playerId.getBytes());

 Double score = connection.zScore(key.getBytes(), playerId.getBytes());

 LeaderboardEntry entry = new LeaderboardEntry(playerId, 

 score!=null?score.intValue():-1, rank!=null?rank.intValue():-1);

 entries.add(entry);

 return null; 

 }); 

 return entries; 

副本集和切分

Redis 支持主从副本配置。和 MongoDB 一样,副本集也是不对称的,因为从节点是只读的,以便共享读取工作量。我在文章开头提到过,也可以执行切分来横向扩展 Redis 的处理能力和存储容量。事实上,Redis 非常强大,据亚马逊公司的内部基准显示,类型 r3.4xlarge 的一个 EC2 实例每秒可轻松处理 100000 次请求。传说还有把每秒 700000 次请求作为基准的。对于中小型应用程序,通常无需考虑 Redis 切分。(请参见这篇非常出色的文章《运行中的 Redis》,进一步了解 Redis 的性能优化和切分。)

Redis 中的事务

Redis 并不像关系数据库管理系统那样能支持全面的 ACID 事务,但其自有的事务也非常有效。从本质上来说,Redis 事务是管道、乐观锁、确定提交和回滚的结合。其思想是执行一个管道中的一个命令列表,然后观察某一关键记录的潜在更新(乐观锁)。根据所观察的记录是否会被另一个进程更新,该命令列表或整体确定提交,或完全回滚。

下面以某个拍卖网站上的卖方库存为例。买方试图从卖方处购买某件商品时,你负责观察 Redis 事务内的卖方库存变化。同时,你要从同一个库存中删除此商品。事务关闭前,如果库存被一个以上进程触及(例如,如果两个买方同时购买了同一件商品),事务将回滚,否则事务会确定提交。回滚后可开始重试。

Spring Data Redis 中的事务陷阱

我在 Spring 的 RedisTemplate 类 redisTemplate.setEnableTransactionSupport(true); 中启用 Redis 事务时得到一个惨痛的教训:Redis 会在运行几天后开始返回垃圾数据,导致数据严重损坏。StackOverflow 上也报道了类似情况。

在运行一个 monitor 命令后,我的团队发现,在进行 Redis 操作或 RedisCallback 后,Spring 并没有自动关闭 Redis 连接,而事实上它是应该关闭的。如果再次使用未关闭的连接,可能会从意想不到的 Redis 密钥返回垃圾数据。有意思的是,如果在 RedisTemplate 中把事务支持设为 false,这一问题就不会出现了。

我们发现,我们可以先在 Spring 语境里配置一个 PlatformTransactionManager(例如 DataSourceTransactionManager),然后再用 @Transactional 注释来声明 Redis 事务的范围,让 Spring 自动关闭 Redis 连接。

根据这一经验,我们相信,在 Spring 语境里配置两个单独的 RedisTemplate 是很好的做法:其中一个 RedisTemplates 的事务设为 false,用于大多数 Redis 操作,另一个 RedisTemplates 的事务已激活,仅用于 Redis 事务。当然必须要声明 PlatformTransactionManager 和 @Transactional,以防返回垃圾数值。

另外,我们还发现了 Redis 事务和关系数据库事务(在本例中,即 JDBC)相结合的不利之处。混合型事务的表现和预想的不太一样。

我希望通过这篇文章向其他 Java 企业开发师介绍 Redis 的强大之处,尤其是将 Redis 用作远程数据缓存和用于易挥发数据时。在这里我介绍了 Redis 的六个有效用例,分享了一些性能优化技巧,还说明了我的 Glu Mobile 团队怎样解决了 Spring Data Redis 事务配置不当造成的垃圾数据问题。我希望这篇文章能够激发你对 Redis NoSQL 的好奇心,让你能够受到启发,在自己的 Java 企业版系统里创造出一番天地。

本文转自 OneAPM 官方博客

原文地址:http://www.javaworld.com/article/3062899/big-data/lightning-fast-nosql-with-spring-data-redis.html?page=2


Redis 击穿&穿透&雪崩&spring data redis 概念:redis作为缓存,设置了key的过期时间,key在过期的时候刚好出现并发访问,直接击穿redis,访问数据库 解决方案:使用setnx() - 相当于一把锁,设置的时候,发现设置过期,加锁,只有获得锁的人才可以访问DB,这样就能防止击穿。
Java操作Redis(Spring Data Redis) Spring Data Redis 是 Spring 的一部分,提供了在 Spring 应用中通过简单的配置就可以访问 Redis 服务,对 Redis 底层开发包进行了高度封装。在 Spring 项目中,可以使用Spring Data Redis来简化 Redis 操作。网址:https://spring.io/projects/spring-data-redismaven坐标:
Spring Data Redis 详解及实战一文搞定 SDR - Spring Data Redis的简称。 Spring Data Redis提供了从Spring应用程序轻松配置和访问Redis的功能。它提供了与商店互动的低级别和高级别抽象,使用户免受基础设施问题的困扰。 Spring Boot 实战