Redis实战(一)

第一章:Redis基础

redis的存储类型

结构类型存储值读写能力
string字符串,整数或者浮点数操作字符串,对整数和浮点数自增或自减
list每个结点都包含一个字符串的链表链表两端推入或弹出元素,依据偏移量修剪链表,读取单个或者多个元素,根据值查找或者移除元素
set无序不重复的集合添加,获取,移除单个元素,检查元素是否存在,计算交并补,从集合里面随机获取元素
hash包含键值对的无序散列表添加获取移除单个键值对,获取所有键值对
zset字符串成员(member)和浮点数分值(score)之间的有序映射,排序由分值的大小决定添加获取移除单个键值对,根据分值范围或者成员来获取元素

各存储类型基本操作

string命令行为
get获取value
set设置key-value
del删除value
$ redis-cli                 # 启动redis-cli 客户端

redis 127.0.0.1:6379> set hello world    # 将键 hello 的值设置为 world 。

OK                     # SET 命令在执行成功时返回 OK ,Python 客户端会将这个 OK 转换成 True

redis 127.0.0.1:6379> get hello       # 获取储存在键 hello 中的值。

"world"                   # 键的值仍然是 world ,跟我们刚才设置的一样。

redis 127.0.0.1:6379> del hello       # 删除这个键值对。

(integer) 1                 # 在对值进行删除的时候,DEL 命令将返回被成功删除的值的数量。

redis 127.0.0.1:6379> get hello       # 因为键的值已经不存在,所以尝试获取键的值将得到一个 nil ,

(nil)                    # Python 客户端会将这个 nil 转换成 None。
list命令行为
rpush值推入列表右端
lrange获取范围内的所有值
lindex获取给定位置单个元素
lpop左端弹出一个值并返回
redis 127.0.0.1:6379> rpush list-key item  # 在向列表推入新元素之后,该命令会返回列表的当前长度。

(integer) 1                 #

redis 127.0.0.1:6379> rpush list-key item2 #

(integer) 2                 #

redis 127.0.0.1:6379> rpush list-key item  #

(integer) 3                 #

redis 127.0.0.1:6379> lrange list-key 0 -1 # 使用0为范围的起始索引,-1为范围的结束索引,

1) "item"                  # 可以取出列表包含的所有元素。

2) "item2"                 #

3) "item"                  #

redis 127.0.0.1:6379> lindex list-key 1   # 使用LINDEX可以从列表里面取出单个元素。

"item2"                   #

redis 127.0.0.1:6379> lpop list-key     # 从列表里面弹出一个元素,被弹出的元素不再存在于列表。

"item"                   #

redis 127.0.0.1:6379> lrange list-key 0 -1 #

1) "item2"                 #

2) "item"                  #
set命令行为
sadd添加元素到集合
smembers返回所有元素
sismember检查元素是否存在
srem如果存在就移除元素
redis 127.0.0.1:6379> sadd set-key item   # 在尝试将一个元素添加到集合的时候,

(integer) 1                 # 命令返回1表示这个元素被成功地添加到了集合里面,

redis 127.0.0.1:6379> sadd set-key item2  # 而返回0则表示这个元素已经存在于集合中。

(integer) 1                 # 

redis 127.0.0.1:6379> sadd set-key item3  #

(integer) 1                 # 

redis 127.0.0.1:6379> sadd set-key item   #

(integer) 0                 #

redis 127.0.0.1:6379> smembers set-key   # 获取集合包含的所有元素将得到一个由元素组成的序列,

1) "item"                  # Python客户端会将这个序列转换成Python集合。

2) "item2"                 #

3) "item3"                 #

redis 127.0.0.1:6379> sismember set-key item4  # 检查一个元素是否存在于集合中,

(integer) 0                   # Python客户端会返回一个布尔值来表示检查结果。

redis 127.0.0.1:6379> sismember set-key item  #

(integer) 1                   #

redis 127.0.0.1:6379> srem set-key item2  # 在使用命令移除集合中的元素时,命令会返回被移除的元素数量。

