django-docker-redis-tutorial 基本教學 📝
Redis 是 open source,也是 in-memory data structure store ( key-value ),常被使用在 database、cache 、
message broker,像是可以透過 cache 減輕 database 的壓力 ( redis 讀寫速度比一般的 database 快非常多 ),
而 message broker 可以用在像是 Celery 的應用( Celery 的應用可參考我之前寫的 django-celery-tutorial 以及
docker-django-celery-tutorial。
透過這篇文章,你將會學會
在開始教學前,建議大家可以先閱讀官方的 Redis Persistence ,
裡面詳細的介紹了 RDB persistence 以及 AOF persistence 的觀念,這兩個觀念很重要:+1:
請在命令提示字元 ( cmd ) 直接執行以下指令
docker run --name some-redis -p 6379:6379 -d redis redis-server --appendonly yes
如果要設定密碼
docker run --name some-redis -p 6379:6379 -d redis redis-server --appendonly yes --requirepass "changeme"
以上這段指令,比較需要特別解釋的就是 --appendonly
,當如果你沒有設定時,
假如今天斷電或是不小心意外終止 redis,可能會遺失當下的資料,如果我們設定了 Append-only file ( AOF ),
AOF 預設的 policy 是每秒寫入一次 ( 當然,還是有可能會遺失一秒的資料,但相對比 RDB ( Snapshotting,
因為預設的是存在硬碟上,binary file 為 dump.rdb,所以稱為 RDB ),AOF 比起 RDB 有更好的 Persistence 。
當選擇使用 AOF 時 ,如果重起 redis,會依照 AOF 去重新建立狀態。
更多詳細資料可參考 append-only-file。
或是直接使用 docker-compose.yml.
確認建立完成後,即可使用 redis-cli 開始玩 redis
docker exec -it <container name> redis-cli
如果你有設定密碼要加上 -a
docker exec -it <container name> redis-cli -a changeme
更多 redis 可參考 redis command 以及支援的 redis data-types。
127.0.0.1:6379> ping
PONG
set key
127.0.0.1:6379> set id twtrubiks
OK
get key
127.0.0.1:6379> get id
"twtrubiks"
exists key,更多可參考 EXISTS key
# if the key exists.
127.0.0.1:6379> exists id
(integer) 1
# if the key does not exist.
127.0.0.1:6379> exists not_exist
(integer) 0
設定 key 一個有效時間 ( Redis 常常拿來當做是 Cache ),更多可參考 EXPIRE key seconds
127.0.0.1:6379> set name twtrubiks
OK
127.0.0.1:6379> get name
"twtrubiks"
127.0.0.1:6379> expire name 10
(integer) 1
127.0.0.1:6379> get name
"twtrubiks"
# Wait for 10 seconds and try again
127.0.0.1:6379> get name
(nil)
刪除 key,更多可參考 DEL key
127.0.0.1:6379> set num 1
OK
127.0.0.1:6379> del num
(integer) 1
127.0.0.1:6379> get num
(nil)
一次刪除全部的 key
127.0.0.1:6379> flushall
OK
得到目前全部的 keys,更多可參考 KEYS pattern
keys *
TTL key,查看目前還剩多久時間會 timeout,
更多可參考 TTL key
127.0.0.1:6379> set name twtrubiks_ttl
OK
127.0.0.1:6379> expire name 10
(integer) 1
# Wait for 4 seconds and try again
127.0.0.1:6379> ttl name
(integer) 6
PERSIST key,將 key 從 volatile ( a key with an expire set ) 轉變成
persistent ( a key that will never expire as no timeout is associated ),
說白話一點,就是將 key 轉變成永遠不會過期 ( timeout ),更多可參考 PERSIST key
127.0.0.1:6379> set mykey hello
OK
127.0.0.1:6379> expire mykey 20
(integer) 1
# Wait for 5 seconds and try again
127.0.0.1:6379> ttl mykey
(integer) 15
127.0.0.1:6379> persist mykey
(integer) 1
127.0.0.1:6379> TTL mykey
(integer) -1
# -1 if the key exists but has no associated expire.
選擇資料庫,有 16 個資料庫 ( 0-15 ),預設是第 0 個資料庫,
如下方範例為切換到第一個資料庫,
127.0.0.1:6379> select 1
OK
127.0.0.1:6379[1]>
redis 非常適合投票這種使用情境,可參考以下範例
# 投給 a 一票
127.0.0.1:6379> zincrby vote 1 a
"1"
# 投給 b 兩票
127.0.0.1:6379> zincrby vote 2 b
"2"
# 投給 a 三票
127.0.0.1:6379> zincrby vote 3 a
"4"
# 查看 a 總投票數
127.0.0.1:6379> zscore vote a
"4"
# 得到 a 排名 ( 由高到低 )
127.0.0.1:6379> zrevrank vote a
(integer) 0
# 得到 b 排名 ( 由高到低 )
127.0.0.1:6379> zrevrank vote b
(integer) 1
# 得到前10名 ( 由高到低 )
127.0.0.1:6379> zrevrange vote 0 9
1) "a"
2) "b"
# 得到前10名以及對應的分數 ( 從高到低 )
127.0.0.1:6379> zrevrange vote 0 9 withscores
1) "a"
2) "4"
3) "b"
4) "2"
由於 redis command 很多,這邊不可能一一介紹,所以更詳細的可參考 commands😄
接下來和大家介紹 django-redis 這個套件,
我將簡單介紹他的使用方法,請先安裝套件
pip install django-redis
接著在 settings.py 中加入下方程式碼
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://localhost:6379/0",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
}
}
}
Django 預設的 session 是存放在 database 中,但這邊要將他修改成 redis ,
修改的方式很簡單,只需要將 SESSION_ENGINE 改成 django.contrib.sessions.backends.cache
即可,
Configure as session backend,在 settings.py 中加入下方程式碼
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_CACHE_ALIAS = "default"
詳細的 Session 參數介紹,可參考 Django 官網的 sessions 文件,
設定完成後,Session 將會儲存在 redis 中( 速度更快 ),
如果你不了解 Session ,可以參考我之前寫的這篇 Session,
Session 存在 redis 中的範例,後面我會再介紹給各位。
有時候我們可能需要 access 原生的 Redis 功能,所以這時候就需要採用下面的方式使用 redis,
>>> from django_redis import get_redis_connection
>>> con = get_redis_connection("default")
>>> con
<redis.client.StrictRedis object at 0x2dc4510>
官方文件可參考 The low-level cache API,
直接使用 Python Console 操作以下指令,
set(key, value, timeout)
>>> from django.core.cache import cache
>>> cache.set('my_key', 'hello, world!')
True
>>> cache.get('my_key')
'hello, world!'
set(key, value, timeout)
and get(key)
timeout 如果沒設定或是設定為 None 時,資料將為 forever
>>> from django.core.cache import cache
>>> cache.set('my_key', 'hello, world!')
True
>>> cache.get('my_key')
'hello, world!'
設定 timeout 為 10 秒
>>> from django.core.cache import cache
>>> cache.set('key_test', 'hello, world test !',10)
True
## if key_test has expired ( not exist ) , show has expired
>>> cache.get('key_test', 'has expired')
'hello, world test !'
## Wait 10 seconds for 'my_key' to expire...
>>> cache.get('key_test', 'has expired')
'has expired'
add()
如果這個 key 不存在,就會設定指定的 key,如果 key 已經存在,則不會更新既有的 key 值
>>> cache.set('my_key', 'hello, world!')
True
>>> cache.get('my_key')
'hello, world!'
>>> cache.add('my_key','demo')
>>> cache.get('my_key')
'hello, world!'
>>> cache.add('my_key_2','test2')
True
get_or_set()
可以使用這個來取得 ( 設定 ) key 值 , 假如這個 key 不存在 ,就設定 key 值 ,如果存在就將 key 值顯示出來
>>> cache.get('my_new_key')
>>> cache.get_or_set('my_new_key', 'my new value')
'my new value'
set_many()
and get_many()
>>> cache.set_many({'a': 1, 'b': 2, 'c': 3})
>>> cache.get_many(['a', 'b', 'c'])
OrderedDict([('a', 1), ('b', 2), ('c', 3)])
delete()
and delete_many()
>>> cache.delete('a')
1
>>> cache.delete_many(['a', 'b', 'c'])
2
clear()
>>> cache.clear()
incr()
and decr()
>>> cache.set('num', 1)
True
>>> cache.incr('num')
2
>>> cache.incr('num', 10)
12
>>> cache.decr('num')
11
cache.decr('num',2)
9
# A ValueError will be raised if you attempt to increment or decrement a nonexistent cache key
cache.get('test')
cache.decr('test',2)
>>> ValueError: Key ':1:tst' not found
incr_version()
and decr_version()
# default version =1
>>> cache.set('my_key', 'hello world!')
True
>>> cache.get('my_key',version=1)
'hello world!'
>>> cache.get('my_key',version=2)
# incr_version
>>> cache.incr_version('my_key')
2
>>> cache.get('my_key',version=2)
'hello world!'
>>> cache.get('my_key',version=1)
>>> cache.set('my_key', 'test', version=1)
True
>>> cache.get('my_key',version=1)
'test'
>>> cache.get('my_key',version=2)
'hello world!'
接著可以使用 redis-cli 觀看,
127.0.0.1:6379> keys *
1) ":1:my_key"
2) ":2:my_key"
有沒有發現一件事情,我們明明設定的是 cache.set('my_key', 'test', version=1)
,
但為什麼透過 django 設定的 key 都會變成 ":1:my_key"
這樣的格式呢 ?
原因是因為 django cache 本身的機制,
def make_key(
self, key: Any, version: Optional[Any] = None, prefix: Optional[str] = None
) -> CacheKey:
if isinstance(key, CacheKey):
return key
if prefix is None:
prefix = self._backend.key_prefix
if version is None:
version = self._backend.version
return CacheKey(self._backend.key_func(key, prefix, version))
django 使用 make_key 建立新的 key ,原始的 key 在 _backend.key_func
裡。
def default_key_func(key, key_prefix, version):
"""
Default function to generate keys.
Construct the key used by all other methods. By default, prepend
the `key_prefix`. KEY_FUNCTION can be used to specify an alternate
function with custom key making behavior.
"""
return "%s:%s:%s" % (key_prefix, version, key)
def get_key_func(key_func):
"""
Function to decide which key function to use.
Default to ``default_key_func``.
"""
if key_func is not None:
if callable(key_func):
return key_func
else:
return import_string(key_func)
return default_key_func
class BaseCache:
_missing_key = object()
def __init__(self, params):
timeout = params.get("timeout", params.get("TIMEOUT", 300))
if timeout is not None:
try:
timeout = int(timeout)
except (ValueError, TypeError):
timeout = 300
self.default_timeout = timeout
options = params.get("OPTIONS", {})
max_entries = params.get("max_entries", options.get("MAX_ENTRIES", 300))
try:
self._max_entries = int(max_entries)
except (ValueError, TypeError):
self._max_entries = 300
cull_frequency = params.get("cull_frequency", options.get("CULL_FREQUENCY", 3))
try:
self._cull_frequency = int(cull_frequency)
except (ValueError, TypeError):
self._cull_frequency = 3
self.key_prefix = params.get("KEY_PREFIX", "")
self.version = params.get("VERSION", 1)
self.key_func = get_key_func(params.get("KEY_FUNCTION"))
這也就是為什麼透過 django 設定的 key 都會變成 %s:%s:%s
這樣的格式了。
redis 可以應用的場合真的非常的多,這次的 demo 將使用到以下情境 ( 其他的情境大家可以再自行 google 了解 ),
- 統計頁面點擊數
當需要記錄頁面的瀏覽次數( 或點擊數 )時,就非常適合使用 redis,為什麼不使用 db 呢 ?
因為假如有非常大量的人瀏覽( 或點擊 )網頁時,可能會導致 db 的鎖互搶,影響到效能。
關於鎖這部份,可以稍微參考一下我之前寫的 django-transactions-tutorial,裡面有稍微
提到部分的概念。
images/views.py 中片段程式碼
cache.get_or_set('click', 0, timeout=None)
total_views = cache.incr('click')
這段程式碼相當簡單,當你瀏覽到這個頁面時,就將 click
這個 key 加一,
然後就可以統計出目前有多少人瀏覽過你的頁面 ( 聰明的你現在一定想到,
那我就一直瘋狂 F5 不就可以一直刷瀏覽數量了嗎 ?沒錯,但這個問題大家
可以自行想想,這邊只是帶給大家簡單的概念 )。
- 排行榜
前面介紹 redis 適合投票這種情境,當然,也適合排行榜這種使用情境,
images/views.py 中片段程式碼
def index(request):
......
rank = con.zrevrange(name='images', start=0, end=9, withscores=True, score_cast_func=int)
rank_seq = [
{"url": str(r[0], 'utf-8'),
"value": r[1]}
for r in rank
]
return render(request, 'images/index.html', {
'images': images_seq,
'total_views': total_views,
'ranks': rank_seq,
})
def detail(request, image_id):
image = get_object_or_404(Image, id=image_id)
total_views = con.zincrby(name='images', amount=1 ,value=image.url)
return render(request,
'images/detail.html', {
'image': image,
'total_views': int(total_views)
})
在 detail
中,我們將圖片的 url 存到 images 這個 key 值中,並且給他加一,
在 index
中,透過 zrevrange
這個方法統計目前圖片被瀏覽次數的排名。
- Session Cache
為了方便介紹,這邊直接使用登入 admin 後台來觀察 session,直接瀏覽 http://127.0.0.1:8000/admin/ ,
預設的帳號密碼為 ( twtrubiks / password123 ),
登入後你會發現,你的 redis 多了 session 的 key 值( 而 database 中沒有增加 ),如下圖
redis 中多了 session 的 key
如果沒特別另外設定,django 預設是存放在 database ( django_session 表格 ),
這邊是空的很正常,因為我們已經設定 redis 了,
- 減輕 database 壓力
這邊和大家簡單說明如何減輕 database 的壓力:satisfied:
可參考 musics/views.py
# Create your views here.
class MusicViewSet(viewsets.ModelViewSet):
queryset = Music.objects.all()
serializer_class = MusicSerializer
permission_classes = (IsAuthenticated,)
parser_classes = (JSONParser,)
def list(self, request, **kwargs):
if self.request.version == '1.0':
if 'musics' in cache:
# from cache get musics
musics = cache.get('musics')
else:
musics = Music.objects.all()
serializer = MusicSerializer(musics, many=True)
musics = serializer.data
# store data to cache
cache.set('musics', musics, timeout=None)
else:
musics = Music.objects.all()
serializer = MusicSerializer(musics, many=True)
musics = serializer.data
return Response(musics, status=status.HTTP_200_OK)
假設 database 的 music 這張表格中有一萬筆資料,然後如果每個人每次發送 request 過來都要重新撈這一萬筆資料,
會對資料庫造成很大的壓力,也沒什麼效率,這時候就可以透過 redis 來幫助我們。
程式其實很簡單,當 redis 中沒有 musics 這個 key 的時候,我就去資料庫撈,然後將撈到的資料存進 redis 中,
這樣當下一次我還需要時,就可以直接從 redis 中取得資料( 不需要再重新從資料庫中撈資料 )。
你可能會問我為什麼要用 self.request.version
,原因是等等我們模擬簡單壓力測試要使用的:grinning:
Django 的 version 使用方法可參考我之前寫的 django-rest-framework-tutorial#versioning。
以下是兩個的比較,
第一次執行會從資料庫撈一萬筆的資料
第二次開始,都會從 redis 撈一萬筆的資料 ( 速度快很多 )
從秒數來看,速度至少快了 16 倍,你可能會說,其實還好阿:confused:
不過,假設今天有 100 個人呢? 相信就非常有感了:smirk:
讓我們透過數據說話,使用 loadtest 來簡單模擬,
先安裝 loadtest
npm install --location=global loadtest
使用方法
loadtest [-n requests] [-c concurrency] [-k] URL
先來測試 沒有 redis 的情況 ( 50 個 request )
loadtest -H "Authorization: Basic dHd0cnViaWtzOnBhc3N3b3JkMTIz" -n 50 -k http://127.0.0.1:8000/api/musics/
如下圖,慢到我不想等他跑完 😓 ( 還在 38%)
再來測試 有 redis 的情況 ( 50 個 request )
loadtest -H "Authorization: Basic dHd0cnViaWtzOnBhc3N3b3JkMTIz" -H "Accept: application/json;version=1.0" -n 50 -k http://127.0.0.1:8000/api/musics/
如下圖,很快就跑完了,而且花最久的時間是 229ms
可以發現,有 redis 的情況下,效能好很多:heart_eyes:
後面我有再補充另一段 code, musics/views.py
def list_lock(self, request, **kwargs):
if self.request.version == '1.0':
if 'musics' in cache:
# from cache get musics
musics = cache.get('musics')
else:
if con.set("my_key", "secret", nx=True, px=1000):
musics = Music.objects.all()
serializer = MusicSerializer(musics, many=True)
musics = serializer.data
cache.set('musics', musics, timeout=None)
print('store data to cache')
else:
print("pending")
while 1:
time.sleep(0.5)
print('sleep')
if 'musics' in cache:
musics = cache.get('musics')
print('break')
break
......
return Response(musics, status=status.HTTP_200_OK)
主要是透過 redis 的鎖, 確保當下只有一個 request 可以進 db 拿資料,
剩下的 request 全部 pending, 直到 redis 有資料.
請搭配以下測試
loadtest -H "Authorization: Basic dHd0cnViaWtzOjEyMw==" -H "Accept: application/json;version=1.0" -n 15 -c 15 -k http://127.0.0.1:8000/api/musics/
(要多測幾次, 確保有顯示 "pending" )
相信這時候大家又會問,不過這樣子 redis 裡面的資料有很高的機會是舊的,沒錯,所以更好的方法,
可以搭配 Celery 設定排成 ( Celery 可以參考我之前寫的 docker-django-celery-tutorial ),一天或
一段時間更新一次 ( 將 db 裡的資料讀出來寫進 redis 中 ),這樣就可以確保 redis 裡面的資料盡量和 db 裡面一樣。
請直接瀏覽 http://127.0.0.1:8000/images/,你會發現有一個 View Count : 1
如果你一直重新整理 ( 狂按 F5 ),會發現 View Count 一直增加
可以點選自己喜歡的圖片進去觀看,也會有屬於這張照片的 View Count
如果你一直重新整理 ( 狂按 F5 ),也會發現 View Count 一直增加
回到首頁 ( http://127.0.0.1:8000/images/ ),你會發現多了一個排行榜
當瀏覽越多圖片,排行榜就會向下面這樣 (也顯示每張圖片的 View Count )
這次帶大家了解 redis 的一些基礎應用,相信大家一定覺得 redis 真的很有趣,然後 redis 可以做的事情絕對不只
這次教學所帶給大家的,他可以做的應用真非常的多,本教學只是帶給大家一個基礎,了解到底什麼是 redis,
然後可以做怎麼樣的應用,希望這教學對想了解 redis 多少有幫助,謝謝大家:relaxed:
- Python 3.8
文章都是我自己研究內化後原創,如果有幫助到您,也想鼓勵我的話,歡迎請我喝一杯咖啡:laughing:
MIT licens