本文主要介绍Redis的常用场景,这都是由Redis的特性决定的。并尽量从源码角度说明Redis为什么适合于这种场景。
I、积分排行榜
1、积分排行榜是Redis的经典应用,这是得益于Redis提供了zset的有序集合数据结构。
2、针对zset,Redis提供了一系列的命令:
ZADD
: 添加新元素;
ZRANGE
: 按分数从低到高返回给定排名区间的元素;
ZREVRANGE
: 按分数从高到底返回给定区间的元素;
ZRANK
: 返回某个元素的排名。
3、实现:下面的例子为用python完成的一个leaderboard
# -*- coding: utf-8 -*-
#!/usr/bin/python
import redis
class Leaderboard:
def __init__(self,host,port,key,db):
self.host = host
self.port = port
self.key = key
self.db = db
self.r = redis.StrictRedis(host=self.host,port=self.port,db=self.db)
def isRedisValid(self):
return self.r is None
def addMember(self,member,score):
if self.isRedisValid():
return None
return self.r.zadd(self.key,score,member)
def delMember(self,member):
if self.isRedisValid():
return None
return self.r.zrem(self.key,member)
def incrScore(self,member,increment):
"""increase score on specified member"""
if self.isRedisValid():
return None
return self.r.zincrby(self.key,member,increment)
def getRankByMember(self,member):
"""Get ranking by specified member."""
if self.isRedisValid():
return None
return self.r.zrank(self.key,member)
def getLeaderboard(self,start,stop,reverse,with_score):
"""Return the whole leaderboard."""
if self.isRedisValid():
return None
return self.r.zrange(self.key,start,stop,reverse,with_score)
def getLeaderboardByPage(self,item_per_page,page_num,reverse=False,with_score=False):
"""Return part of leaderboard configurably."""
# fix parameters
if item_per_page <= 0:
item_per_page = 5
if page_num <= 0:
page_num = 1
# note: it is possible that return value is empty list.
return self.getLeaderboard((page_num-1)*item_per_page,
page_num*item_per_page-1,
reverse,with_score)
def getWholeLeaderboard(self,reverse=False,with_score=False):
"""Return the whole leaderboard."""
return self.getLeaderboard(0,-1,reverse,with_score)
II、分布式锁
1、我们通常所提到的锁,一般都是在本地环境下对多线程完成互斥操作,而如果共享资源存在于网络上,本地的锁就不起作用了。
互斥访问某个网络资源的时候,需要有一个在网络上的锁服务器,负责锁的申请与回收,Redis就可以作为这种分布式锁的服务器。
2、因为Redis是单进程单线程的工作模式,所以前来申请锁资源的请求都被排队处理,能保证锁资源的同步访问。
3、实现:可以在Redis服务器设置一个键值对,用以表示一把分布式互斥锁,当申请锁的时候,申请方SET
这个键值对,当释放锁的时候,释放方DEL
这个键值对:
lock = redis.get("mutex_lock");
if(!lock)
error("apply the lock error.");
else
-- 确定可以申请锁
redis.set("mutex_lock","locking");
do_something();
4、上述的申请方法,涉及到客户端与Redis服务器的多次交互,当客户端确定可以加锁的时候,可能这时候锁已经被其他客户端申请了,最终导致两个客户端同时持有锁。解决这个问题的方法就是将申请/释放锁的逻辑都放在服务器上。Redis Lua脚本可以完成这个问题:
-- apply for lock
local key = KEYS[1]
local res = redis.call('get', key)
-- 锁被占用,申请失败
if res == '0' then
return -1
-- 锁可以被申请
else
local setres = redis.call('set', key, 0)
if setres['ok'] == 'OK' then
return 0
end
end
return -1
get 命令不成功返回(nil).
实验命令:保存lua 脚本redis-cli script load ”$(cat mutex_lock.lua)”
-- releae lock
local key = KEYS[1]
local setres = redis.call('set', key, 1)
if setres['ok'] == 'OK' then
return 0
return -1
5、死锁问题
分布式锁由于客户端的崩溃很容易造成锁无法释放,从而出现死锁。所以需要使用Redis的TTL功能,设置超时释放:
-- apply for lock
local key = KEYS[1]
local timeout = KEYS[2]
local res = redis.call('get', key)
-- 锁被占用,申请失败
if res == '0' then
return -1
-- 锁可以被申请
else
local setres = redis.call('set', key, 0)
local exp_res = redis.call('pexpire', key, timeout)
if exp_res == 1 then
return 0
end
end
return -1
6、此外如出现服务器宕机,也会出现死锁问题,这就需要slave服务器。
III、消息中间件
1、消息中间件可以理解为分布式的消息队列。
消息中间件负责接收生产者的消息,并存储转发给对应的消费者,生产者按照topic发布消息,消费者按topic订阅各种消息。生产者只需要向中间件中推送消息,不用等待消费者回应。
2、也就是说,这种分布式的消息中间件就是网络上的一个服务器,起到一个数据中转的功能。
IV、Web服务器存储session
1、以Redis作为session的存储,可以加速后台的处理速度,如购物车数据,就不必直接存储到磁盘数据库中,在这里Redis的作用就是一个缓存。
【参考】
[1] 《Redis源码日志》