(integer) 1                 #

redis 127.0.0.1:6379> srem set-key item2  #

(integer) 0                 #

redis 127.0.0.1:6379> smembers set-key

1) "item"

2) "item3"
hash命令行为
hset在散列里关联给定的键值对
hget获取指定键的值
hgetall获取所有键值对
hdel如果存在就移除键
redis 127.0.0.1:6379> hset hash-key sub-key1 value1 # 在尝试添加键值对到散列的时候,

(integer) 1                     # 命令会返回一个值来表示给定的键是否已经存在于散列里面。

redis 127.0.0.1:6379> hset hash-key sub-key2 value2 #

(integer) 1                     #

redis 127.0.0.1:6379> hset hash-key sub-key1 value1 #

(integer) 0                     #

redis 127.0.0.1:6379> hgetall hash-key       # 获取散列包含的所有键值对,

1) "sub-key1"                    # Python客户端会将这些键值对转换为Python字典。

2) "value1"                     #

3) "sub-key2"                    #

4) "value2"                     #

redis 127.0.0.1:6379> hdel hash-key sub-key2    # 在删除键值对的时候,

(integer) 1                     # 命令会返回一个值来表示给定的键在移除之前是否存在于散列里面。

redis 127.0.0.1:6379> hdel hash-key sub-key2    #

(integer) 0                     #

redis 127.0.0.1:6379> hget hash-key sub-key1    # 从散列里面单独取出一个域。

"value1"                      #

redis 127.0.0.1:6379> hgetall hash-key

1) "sub-key1"

2) "value1"
zset命令行为
zadd将一个带有给定分值的成员添加到有序集合里
zrange根据元素在有序排列中所处的位置(从0开始),从有序集合里面获取多个元素
zrangebyscore获取给定分值范围内的所有元素
zrem如果存在就移除成员
redis 127.0.0.1:6379> zadd zset-key 728 member1   # 在尝试向有序集合添加元素的时候,

(integer) 1                     # 命令会返回新添加元素的数量。

redis 127.0.0.1:6379> zadd zset-key 982 member0   #

(integer) 1                     #

redis 127.0.0.1:6379> zadd zset-key 982 member0   #

(integer) 0                     #

redis 127.0.0.1:6379> zrange zset-key 0 -1 withscores  # 获取有序集合包含的所有元素,

1) "member1"                      # 这些元素会按照分值进行排序,

2) "728"                        # Python客户端会将这些分值转换成浮点数。

3) "member0"                      #

4) "982"                        #

redis 127.0.0.1:6379> zrangebyscore zset-key 0 800 withscores  # 也可以根据分值来获取有序集合的其中一部分元素。

1) "member1"                          #

2) "728"                            #

redis 127.0.0.1:6379> zrem zset-key member1   # 在移除有序集合元素的时候,

(integer) 1                   # 命令会返回被移除元素的数量。

redis 127.0.0.1:6379> zrem zset-key member1   #

(integer) 0                   #

redis 127.0.0.1:6379> zrange zset-key 0 -1 withscores

1) "member0"

2) "982"

案例演示——文章投票平台

用户投票
# 准备好需要用到的常量。
ONE_WEEK_IN_SECONDS = 7 * 86400
VOTE_SCORE = 432
def article_vote(conn, user, article):
  # 计算文章的投票截止时间。
  cutoff = time.time() - ONE_WEEK_IN_SECONDS
  # 检查是否还可以对文章进行投票(虽然使用散列也可以获取文章的发布时间,但有序集合返回的文章发布时间为浮点数,可以不进行转换直接使用)。
  if conn.zscore('time:', article) < cutoff:
  	return
  # 从article:id标识符(identifier)里面取出文章的ID。
  article_id = article.partition(':')[-1]
  # 如果用户是第一次为这篇文章投票,那么增加这篇文章的投票数量和评分。
  if conn.sadd('voted:' + article_id, user):
	conn.zincrby('score:', article, VOTE_SCORE)
	conn.hincrby(article, 'votes', 1)
