首先,簡單介紹:
Redis是一個基於內存的鍵值對存儲系統,常用作數據庫、緩存和消息代理。
支持:字符串,字典,列表,集合,有序集合,位圖(bitmaps),地理位置,HyperLogLog等多種數據結構。
支持事務、分片、主從復之、支持RDB(內存數據保存的文件)和AOF(類似於MySQL的binlog)兩種持久化方式。3.0加入訂閱分發、Lua腳本、集群等特性。
命令參考:
http://doc.redisfans.com
中文官網:
http://www.redis.net.cn
安裝(都大同小異,安裝前建議用先用search搜索一下):
ubuntu:sudo apt-get install redis-server -yq
MAC: sudo port install redis-server
CentOS: yum install redis-server
安裝之后已經啟動,可以用redis-cli驗證,也可以ps -ef | grep redis
安裝redis的Python包:
pip install redis
安裝之后可以這樣基本使用:
In [15]: import redis
In [16]: conn = redis.Redis()
In [17]: conn.rpush('a','2')
Out[17]: 1
In [18]: conn = redis.StrictRedis(host='localhost', port=6379, db=0)
In [19]: conn.lrange('a',0,-1)
Out[19]: [b'2']
Python調用redis的函數和redis本身的命令都差不多,一般參數個數和類型都相同,可以直接查看Redis命令參考,再去Python的redis模塊的client.py中去找
OK,接下來看看Python都可以用Redis來做些什么吧,四種常用使用場景:
1.取最新N個數據操作:
該場景應用於比如每天更新10萬條數據,均寫入關系型數據庫(如MySQL),可以在Redis中保存最新的1萬條數據,方便讀取和查詢,極大提高讀取查詢效率。
使用Redis的List數據結構,用到常用命
令LPUSH,LTRIM和
LRANGE即可,用Flak來呈現,用SQLAlchemy來連接MySQL:
服務端代碼:
# coding=utf-8 import json from datetime import datetime from flask import Flask, request, jsonify from flask_sqlalchemy import SQLAlchemy import redis import pymysql pymysql.install_as_MySQLdb() app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://web:web@localhost:3306/r' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False db = SQLAlchemy(app) r = redis.StrictRedis(host='localhost', port=6379, db=0) MAX_FILE_COUNT = 50 #數據模型,有id,name和uploadtime三個字段 class PasteFile(db.Model): __tablename__ = 'files' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(5000), nullable=False) uploadtime = db.Column(db.DateTime, nullable=False) def __init__(self, name='', uploadtime=None): self.uploadtime = datetime.now() if uploadtime is None else uploadtime self.name = name db.create_all() #該函數對POST傳入的id和name,寫MySQL數據庫,LPUSH到Redis中,並LTRIM裁剪保留MAX_FILE_COUNT個 @app.route('/upload', methods=['POST']) def upload(): name = request.form.get('name') pastefile = PasteFile(name) db.session.add(pastefile) db.session.commit() r.lpush('latest.files', pastefile.id) r.ltrim('latest.files', 0, MAX_FILE_COUNT - 1) return jsonify({'r': 0}) #該視圖函數截取start-limit個數據,通過json呈現在街面上,默認0-20就是最新插入的20條 @app.route('/lastest_files') def get_lastest_files(): start = request.args.get('start', default=0, type=int) limit = request.args.get('limit', default=20, type=int) ids = r.lrange('latest.files', start, start + limit - 1) files = PasteFile.query.filter(PasteFile.id.in_(ids)).all() return json.dumps([{'id': f.id, 'filename': f.name} for f in files]) if __name__ == '__main__': app.run(host='0.0.0.0', port=9000, debug=True)
隨機生成100條數據:
from lastest_files import app, PasteFile, r import time ,random, string #隨機生成100條name插入MySQl表,id自增 with app.test_client() as client: for _ in range(1,101): data = ''.join(random.sample(string.ascii_letters,10))+'_'+str(_) print ('input data: ',data) client.post('/upload',data={'name':data}) time.sleep(0.5)
測試結果:
2.取TOP N操作(排行榜應用)
該場景用於游戲或者需要分數排名的地方
主要利用Redis的有序集合(SortedSet)其中:
score值遞減(從大到小)的次序排列。
用到Redis有序集合的:ZADD,ZREVRANGE,ZCOUNT,ZREVRANGEBYSCORE命令
測試代碼:
# coding=utf-8 import string import random import redis r = redis.StrictRedis(host='localhost', port=6379, db=0) GAME_BOARD_KEY = 'game.board' for _ in range(1000): score = round((random.random() * 100), 2) user_id = ''.join(random.sample(string.ascii_letters, 6)) #隨機生成1000個用戶,每個用戶具有得分和用戶名字,插入Redis的有序集合中 r.zadd(GAME_BOARD_KEY, score, user_id) # 隨機獲得一個用戶和他的得分 user_id, score = r.zrevrange(GAME_BOARD_KEY, 0, -1, withscores=True)[random.randint(0, 200)] print (user_id, score) #用有序集合的ZCOUNT獲取0-100的個數也就是所有人的數量,獲取0-score分數段的人數,也就是這個用戶分數超過了多少人 board_count = r.zcount(GAME_BOARD_KEY, 0, 100) current_count = r.zcount(GAME_BOARD_KEY, 0, score) print (current_count, board_count) print ('TOP 10') print ('-' * 20) #用有序集合的ZREVRANGEBYSCORE返回指定區間的元素 #ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count] for user_id, score in r.zrevrangebyscore(GAME_BOARD_KEY, 100, 0, start=0, num=10, withscores=True): print (user_id, score)
測試結果:
b'mgOvfl' 83.04 811 1000 TOP 10 -------------------- b'rbhXNd' 99.91 b'KJFELh' 99.88 b'cyjNrJ' 99.81 b'RXohkG' 99.64 b'SMVFbu' 99.51 b'FMBEgz' 99.5 b'ajxhdp' 99.45 b'QuMSpL' 99.33 b'IFYCOs' 99.31 b'VyWnYC' 98.74
3.計數器:
Redis非常適合用來做計數器:
沒什么好解釋的,就是
INCR,DECR,INCRBY
4.實時統計:
Redis的位圖提供了二進制操作,非常適合存儲布爾類型的值,常見場景就是記錄用戶登陸狀態。
該場景用二進制的方式表示用戶是否登錄,比如說有10個用戶,則0000000000表示無人登錄,0010010001表示第3個、第6個、第10個用戶登錄過,即是活躍的。
用到Redis字符串(String)結構中的:
BITCOUNT,GETBIT,BITOP命令
對本月每天的用戶登錄情況進行統計,會針對每天生成key,例如今天的:account:active:2016:11:23,也會生成月的key:account:active:2016:11和年的key:key:account:active:2016
每個key中的字符串長度就是人數(可能有的key的str沒有那么長,那是因為最后一個bit沒有set成1,不過沒有就相當於是0)
測試代碼:
# coding=utf-8 import time import random from datetime import datetime import redis r = redis.StrictRedis(host='localhost', port=6379, db=0) ACCOUNT_ACTIVE_KEY = 'account:active' r.flushall() # r.delete(ACCOUNT_ACTIVE_KEY) now = datetime.utcnow() def record_active(account_id, t=None): #第一次t自己生成,后面t接受傳入的年月日 if t is None: t = datetime.utcnow() #Redis事務開始 p = r.pipeline() key = ACCOUNT_ACTIVE_KEY #組合了年月日三種鍵值,同時將三個鍵值對應字符串的account_id位置為1 #符合邏輯:該人在這一天登陸,肯定也在當前月登陸,也在當年登陸 for arg in ('year', 'month', 'day'): key = '{}:{}'.format(key, getattr(t, arg)) p.setbit(key, account_id, 1) #Redis事務提交,真正執行 p.execute() def gen_records(max_days, population, k): #循環每天的情況,從1-max_days天 for day in range(1, max_days): time_ = datetime(now.year, now.month, day) #每天隨機生成k個數字,表示k個人活躍 accounts = random.sample(range(population), k) #將這k個人對應在當天的字符串中修改,對應位置的bit置為1,表明這個天他有登陸過 for account_id in accounts: record_active(account_id, time_) #查看記錄100萬數據中隨機選擇10萬活躍用戶時的內存占用 def calc_memory(): r.flushall() #執行前先看當前的內存占用 print ('USED_MEMORY: {}'.format(r.info()['used_memory_human'])) start = time.time() #100萬種選擇10萬,20天 gen_records(21, 1000000, 100000) #記錄話費時間 print ('COST: {}'.format(time.time() - start)) #添加記錄后的內存占用 print ('USED_MEMORY: {}'.format(r.info()['used_memory_human'])) gen_records(29, 10000, 2000) #這個月總的活躍用戶數,直接查詢記錄月的key:bitcount "account:active:2016:11" print (r.bitcount('{}:{}:{}'.format(ACCOUNT_ACTIVE_KEY, now.year, now.month))) #今天的活躍用戶數:bitcount "account:active:2016:11:23" print (r.bitcount('{}:{}:{}:{}'.format(ACCOUNT_ACTIVE_KEY, now.year, now.month, now.day))) #隨機找一個account_id為1200的用戶,查看他是否登陸過:getbit "account:active:2016:11" 1200 account_id = 1200 print (r.getbit('{}:{}:{}'.format(ACCOUNT_ACTIVE_KEY, now.year, now.month), account_id)) #getbit "account:active:2016:11" 10001 print (r.getbit('{}:{}:{}'.format(ACCOUNT_ACTIVE_KEY, now.year, now.month), 10001)) #獲取當月1號和2號的建 keys = ['{}:{}:{}:{}'.format(ACCOUNT_ACTIVE_KEY, now.year, now.month, day) for day in range(1, 3)] #獲取1號和2號的活躍的用戶總數 r.bitop('or', 'destkey:or', *keys) print (r.bitcount('destkey:or')) #獲取在1號和2號都活躍的用戶數 r.bitop('and', 'destkey:and', *keys) print (r.bitcount('destkey:and'))
測試結果:
9974
2000
1
0
3593
407
對應的Redis操作結果:
127.0.0.1:6379> bitcount "account:active:2016:11" (integer) 9974 127.0.0.1:6379> bitcount "account:active:2016:11:23" (integer) 2000 127.0.0.1:6379> getbit "account:active:2016:11" 1200 (integer) 1 127.0.0.1:6379> getbit "account:active:2016:11" 10001 (integer) 0 127.0.0.1:6379> bitop 'or' 'destkey:or' "account:active:2016:11:1","account:active:2016:11:2" Invalid argument(s) 127.0.0.1:6379> bitop 'or' 'destkey:or' "account:active:2016:11:1" "account:active:2016:11:2" (integer) 1250 127.0.0.1:6379> bitcount destkey:or (integer) 3593 127.0.0.1:6379> bitop 'and' 'destkey:and' "account:active:2016:11:1" "account:active:2016:11:2" (integer) 1250 127.0.0.1:6379> bitcount destkey:and (integer) 407
Python代碼中輸出與Redis操作對應關系:
最后,用函數calc_memory()單獨測試了一下性能(運行比較慢):
USED_MEMORY: 1.05M COST: 427.4658901691437 USED_MEMORY: 5.82M
所以說明存儲200萬活躍用戶技術,總計20天*100萬人,相當於2000萬人,采用此方案,只需要5M多空間。
以上代碼內容均來自董偉明老師的《Python Web開發實踐》,自己實踐了一遍,這里主要用來學習、記錄和分享。
