Spring Data Redis 让 NoSQL 快如闪电(2)
【编者按】本文作者为 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
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
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
public Long
doInRedis(RedisConnection connection)
throws DataAccessException
{
return connection.ttl(redisTemplate
.getStringSerializer().serialize(lockKey));
}
})) { // set expiration of the lock
redisTemplate.execute(new RedisCallback
{
@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
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)过滤器
布隆过滤器 (Bloom filter) 是一种空间利用率较高的概率数据结构,用来测试某元素是否某个集的一员。可能会出现误报匹配,但不会漏报。查询可返回“可能在集内”或“肯定不在集内”。
就在线服务和离线服务包括大数据分析等方面,布隆过滤器数据结构都能派上很多用场。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
final List
redisTemplate.executePipelined(new RedisCallback
浙公网安备 33010602011771号