发表文章
def post_article(conn, user, title, link):
  # 生成一个新的文章ID。
  article_id = str(conn.incr('article:'))
  voted = 'voted:' + article_id
  # 将发布文章的用户添加到文章的已投票用户名单里面,
  # 然后将这个名单的过期时间设置为一周(第3章将对过期时间作更详细的介绍)。
  conn.sadd(voted, user)
  conn.expire(voted, ONE_WEEK_IN_SECONDS)
  now = time.time()
  article = 'article:' + article_id
  # 将文章信息存储到一个散列里面。
  conn.hmset(article, {
	'title': title,
	'link': link,
	'poster': user,
	'time': now,
	'votes': 1,
  })
  # 将文章添加到根据发布时间排序的有序集合和根据评分排序的有序集合里面。
  conn.zadd('score:', article, now + VOTE_SCORE)
  conn.zadd('time:', article, now)
  return article_i
读取文章
ARTICLES_PER_PAGE = 25
def get_articles(conn, page, order='score:'):
  # 设置获取文章的起始索引和结束索引。
  start = (page-1) * ARTICLES_PER_PAGE
  end = start + ARTICLES_PER_PAGE - 1
  # 获取多个文章ID。
  ids = conn.zrevrange(order, start, end)
  articles = []
  # 根据文章ID获取文章的详细信息。
  for id in ids:
	article_data = conn.hgetall(id)
	article_data['id'] = id
	articles.append(article_data)
  return articles
管理群组
def add_remove_groups(conn, article_id, to_add=[], to_remove=[]):
  # 构建存储文章信息的键名。
  article = 'article:' + article_id
  for group in to_add:
	# 将文章添加到它所属的群组里面。
	conn.sadd('group:' + group, article)
  for group in to_remove:
	# 从群组里面移除文章。
	conn.srem('group:' + group, article)
从群组里获取一整页文章
def get_group_articles(conn, group, page, order='score:'):
  # 为每个群组的每种排列顺序都创建一个键。
  key = order + group
  # 检查是否有已缓存的排序结果,如果没有的话就现在进行排序。
  if not conn.exists(key): 
	# 根据评分或者发布时间,对群组文章进行排序。
	conn.zinterstore(key,
		['group:' + group, order],
		aggregate='max',
	)
	# 让Redis在60秒钟之后自动删除这个有序集合。
	conn.expire(key, 60)
  # 调用之前定义的get_articles()函数来进行分页并获取文章数据。
  return get_articles(conn, page, key)
测试一下
class TestCh01(unittest.TestCase):
  def setUp(self):
     import redis
     self.conn = redis.Redis(db=15)
     
  def tearDown(self):
     del self.conn
     print
     print
     
  def test_article_functionality(self):
     conn = self.conn
     
     import pprint
     article_id = str(post_article(conn, 'username', 'A title', 'http://www.google.com'))
     print "We posted a new article with id:", article_id
     print
     
     self.assertTrue(article_id)
     print "Its HASH looks like:"
     r = conn.hgetall('article:' + article_id)
     print r
     print
     
     self.assertTrue(r)
     article_vote(conn, 'other_user', 'article:' + article_id)
     print "We voted for the article, it now has votes:",
     v = int(conn.hget('article:' + article_id, 'votes'))
     print v
     print
     
     self.assertTrue(v > 1)
     print "The currently highest-scoring articles are:"
     articles = get_articles(conn, 1)
     pprint.pprint(articles)
     print
     
     self.assertTrue(len(articles) >= 1)
     add_remove_groups(conn, article_id, ['new-group'])
     print "We added the article to a new group, other articles include:"
     articles = get_group_articles(conn, 'new-group', 1)
     pprint.pprint(articles)
     print
     
     self.assertTrue(len(articles) >= 1)
     to_del = (
       conn.keys('time:*') + conn.keys('voted:*') + conn.keys('score:*') + 
       conn.keys('article:*') + conn.keys('group:*')
     )
     
     if to_del:
       conn.delete(*to_del)

if __name__ == '__main__':
  unittest.main()

第二章:用redis构建web应用

登录和缓存

cookie类型优点缺点存储内容
签名cookie验证cookie的一切信息都存储在cookie里面,可以包含额外的信息,签名非常容易正确地签名很难,容易忘记对数据进行签名,或者忘记验证数据的签名,从而造成安全漏洞签名(服务器可以使用这个签名验证浏览器发送的信息是否经过未经改动),用户名,可能还有用户ID,最后一次登录时间以及网站觉得有用的其他信息。
令牌cookie添加信息非常容易,cookie的体积非常小,因此移动终端和速度较慢的客户端可以更快的发送请求需要在服务器中存储更多信息,如果使用关系型数据库那么载入和存储cookie的代价可能会很高一串随机字节作为令牌,服务器可以依据令牌在数据库中查找令牌的拥有者
检查登录token
def check_token(conn, token):
  return conn.hget('login:', token)  # 尝试获取并返回令牌对应的用户。
更新令牌
def update_token(conn, token, user, item=None):
  # 获取当前时间戳。
  timestamp = time.time()
  # 维持令牌与已登录用户之间的映射。
  conn.hset('login:', token, user)
  # 记录令牌最后一次出现的时间。
  conn.zadd('recent:', token, timestamp)
  if item:
	# 记录用户浏览过的商品。
	conn.zadd('viewed:' + token, item, timestamp)
	# 移除旧的记录,只保留用户最近浏览过的25个商品。
	conn.zremrangebyrank('viewed:' + token, 0, -26)
清理旧会话程序
QUIT = False
LIMIT = 10000000
# 如果清理函数正在删除某个用户的信息,而这个用户又在同一时间访问网站的话,就会产生竟态条件
# 会导致用户的信息被错误地删除
def clean_sessions(conn):
  while not QUIT:
	# 找出目前已有令牌的数量。
	size = conn.zcard('recent:')
	# 令牌数量未超过限制,休眠并在之后重新检查。
	if size <= LIMIT:
		time.sleep(1)
		continue
	# 获取需要移除的令牌ID。
	end_index = min(size - LIMIT, 100)
	tokens = conn.zrange('recent:', 0, end_index-1)
	# 为那些将要被删除的令牌构建键名。
	session_keys = []
	for token in tokens:
		session_keys.append('viewed:' + token)
	# 移除最旧的那些令牌。
	conn.delete(*session_keys)
	conn.hdel('login:', *tokens)
	conn.zrem('recent:', *tokens)

实现购物车

更新购物车
def add_to_cart(conn, session, item, count):
  if count <= 0:
	# 从购物车里面移除指定的商品。
	conn.hrem('cart:' + session, item) 
  else:
  # 将指定的商品添加到购物车。
  conn.hset('cart:' + session, item, count) 
清理旧会话和旧会话的购物车
def clean_full_sessions(conn):
  while not QUIT:
	size = conn.zcard('recent:')
	if size <= LIMIT:
		time.sleep(1)
		continue
	end_index = min(size - LIMIT, 100)
	sessions = conn.zrange('recent:', 0, end_index-1)
	session_keys = []
	for sess in sessions:
		session_keys.append('viewed:' + sess)
		# 新增加的这行代码用于删除旧会话对应用户的购物车。
		session_keys.append('cart:' + sess)  
	conn.delete(*session_keys)
	conn.hdel('login:', *sessions)
	conn.zrem('recent:', *sessions)

网页缓存

缓存页面
def cache_request(conn, request, callback):
  # 对于不能被缓存的请求,直接调用回调函数。
  if not can_cache(conn, request):
	return callback(request)
  # 将请求转换成一个简单的字符串键,方便之后进行查找。
  page_key = 'cache:' + hash_request(request) 
  # 尝试查找被缓存的页面。
  content = conn.get(page_key)
  if not content:
	# 如果页面还没有被缓存,那么生成页面。
	content = callback(request)
	# 将新生成的页面放到缓存里面。
	conn.setex(page_key, content, 300)
  # 返回页面。
  return content

数据行缓存

编写一个持续运行的守护进程函数,让这个函数将指定的数据行缓存到redis里,并不定期地对这些缓存进行更新。缓存函数会将数据行编码为json字典并存储在redis的字符串里面。其中数据列的名字会被映射为json字典的键而数据行的值则会被映射为json字典的值。

调度缓存和终止缓存
def schedule_row_cache(conn, row_id, delay):
  # 先设置数据行的延迟值。
  conn.zadd('delay:', row_id, delay) 
  # 立即缓存数据行。
  conn.zadd('schedule:', row_id, time.time()) 
进行缓存
def cache_rows(conn):
  while not QUIT:
	# 尝试获取下一个需要被缓存的数据行以及该行的调度时间戳,
	# 命令会返回一个包含零个或一个元组(tuple)的列表。
	next = conn.zrange('schedule:', 0, 0, withscores=True) 
	now = time.time()
	if not next or next[0][1] > now:
		# 暂时没有行需要被缓存,休眠50毫秒后重试。
		time.sleep(.05) 
		continue
	row_id = next[0][0]
	# 获取下一次调度前的延迟时间。
	delay = conn.zscore('delay:', row_id)
	if delay <= 0:
		# 不必再缓存这个行,将它从缓存中移除。
		conn.zrem('delay:', row_id) 
		conn.zrem('schedule:', row_id)
		conn.delete('inv:' + row_id)
		continue
	# 读取数据行。
	row = Inventory.get(row_id)
	# 更新调度时间并设置缓存值。
	conn.zadd('schedule:', row_id, now + delay)     
	conn.set('inv:' + row_id, json.dumps(row.to_dict())) 

网页分析

我们可以通过修改update_token函数来满足计算用户最经常浏览的商品的需求

def rescale_viewed(conn):
  while not QUIT:
  	# 删除所有排名在20 000名之后的商品。
  	conn.zremrangebyrank('viewed:', 20000, -1)
	# 将浏览次数降低为原来的一半
	conn.zinterstore('viewed:', {'viewed:': .5}) 
	# 5分钟之后再执行这个操作。
	time.sleep(300) 

我们同时更新一下缓存的策略,让它使用新的方法来判断页面是否需要被缓存

def can_cache(conn, request):
  # 尝试从页面里面取出商品ID。
  item_id = extract_item_id(request)
  # 检查这个页面能否被缓存以及这个页面是否为商品页面。
  if not item_id or is_dynamic(request):
  	return False
  # 取得商品的浏览次数排名。
  rank = conn.zrank('viewed:', item_id)
  # 根据商品的浏览次数排名来判断是否需要缓存这个页面。
  return rank is not None and rank < 10000 

测试一下

def extract_item_id(request):
    parsed = urlparse.urlparse(request)
    query = urlparse.parse_qs(parsed.query)
    return (query.get('item') or [None])[0]

def is_dynamic(request):
    parsed = urlparse.urlparse(request)
    query = urlparse.parse_qs(parsed.query)
    return '_' in query

def hash_request(request):
    return str(hash(request))

class Inventory(object):
    def __init__(self, id):
        self.id = id

    @classmethod
    def get(cls, id):
        return Inventory(id)

    def to_dict(self):
        return {'id':self.id, 'data':'data to cache...', 'cached':time.time()}

class TestCh02(unittest.TestCase):
    def setUp(self):
        import redis
        self.conn = redis.Redis(db=15)

    def tearDown(self):
        conn = self.conn
        to_del = (
            conn.keys('login:*') + conn.keys('recent:*') + conn.keys('viewed:*') +
            conn.keys('cart:*') + conn.keys('cache:*') + conn.keys('delay:*') + 
            conn.keys('schedule:*') + conn.keys('inv:*'))
        if to_del:
            self.conn.delete(*to_del)
        del self.conn
        global QUIT, LIMIT
        QUIT = False
        LIMIT = 10000000
        print
        print

    def test_login_cookies(self):
        conn = self.conn
        global LIMIT, QUIT
        token = str(uuid.uuid4())

        update_token(conn, token, 'username', 'itemX')
        print "We just logged-in/updated token:", token
        print "For user:", 'username'
        print

        print "What username do we get when we look-up that token?"
        r = check_token(conn, token)
        print r
        print
        self.assertTrue(r)


        print "Let's drop the maximum number of cookies to 0 to clean them out"
        print "We will start a thread to do the cleaning, while we stop it later"

        LIMIT = 0
        t = threading.Thread(target=clean_sessions, args=(conn,))
        t.setDaemon(1) # to make sure it dies if we ctrl+C quit
        t.start()
        time.sleep(1)
        QUIT = True
        time.sleep(2)
        if t.isAlive():
            raise Exception("The clean sessions thread is still alive?!?")

        s = conn.hlen('login:')
        print "The current number of sessions still available is:", s
        self.assertFalse(s)

    def test_shoppping_cart_cookies(self):
        conn = self.conn
        global LIMIT, QUIT
        token = str(uuid.uuid4())

        print "We'll refresh our session..."
        update_token(conn, token, 'username', 'itemX')
        print "And add an item to the shopping cart"
        add_to_cart(conn, token, "itemY", 3)
        r = conn.hgetall('cart:' + token)
        print "Our shopping cart currently has:", r
        print

        self.assertTrue(len(r) >= 1)

        print "Let's clean out our sessions and carts"
        LIMIT = 0
        t = threading.Thread(target=clean_full_sessions, args=(conn,))
        t.setDaemon(1) # to make sure it dies if we ctrl+C quit
        t.start()
        time.sleep(1)
        QUIT = True
        time.sleep(2)
        if t.isAlive():
            raise Exception("The clean sessions thread is still alive?!?")

        r = conn.hgetall('cart:' + token)
        print "Our shopping cart now contains:", r

        self.assertFalse(r)

    def test_cache_request(self):
        conn = self.conn
        token = str(uuid.uuid4())

        def callback(request):
            return "content for " + request

        update_token(conn, token, 'username', 'itemX')
        url = 'http://test.com/?item=itemX'
        print "We are going to cache a simple request against", url
        result = cache_request(conn, url, callback)
        print "We got initial content:", repr(result)
        print

        self.assertTrue(result)

        print "To test that we've cached the request, we'll pass a bad callback"
        result2 = cache_request(conn, url, None)
        print "We ended up getting the same response!", repr(result2)

        self.assertEquals(result, result2)

        self.assertFalse(can_cache(conn, 'http://test.com/'))
        self.assertFalse(can_cache(conn, 'http://test.com/?item=itemX&_=1234536'))

    def test_cache_rows(self):
        import pprint
        conn = self.conn
        global QUIT
        
        print "First, let's schedule caching of itemX every 5 seconds"
        schedule_row_cache(conn, 'itemX', 5)
        print "Our schedule looks like:"
        s = conn.zrange('schedule:', 0, -1, withscores=True)
        pprint.pprint(s)
        self.assertTrue(s)

        print "We'll start a caching thread that will cache the data..."
        t = threading.Thread(target=cache_rows, args=(conn,))
        t.setDaemon(1)
        t.start()

        time.sleep(1)
        print "Our cached data looks like:"
        r = conn.get('inv:itemX')
        print repr(r)
        self.assertTrue(r)
        print
        print "We'll check again in 5 seconds..."
        time.sleep(5)
        print "Notice that the data has changed..."
        r2 = conn.get('inv:itemX')
        print repr(r2)
        print
        self.assertTrue(r2)
        self.assertTrue(r != r2)

        print "Let's force un-caching"
        schedule_row_cache(conn, 'itemX', -1)
        time.sleep(1)
        r = conn.get('inv:itemX')
        print "The cache was cleared?", not r
        print
        self.assertFalse(r)

        QUIT = True
        time.sleep(2)
        if t.isAlive():
            raise Exception("The database caching thread is still alive?!?")

    # We aren't going to bother with the top 10k requests are cached, as
    # we already tested it as part of the cached requests test.

if __name__ == '__main__':
    unittest.main()

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值