PythonでRedisを使用する方法

PythonでRedisを使用する方法

このチュートリアルでは、RedisでPythonを使用する方法を学習します(https://redis.io/topics/faq[RED-iss]、またはhttps://groups.google.com/forum/#!topicと発音します)/redis-db/MtwjZC5gCeE [REE-diss]またはhttps://en.wikipedia.org/wiki/Talk:Redis#Pronounciation[Red-DEES]、尋ねる人に応じて)、これは超高速のインメモリですAからZのすべてに使用できるキー値ストア。 以下は、データベースに関する一般的な本である_Seven Weeks in Seven Weeks_がRedisについて述べていることです。

_ _ 簡単に使用できるわけではありません。それは喜びです。 APIがプログラマ向けのUXである場合、RedisはMac Cubeと並んで現代美術館にいるはずです。

そして、スピードに関して言えば、Redisは他の追随を許しません。 読み取りは高速で、書き込みはさらに高速で、一部のベンチマークでは毎秒100,000件以上の「+ SET +」操作を処理します。 (https://realpython.com/asins/1680502530/[ソース]) _ _

興味がありますか? このチュートリアルは、Redisの経験がまったくないかほとんどないPythonプログラマ向けに作成されています。 2つのツールに同時に取り組み、Redis自体とPythonクライアントライブラリの1つhttps://github.com/andymccurdy/redis-py [+ redis-py +]の両方を紹介します。

+ redis-py +(これは単なる `+ redis +`としてインポートします)は、Redisの多くのPythonクライアントの1つですが、https://redis.io/clients#python ["現在Pythonに移行する方法」]は、Redis開発者自身によって作成されました。 PythonからRedisコマンドを呼び出して、見慣れたPythonオブジェクトを返すことができます。

このチュートリアルでは、以下について説明します

  • ソースからRedisをインストールし、結果のバイナリの目的を理解する

  • 構文、プロトコル、デザインなど、Redisの1バイトサイズのスライスを学習する

  • `+ redis-py +`をマスターすると同時に、Redisのプロトコルの実装方法を垣間見る

  • Amazon ElastiCache Redisサーバーインスタンスのセットアップと通信

*無料ボーナス:*リンク:[Python Tricks:The Book]の章にアクセスするには、ここをクリックして、Pythonのベストプラクティスを簡単な例とともに示します。すぐに適用して、より美しい+ Pythonコードを記述できます。

ソースからのRedisのインストール

私のgreat祖父が言ったように、ソースからインストールすることほど不利な点はありません。 このセクションでは、Redisのダウンロード、作成、インストールについて説明します。 私はこれが少し傷つけないことを約束します!

:このセクションは、Mac OS XまたはLinuxでのインストールを対象としています。 Windowsを使用している場合、WindowsサービスとしてインストールできるRedisのMicrosoft forkがあります。 プログラムとしてのRedisはLinuxボックスで最も快適に動作し、Windowsでのセットアップと使用は難しいかもしれないと言うだけで十分です。

まず、Redisソースコードをtarballとしてダウンロードします。

$ redisurl="http://download.redis.io/redis-stable.tar.gz"
$ curl -s -o redis-stable.tar.gz $redisurl

次に、「+ root 」に切り替えて、アーカイブのソースコードを「/usr/local/lib/+」に抽出します。

$ sudo su root
$ mkdir -p/usr/local/lib/
$ chmod a+w/usr/local/lib/
$ tar -C/usr/local/lib/-xzf redis-stable.tar.gz

オプションで、アーカイブ自体を削除できるようになりました。

$ rm redis-stable.tar.gz

これにより、ソースコードリポジトリが +/usr/local/lib/redis-stable/+`に残ります。 RedisはCで記述されているため、https://www.gnu.org/software/make/[+ make +`]ユーティリティを使用してコンパイル、リンク、インストールする必要があります。

$ cd/usr/local/lib/redis-stable/
$ make && make install

`+ make install +`を使用すると、2つのアクションが実行されます。

  1. 最初の `+ make +`コマンドはソースコードをコンパイルしてリンクします。

  2. `+ make install `部分はバイナリを取得し、それらを `/usr/local/bin/`にコピーして、どこからでも実行できるようにします( `/usr/local/bin/+`が ` + PATH + `)。

ここまでの手順はすべて次のとおりです。

$ redisurl="http://download.redis.io/redis-stable.tar.gz"
$ curl -s -o redis-stable.tar.gz $redisurl
$ sudo su root
$ mkdir -p/usr/local/lib/
$ chmod a+w/usr/local/lib/
$ tar -C/usr/local/lib/-xzf redis-stable.tar.gz
$ rm redis-stable.tar.gz
$ cd/usr/local/lib/redis-stable/
$ make && make install

この時点で、Redisが `+ PATH +`にあることを確認し、バージョンを確認してください:

$ redis-cli --version
redis-cli 5.0.3

シェルで「+ redis-cli 」が見つからない場合は、「/usr/local/bin/」が「 PATH +」環境変数にあることを確認し、ない場合は追加します。

`+ redis-cli `に加えて、 ` make install `は実際には少数の異なる実行可能ファイル(および1つのシンボリックリンク)を `/usr/local/bin/+`に配置します。

$ # A snapshot of executables that come bundled with Redis
$ ls -hFG/usr/local/bin/redis-* | sort
/usr/local/bin/redis-benchmark*
/usr/local/bin/redis-check-aof*
/usr/local/bin/redis-check-rdb*
/usr/local/bin/redis-cli*
/usr/local/bin/[email protected]
/usr/local/bin/redis-server*

これらはすべて用途がありますが、おそらく最も重要な2つは、「+ redis-cli 」と「 redis-server +」です。 しかし、その前に、いくつかのベースライン構成を適切にセットアップします。

Redisの構成

Redisは高度に設定可能です。 そのまま使用できますが、データベースの永続性と基本的なセキュリティに関連する基本的な設定オプションを少し設定してみましょう。

$ sudo su root
$ mkdir -p/etc/redis/
$ touch/etc/redis/6379.conf

ここで、次を「+/etc/redis/6379.conf +」に書き込みます。 これらのほとんどがチュートリアル全体で徐々に意味することを説明します。

#/etc/redis/6379.conf

port              6379
daemonize         yes
save              60 1
bind              127.0.0.1
tcp-keepalive     300
dbfilename        dump.rdb
dir               ./
rdbcompression    yes

Redisの設定は自己文書化されており、http://download.redis.io/redis-stable/redis.conf [サンプル `+ redis.conf +`ファイル]が読みやすいようにRedisソースにあります。 実稼働システムでRedisを使用している場合は、すべての注意散漫をブロックし、このサンプルファイルを完全に読んでRedisの詳細を理解し、セットアップを微調整するのに時間がかかります。

Redisのドキュメントの一部を含む一部のチュートリアルでは、http://download.redis.io/redis-stable/utils/install_server.sh [+ redis/utilsにあるシェルスクリプト + install_server.sh + `を実行することも推奨されます。/install_server.sh + `]。 上記のより包括的な代替手段としてこれを実行することをぜひ歓迎しますが、「+ install_server.sh +」に関するいくつかの細かい点に注意してください。

  • Mac OS Xでは動作しません。DebianとUbuntu Linuxのみです。

  • 設定オプションの完全なセットを `+/etc/redis/6379.conf +`に注入します。

  • System V https://bash.cyberciti.biz/guide//etc/init.d [+ init + script]を `/etc/init.d/redis_6379 +`に書き込みます。これにより、 ` sudoサービスredis_6379 start + `。

Redisクイックスタートガイドには、https://redis.io/topics/quickstart#installing-redis-more-properly [より適切なRedisセットアップ]のセクションも含まれていますが、上記の構成オプションは、このチュートリアルと始めました。

*セキュリティに関する注意:*数年前、Redisの作成者は、構成が設定されていない場合、Redisの以前のバージョンのセキュリティの脆弱性を指摘しました。 Redis 3.2(2019年3月現在のバージョン5.0.3)は、この侵入を防ぐための手順を実行し、デフォルトで `+ protected-mode `オプションを ` yes +`に設定しました。

Redisがローカルホストインターフェースからのみ接続をリッスンできるように `+ bind 127.0.0.1 `を明示的に設定しますが、実際の本番サーバーではこのホワイトリストを拡張する必要があります。 ` protected-mode `のポイントは、 ` bind +`オプションの下で何も指定しない場合、このローカルホストへのバインド動作を模倣するセーフガードとしてです。

これを二乗すると、Redis自体の使用を掘り下げることができます。

Redisに10分ほど

このセクションでは、Redisの危険性を十分に把握し、その設計と基本的な使用法を概説します。

入門

Redisには*クライアントサーバーアーキテクチャ*があり、*要求-応答モデル*を使用します。 これは、ユーザー(クライアント)がデフォルトでポート6379でTCP接続を介してRedisサーバーに接続することを意味します。 何らかのアクション(何らかの形式の読み取り、書き込み、取得、設定、または更新など)を要求し、サーバーが応答を返します。

同じサーバーと通信する多くのクライアントが存在する可能性があります。これは、Redisまたはクライアントサーバーアプリケーションのすべてです。 各クライアントは、サーバーの応答を待機しているソケットで(通常はブロックして)読み取りを行います。

`+ redis-cli `の ` cli `は *command line interface* を表し、 ` redis-server `の ` server `はサーバーを実行するためのものです。 コマンドラインで ` python `を実行するのと同じ方法で、 ` redis-cli +`を実行して、シェルから直接クライアントコマンドを実行できるインタラクティブなREPL(Read Eval Print Loop)にジャンプできます。

ただし、最初に、 + redis-server +`を起動して、実行中のRedisサーバーと通信する必要があります。 開発でこれを行う一般的な方法は、Redisに指示しない限りデフォルトであるhttps://en.wikipedia.org/wiki/Localhost[localhost](IPv4アドレス `+ 127.0.0.1 +)でサーバーを起動することですさもないと。 設定ファイルの名前を `+ redis-server +`に渡すこともできます。これは、すべてのキーと値のペアをコマンドライン引数として指定することに似ています。

$ redis-server/etc/redis/6379.conf
31829:C 07 Mar 2019 08:45:04.030 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
31829:C 07 Mar 2019 08:45:04.030 # Redis version=5.0.3, bits=64, commit=00000000, modified=0, pid=31829, just started
31829:C 07 Mar 2019 08:45:04.030 # Configuration loaded

`+ daemonize `設定オプションを ` yes `に設定して、サーバーがバックグラウンドで実行されるようにします。 (そうでない場合は、 `-disdis-server `のオプションとして `-daemonize yes +`を使用します。)

これで、Redis REPLを起動する準備が整いました。 コマンドラインに「+ redis-cli 」と入力します。 サーバーの_host:port_ペアに続いて `> +`プロンプトが表示されます:

127.0.0.1:6379>

最も単純なRedisコマンドの1つであるhttps://redis.io/commands/ping [+ PING +]は、サーバーへの接続をテストし、問題がなければ `" PONG "`を返します。

127.0.0.1:6379> PING
PONG

Redisコマンドは大文字と小文字を区別しませんが、Pythonの対応するコマンドはほとんど間違いありません。

*注意:*別の健全性チェックとして、 `+ pgrep +`でRedisサーバーのプロセスIDを検索できます:

$ pgrep redis-server
26983

サーバーを強制終了するには、コマンドラインから「+ pkill redis-server 」を使用します。 Mac OS Xでは、 ` redis-cli shutdown +`も使用できます。

次に、一般的なRedisコマンドのいくつかを使用して、それらを純粋なPythonでの表示と比較します。

Python辞書としてのRedis

Redisは Remote Dictionary Service の略です。

「つまり、Python dictionaryのように?」あなたが尋ねることができます。

Yes. 大まかに言って、Pythonディクショナリ(または汎用ハッシュテーブル)とRedisの機能との間に多くの類似点があります。

  • Redisデータベースは_key:value_ペアを保持し、「+ GET 」、「 SET 」、「 DEL 」などのコマンド、およびhttps://redis.io/commands [数百]の追加コマンドをサポートします。 *Redis* keys *は常に文字列です。 *Redis* 値*は、いくつかの異なるデータ型である場合があります。 このチュートリアルでは、「 string 」、「 list 」、「 hashes 」、および「 sets +」というより重要な値のデータ型の一部を取り上げます。 一部の高度なタイプには、https://redis.io/commands#geo [geospatial items]および新しいhttps://redis.io/commands#stream[stream]タイプが含まれます。

  • 多くのRedisコマンドは、Pythonの `+ dict +`または任意のハッシュテーブルから値を取得するように、一定のO(1)時間で動作します。

Redisの作成者であるSalvatore Sanfilippoは、おそらくRedisデータベースと普通のPython `+ dict +`との比較を好まないでしょう。 彼はプロジェクトを「データ構造サーバー」と呼びます(https://www.memcached.org/[memcached]などのキーと値のストアではなく)。Redisは追加のタイプの_key:value_ _string:string_以外のデータ型。 しかし、ここでの目的のために、Pythonの辞書オブジェクトに精通している場合、これは便利な比較になります。

飛び入り、例で学びましょう。 最初のおもちゃデータベース(ID 0)は_country:capital city_のマッピングになり、https://redis.io/commands/set [+ SET +]を使用してキーと値のペアを設定します。

127.0.0.1:6379> SET Bahamas Nassau
OK
127.0.0.1:6379> SET Croatia Zagreb
OK
127.0.0.1:6379> GET Croatia
"Zagreb"
127.0.0.1:6379> GET Japan
(nil)

純粋なPythonのステートメントの対応するシーケンスは次のようになります。

>>>

>>> capitals = {}
>>> capitals["Bahamas"] = "Nassau"
>>> capitals["Croatia"] = "Zagreb"
>>> capitals.get("Croatia")
'Zagreb'
>>> capitals.get("Japan")  # None

キーが見つからなかった場合、Redisはエラーではなく + nil +`を返すため、 `+ capitals [" Japan "] +`ではなく `+ capitals.get(" Japan ")+`を使用します。これはPythonの+なし+`。

Redisでは、1つのコマンドhttps://redis.io/commands/mset [+ MSET +]とhttps://redis.io/commands/mget[`+MGET+で複数のキーと値のペアを設定および取得することもできます。 `]、それぞれ:

127.0.0.1:6379> MSET Lebanon Beirut Norway Oslo France Paris
OK
127.0.0.1:6379> MGET Lebanon Norway Bahamas
1) "Beirut"
2) "Oslo"
3) "Nassau"

Pythonで最も近いのは `+ dict.update()+`を使用することです:

>>>

>>> capitals.update({
...     "Lebanon": "Beirut",
...     "Norway": "Oslo",
...     "France": "Paris",
... })
>>> [capitals.get(k) for k in ("Lebanon", "Norway", "Bahamas")]
['Beirut', 'Oslo', 'Nassau']

キーが見つからないときにnullのような値を返すRedisの動作を模倣するために、「+ . getitem ()」ではなく「 .get()+」を使用します。

3番目の例として、https://redis.io/commands/exists [+ EXISTS +]コマンドは、キーが存在するかどうかを確認するように聞こえます。

127.0.0.1:6379> EXISTS Norway
(integer) 1
127.0.0.1:6379> EXISTS Sweden
(integer) 0

Pythonには、同じことをテストするためのキーワード「+ in 」があり、「 dict . contains (key)+ `にルーティングされます。

>>>

>>> "Norway" in capitals
True
>>> "Sweden" in capitals
False

これらのいくつかの例は、ネイティブPythonを使用して、いくつかの一般的なRedisコマンドで高レベルで何が起こっているかを示すことを目的としています。 Pythonの例にはクライアントサーバーコンポーネントはありません。また、 `+ redis-py +`はまだ登場していません。 これは、例によってRedis機能を示すことのみを目的としています。

以下は、これまでに見たいくつかのRedisコマンドと、それらに対応するPythonの機能の要約です。

この記事で間もなく説明するPython Redisクライアントライブラリ + redis-py +`は、異なる方法で処理を行います。 Redisサーバーへの実際のTCP接続をカプセル化し、https://redis.io/topics/protocol [REdis Serialization Protocol](RESP)を使用してシリアル化されたバイトとしてサーバーにrawコマンドを送信します。 次に、生の応答を受け取り、それを解析して、 `+ bytes ++ int +、または `+ datetime.datetime +`などのPythonオブジェクトに戻します。

:これまで、対話型の + redis-cli + REPLを介してRedisサーバーと通信してきました。 `+ python myscript.py `のようなスクリプトの名前を ` python +`実行可能ファイルに渡すのと同じ方法で、https://redis.io/topics/rediscli [直接コマンドを発行]することもできます。 。

これまで、Redisの基本的なデータ型のいくつかを見てきました。これは、_string:string_のマッピングです。 このキーと値のペアはほとんどのキーと値のストアで一般的ですが、Redisには他にも多くの可能な値の種類があります。

PythonとRedisのその他のデータ型

+ redis-py + Pythonクライアントを起動する前に、さらにいくつかのRedisデータ型を基本的に把握しておくと役立ちます。 明確にするために、すべてのRedisキーは文字列です。 これは、これまでの例で使用された文字列値に加えて、データ型(または構造)を取り得る値です。

ハッシュ*は、 *field-value ペアと呼ばれる_string:string_のマッピングであり、1つのトップレベルキーの下にあります。

127.0.0.1:6379> HSET realpython url "https://realpython.com/"
(integer) 1
127.0.0.1:6379> HSET realpython github realpython
(integer) 1
127.0.0.1:6379> HSET realpython fullname "Real Python"
(integer) 1

これにより、1つの key 、 `" realpython "`に3つのフィールドと値のペアが設定されます。 Pythonの用語とオブジェクトに慣れていると、混乱する可能性があります。 Redisハッシュは、1レベルの深さにネストされたPythonの `+ dict +`にほぼ類似しています。

data = {
    "realpython": {
        "url": "https://realpython.com/",
        "github": "realpython",
        "fullname": "Real Python",
    }
}

Redisのフィールドは、上記の内部辞書にあるネストされた各キーと値のペアのPythonキーに似ています。 Redisは、ハッシュ構造自体を保持するトップレベルのデータベースキーの用語 key を予約しています。

基本的な_string:string_キーと値のペアに + MSET +`があるように、ハッシュに複数のペアを設定するハッシュ値オブジェクトにhttps://redis.io/commands/hmset [+ HMSET +`]があります。

127.0.0.1:6379> HMSET pypa url "https://www.pypa.io/" github pypa fullname "Python Packaging Authority"
OK
127.0.0.1:6379> HGETALL pypa
1) "url"
2) "https://www.pypa.io/"
3) "github"
4) "pypa"
5) "fullname"
6) "Python Packaging Authority"

`+ HSET `を使用することは、ネストされた各ペアを ` HSET `で行うように設定するのではなく、上記のネストされた辞書に ` data +`を割り当てる方法とほぼ同じです。

追加の2つの値タイプはhttps://redis.io/topics/data-types-intro#redis-lists[lists]およびhttps://redis.io/topics/data-types-intro#redis-sets [ sets ]。ハッシュまたは文字列の代わりにRedis値を使用できます。 それらは主に彼らがどのように聞こえるかですので、私は追加の例であなたの時間を割くことはしません。 ハッシュ、リスト、およびセットにはそれぞれ、特定のデータ型に固有のコマンドがいくつかあります。これらのコマンドは、最初の文字で示される場合があります。

  • *ハッシュ:*ハッシュを操作するコマンドは、「+ HSET 」、「 HGET 」、「 HMSET 」などの「 H +」で始まります。

  • *セット:*セットを操作するコマンドは、「+ SCARD 」などの「 S +」で始まります。これは、指定されたキーに対応するセット値の要素数を取得します。

  • *リスト:*リストを操作するコマンドは、 `+ L `または ` R `で始まります。 例には、「 LPOP 」および「 RPUSH 」が含まれます。 「 L 」または「 R 」は、リストのどちらの側が操作されているかを示します。 いくつかのリストコマンドの先頭には「 B 」が付いています。これは*ブロッキング*を意味します。 ブロッキング操作では、実行中に他の操作によってブロックされることはありません。 たとえば、 ` BLPOP +`はリスト構造でブロッキング左ポップを実行します。

*注: *Redisのリストタイプの注目すべき特徴の1つは、配列ではなくリンクリストであることです。 これは、任意のインデックス番号でのインデックス作成がO(N)であるのに対して、追加がO(1)であることを意味します。

以下は、Redisの文字列、ハッシュ、リスト、およびセットのデータ型に固有のコマンドの簡単なリストです。

Type Commands

Sets

SADD, SCARD, SDIFF, SDIFFSTORE, SINTER, SINTERSTORE, SISMEMBER, SMEMBERS, SMOVE, SPOP, SRANDMEMBER, SREM, SSCAN, SUNION, SUNIONSTORE

Hashes

HDEL, HEXISTS, HGET, HGETALL, HINCRBY, HINCRBYFLOAT, HKEYS, HLEN, HMGET, HMSET, HSCAN, HSET, HSETNX, HSTRLEN, HVALS

Lists

BLPOP, BRPOP, BRPOPLPUSH, LINDEX, LINSERT, LLEN, LPOP, LPUSH, LPUSHX, LRANGE, LREM, LSET, LTRIM, RPOP, RPOPLPUSH, RPUSH, RPUSHX

Strings

APPEND, BITCOUNT, BITFIELD, BITOP, BITPOS, DECR, DECRBY, GET, GETBIT, GETRANGE, GETSET, INCR, INCRBY, INCRBYFLOAT, MGET, MSET, MSETNX, PSETEX, SET, SETBIT, SETEX, SETNX, SETRANGE, STRLEN

この表は、Redisのコマンドとタイプの完全な図ではありません。 geospatial items、https://redis.io/commands#sorted_set [sorted sets]、https://redisなど、より高度なデータ型のバイキングがあります。 io/commands#hyperloglog [HyperLogLog]。 Redis commandsページで、データ構造グループでフィルタリングできます。 https://redis.io/topics/data-types [データ型の概要]とhttps://redis.io/topics/data-types-intro[Redisデータ型の紹介]もあります。

Pythonでの作業に切り替えるため、https://redis.io/commands/flushdb [+ FLUSHDB +]でおもちゃのデータベースをクリアし、 + redis-cli + REPLを終了できます:

127.0.0.1:6379> FLUSHDB
OK
127.0.0.1:6379> QUIT

これにより、シェルプロンプトに戻ります。 チュートリアルの残りの部分でも必要になるため、「+ redis-server +」をバックグラウンドで実行したままにしておくことができます。

`+ redis-py +`の使用:PythonのRedis

Redisの基本を習得したので、ユーザーフレンドリーなPython APIからRedisと対話できるPythonクライアントである `+ redis-py +`にジャンプします。

最初のステップ

https://github.com/andymccurdy/redis-py [+ redis-py +]は、Python呼び出しを介してRedisサーバーと直接通信できる、確立されたPythonクライアントライブラリです。

$ python -m pip install redis

次に、Redisサーバーがバックグラウンドで稼働していることを確認します。 `+ pgrep redis-server `で確認できます。手ぶらで立ち上がったら、 ` redis-server/etc/redis/6379.conf +`でローカルサーバーを再起動します。

それでは、Python中心の部分に取り掛かりましょう。 「+ redis-py +」の「ハローワールド」は次のとおりです。

>>>

 1 >>> import redis
 2 >>> r = redis.Redis()
 3 >>> r.mset({"Croatia": "Zagreb", "Bahamas": "Nassau"})
 4 True
 5 >>> r.get("Bahamas")
 6 b'Nassau'

2行目で使用される `+ Redis `は、パッケージの中心クラスであり、Redisコマンドを(ほぼ)実行する主力クラスです。 TCPソケット接続と再利用はバックグラウンドで行われ、クラスインスタンス ` r +`のメソッドを使用してRedisコマンドを呼び出します。

また、返されたオブジェクトのタイプである6行目の + b’Nassau '+`は、Pythonのhttps://docs.python.org/3/library/stdtypes.html#bytes [+ bytes `]タイプであることに注意してください。 、 ` str `ではありません。 これは、 ` redis-py `で最も一般的な戻り値型である ` str `ではなく ` bytes `です。そのため、 ` r.get(" Bahamas ")。decode(" utf-8 「)+ `返されたバイト文字列で実際に何をしたいかによって異なります。

上記のコードは見慣れていますか? ほとんどすべての場合、メソッドは同じことを行うRedisコマンドの名前と一致します。 ここでは、ネイティブRedis APIの `+ MSET `と ` GET `に対応する ` r.mset()`と ` r.get()+`を呼び出しました。

これは、 `+ HGETALL `が ` r.hgetall()`に、 ` PING `が ` r.ping()+`に、などとなることも意味します。 few例外がありますが、ルールの大部分はコマンドに適用されます。

通常、Redisコマンド引数は類似したメソッドシグネチャに変換されますが、Pythonオブジェクトを受け取ります。 たとえば、上記の例での `+ r.mset()`の呼び出しは、バイト文字列のシーケンスではなく、Pythonの ` dict +`を最初の引数として使用します。

引数なしで `+ Redis `インスタンス ` r +`を構築しましたが、多くのhttps://github.com/andymccurdy/redis-py/blob/b940d073de4c13f8dfb08728965c6ac7c183c935/redis/client.py#L605 [パラメータ]必要な場合:

# From redis/client.py
class Redis(object):
    def __init__(self, host='localhost', port=6379,
                 db=0, password=None, socket_timeout=None,
                 # ...

デフォルトの_hostname:port_ペアは `+ localhost:6379 `であることがわかります。これは、ローカルに保持された ` redis-server +`インスタンスの場合にまさに必要なものです。

`+ db +`パラメータはデータベース番号です。 Redisで複数のデータベースを一度に管理でき、各データベースは整数で識別されます。 データベースの最大数はデフォルトで16です。

コマンドラインから `+ redis-cli `を実行すると、データベース0から開始します。 ` redis-cli -n 5 `のように、 ` -n +`フラグを使用して新しいデータベースを起動します。

許可されるキータイプ

知っておく価値のあることの1つは、「+ redis-py 」では、「 bytes 」、「 str 」、「 int 」、または「 float 」のキーを渡す必要があることです。 (これらのタイプの最後の3つをサーバーに送信する前に ` bytes +`に変換します。)

カレンダーの日付をキーとして使用する場合を考えます。

>>>

>>> import datetime
>>> today = datetime.date.today()
>>> visitors = {"dan", "jon", "alex"}
>>> r.sadd(today,* visitors)
Traceback (most recent call last):
# ...
redis.exceptions.DataError: Invalid input of type: 'date'.
Convert to a byte, string or number first.

Pythonの `+ date `オブジェクトを明示的に ` str `に変換する必要があります。これは、 ` .isoformat()+`で実行できます。

>>>

>>> stoday = today.isoformat()  # Python 3.7+, or use str(today)
>>> stoday
'2019-03-10'
>>> r.sadd(stoday, *visitors)  # sadd: set-add
3
>>> r.smembers(stoday)
{b'dan', b'alex', b'jon'}
>>> r.scard(today.isoformat())
3

要約すると、Redis自体はキーとして文字列のみを許可します。 `+ redis-py +`はPythonタイプが受け入れるものが少しリベラルですが、Redisサーバーに送信する前に最終的にすべてをバイトに変換します。

例:PyHats.com

今度はより完全な例を取り上げます。 高価なウェブサイト、PyHats.comを開始することに決めたとします。PyHats.comは、高すぎる帽子を購入する人に販売し、サイトを構築するためにあなたを雇いました。

Redisを使用して、PyHats.comの製品カタログ、在庫管理、ボットトラフィック検出の一部を処理します。

このサイトの初日であり、3つの限定エディションの帽子を販売する予定です。 各帽子は、フィールドと値のペアのRedisハッシュに保持され、ハッシュには、「+ hat:56854717+」などのプレフィックス付きのランダムな整数のキーがあります。 `+ hat:+`プレフィックスを使用することは、Redisデータベース内に一種の名前空間を作成するためのRedisの慣例です。

import random

random.seed(444)
hats = {f"hat:{random.getrandbits(32)}": i for i in (
    {
        "color": "black",
        "price": 49.99,
        "style": "fitted",
        "quantity": 1000,
        "npurchased": 0,
    },
    {
        "color": "maroon",
        "price": 59.99,
        "style": "hipster",
        "quantity": 500,
        "npurchased": 0,
    },
    {
        "color": "green",
        "price": 99.99,
        "style": "baseball",
        "quantity": 200,
        "npurchased": 0,
    })
}

前の例でデータベース「0」を使用したため、データベース「1」から始めましょう。

>>>

>>> r = redis.Redis(db=1)

このデータをRedisに最初に書き込むには、 + .hmset()+(ハッシュマルチセット)を使用して、各辞書に対して呼び出します。 「マルチ」は、複数のフィールドと値のペアを設定することへの参照です。この場合の「フィールド」は、 `+ hats +`のネストされた辞書のキーに対応します。

 1 >>> with r.pipeline() as pipe:
 2 ...    for h_id, hat in hats.items():
 3 ...        pipe.hmset(h_id, hat)
 4 ...    pipe.execute()
 5 Pipeline<ConnectionPool<Connection<host=localhost,port=6379,db=1>>>
 6 Pipeline<ConnectionPool<Connection<host=localhost,port=6379,db=1>>>
 7 Pipeline<ConnectionPool<Connection<host=localhost,port=6379,db=1>>>
 8 [True, True, True]
 9
10 >>> r.bgsave()
11 True

上記のコードブロックでは、Redis pipeliningの概念も導入しています。これは、データの書き込みまたは読み取りに必要な往復トランザクションの数を削減する方法です。 Redisサーバー。 `+ r.hmset()+`を3回呼び出しただけの場合、書き込まれた各行に対して往復の往復操作が必要になります。

パイプラインでは、すべてのコマンドはクライアント側でバッファリングされ、3行目に `+ pipe.hmset()`を使用して一度に送信されます。 4行目で ` pipe.execute()`を呼び出すと、3つの ` True +`応答がすべて一度に返されるのはこのためです。 パイプラインのより高度なユースケースが間もなく表示されます。

注意:Redisのドキュメントは、ローカルファイルのコンテンツを一括挿入を行います。

Redisデータベースにすべてが揃っていることを簡単に確認しましょう。

>>>

>>> pprint(r.hgetall("hat:56854717"))
{b'color': b'green',
 b'npurchased': b'0',
 b'price': b'99.99',
 b'quantity': b'200',
 b'style': b'baseball'}

>>> r.keys()  # Careful on a big DB. keys() is O(N)
[b'56854717', b'1236154736', b'1326692461']

シミュレートする最初のことは、ユーザーが_Purchase_をクリックしたときに何が起こるかです。 アイテムの在庫がある場合は、「+ npurchased 」を1つ増やし、「 quantity 」(在庫)を1つ減らします。 これを行うには、 ` .hincrby()+`を使用できます。

>>>

>>> r.hincrby("hat:56854717", "quantity", -1)
199
>>> r.hget("hat:56854717", "quantity")
b'199'
>>> r.hincrby("hat:56854717", "npurchased", 1)
1

注意: `+ HINCRBY +`は文字列であるハッシュ値で動作しますが、操作を実行するために文字列を10進数の64ビット符号付き整数として解釈しようとします。

これは、他のデータ構造のインクリメントおよびデクリメントに関連する他のコマンド、つまり「+ INCR 」、「 INCRBY 」、「 INCRBYFLOAT 」、「 ZINCRBY 」、および「 HINCRBYFLOAT +」に適用されます。 値の文字列を整数として表現できない場合、エラーが発生します。

ただし、それほど単純ではありません。 2行のコードで「+ quantity 」と「 npurchased +」を変更すると、クリック、購入、支払いがこれ以上のものになるという現実が見えなくなります。 財布を軽くして帽子を持たない人を放置しないように、さらにいくつかのチェックを行う必要があります。

  • *ステップ1:*アイテムの在庫があるかどうかを確認するか、バックエンドで例外を発生させます。

  • *ステップ2:*在庫がある場合、トランザクションを実行し、 `+ quantity `フィールドを減らし、 ` npurchased +`フィールドを増やします。

  • *ステップ3:*最初の2つのステップ(https://realpython.com/python-concurrency/#threading-version[race condition])の間でインベントリを変更する変更について注意してください。

ステップ1は比較的簡単です。利用可能な数量をチェックするための `+ .hget()+`で構成されています。

ステップ2はもう少し複雑です。 増加操作と減少操作のペアを*原子的に*実行する必要があります。両方とも正常に完了するか、どちらも完了しないかのいずれかです(少なくとも1つが失敗した場合)。

クライアント/サーバーフレームワークでは、原子性に注意を払い、複数のクライアントが一度にサーバーと通信しようとする場合に何が問題になる可能性があるかを常に確認することが重要です。 Redisでのこれに対する答えは、https://redis.io/topics/transactions [ transaction ]ブロックを使用することです。これは、両方のコマンドが通過するか、どちらも通過しないことを意味します。

`+ redis-py `では、 ` Pipeline +`はデフォルトで*トランザクションパイプライン*クラスです。 これは、クラスが実際には別の名前(パイプライン化)で命名されていても、トランザクションブロックの作成にも使用できることを意味します。

Redisでは、トランザクションは「+ MULTI 」で始まり、「 EXEC +」で終わります。

 1 127.0.0.1:6379> MULTI
 2 127.0.0.1:6379> HINCRBY 56854717 quantity -1
 3 127.0.0.1:6379> HINCRBY 56854717 npurchased 1
 4 127.0.0.1:6379> EXEC

+ MULTI +(行1)はトランザクションの開始を示し、 + EXEC +(行4)は終了を示します。 間にあるものはすべて、コマンドの1つのオールオアナッシングバッファーシーケンスとして実行されます。 これは、 `+ quantity `を減分することは不可能であることを意味します(2行目)が、バランスをとる ` npurchased +`増分操作は失敗します(行3)。

ステップ3に戻りましょう。最初の2つのステップの間に在庫を変更する変更を認識しておく必要があります。

ステップ3が最もトリッキーです。 在庫に1つの孤独な帽子が残っているとしましょう。 ユーザーAが残りの帽子の数量を確認し、実際にトランザクションを処理する間に、ユーザーBも在庫を確認し、同様に在庫に1つの帽子がリストされていることを見つけます。 どちらのユーザーも帽子を購入することができますが、2つの帽子ではなく1つの帽子を販売するため、フックにかかっており、1人のユーザーがお金を使い果たしています。 良くない。

Redisには、ステップ3のジレンマに対する賢明な答えがあります。これはhttps://en.wikipedia.org/wiki/Optimistic_concurrency_control[*optimistic locking *]と呼ばれ、PostgreSQLなどのRDBMSでの典型的なロックの仕組みとは異なります。 簡単に言えば、楽観的ロックとは、呼び出し側の関数(クライアント)がロックを取得せず、ロックを保持している時間中に書き込むデータの変化を監視することを意味します。 その間に競合が発生した場合、呼び出し関数は単にプロセス全体を再試行します。

大きなコードの塊を導入し、その後、ステップごとに見ていきましょう。 ユーザーが_Buy Now_または_Purchase_ボタンをクリックするたびに呼び出される `+ buyitem()+`を想像できます。 その目的は、アイテムの在庫を確認し、その結果に基づいて安全な方法でアクションを実行し、すべてが競合状態を検出し、検出された場合は再試行することです。

 1 import logging
 2 import redis
 3
 4 logging.basicConfig()
 5
 6 class OutOfStockError(Exception):
 7     """Raised when PyHats.com is all out of today's hottest hat"""
 8
 9 def buyitem(r: redis.Redis, itemid: int) -> None:
10     with r.pipeline() as pipe:
11         error_count = 0
12         while True:
13             try:
14                 # Get available inventory, watching for changes
15                 # related to this itemid before the transaction
16                 pipe.watch(itemid)
17                 nleft: bytes = r.hget(itemid, "quantity")
18                 if nleft > b"0":
19                     pipe.multi()
20                     pipe.hincrby(itemid, "quantity", -1)
21                     pipe.hincrby(itemid, "npurchased", 1)
22                     pipe.execute()
23                     break
24                 else:
25                     # Stop watching the itemid and raise to break out
26                     pipe.unwatch()
27                     raise OutOfStockError(
28                         f"Sorry, {itemid} is out of stock!"
29                     )
30             except redis.WatchError:
31                 # Log total num. of errors by this user to buy this item,
32                 # then try the same process again of WATCH/HGET/MULTI/EXEC
33                 error_count += 1
34                 logging.warning(
35                     "WatchError #%d: %s; retrying",
36                     error_count, itemid
37                 )
38     return None

クリティカルラインは16行目で `+ pipe.watch(itemid)`で発生し、Redisに値の変更がないかどうか指定された ` itemid `を監視します。 プログラムは、17行目の ` r.hget(itemid、" quantity ")+`の呼び出しを通じてインベントリをチェックします。

16 pipe.watch(itemid)
17 nleft: bytes = r.hget(itemid, "quantity")
18 if nleft > b"0":
19     # Item in stock. Proceed with transaction.

ユーザーがアイテムの在庫を確認して購入しようとする間のこの短いウィンドウの間に在庫に触れると、Redisはエラーを返し、 + redis-py +`は `+ WatchError +(30行目)を発生させます。 つまり、 `+ itemid `が指すハッシュのいずれかが、 ` .hget()`呼び出しの後で、20行目と21行目の後続の ` .hincrby()`呼び出しの前に変更された場合、結果として、 ` while True +`ループの別の反復でプロセス全体を再実行します。

これはロックの「楽観的」な部分です。取得および設定操作を通じてクライアントがデータベース上で時間のかかる合計ロックを保持できるようにするのではなく、Redisに任せてクライアントとユーザーに通知します在庫チェックの再試行を要求します。

ここで重要なのは、*クライアント側*操作と*サーバー側*操作の違いを理解することです。

nleft = r.hget(itemid, "quantity")

このPythonの割り当ては、クライアント側で `+ r.hget()`の結果をもたらします。 逆に、 ` pipe +`で呼び出すメソッドは、すべてのコマンドを1つに効果的にバッファリングし、1回のリクエストでサーバーに送信します。

16 pipe.multi()
17 pipe.hincrby(itemid, "quantity", -1)
18 pipe.hincrby(itemid, "npurchased", 1)
19 pipe.execute()

トランザクションパイプラインの途中でクライアント側にデータが返されることはありません。 結果のシーケンスを一度に取得するには、 + .execute()+(19行目)を呼び出す必要があります。

このブロックには2つのコマンドが含まれていますが、クライアントからサーバーへの往復の1回の往復操作のみで構成されています。

これは、 `+ Pipeline `のメソッドが ` pipe `のみを返すため、クライアントは20行目から ` pipe.hincrby(itemid、" quantity "、-1)`の結果をすぐに使用できないことを意味します。インスタンス自体。 この時点では、サーバーに何も尋ねていません。 通常、 ` .hincrby()+`は結果の値を返しますが、トランザクション全体が完了するまでクライアント側ですぐに参照することはできません。

キャッチ22があります。これが、トランザクションブロックに `+ .hget()`の呼び出しを配置できない理由でもあります。 これを行うと、トランザクションパイプラインに挿入されたコマンドからリアルタイムの結果を取得できないため、 ` npurchased +`フィールドをまだインクリメントするかどうかを知ることができません。

最後に、在庫がゼロの場合、アイテムIDを「+ UNWATCH 」し、「 OutOfStockError +」(行27)を生成します。最終的に、ハットバイヤーが必死にさらに購入したいという切望された_Sold Out_ページを表示しますこれまで以上に奇抜な価格の帽子:

24 else:
25     # Stop watching the itemid and raise to break out
26     pipe.unwatch()
27     raise OutOfStockError(
28         f"Sorry, {itemid} is out of stock!"
29     )

これがイラストです。 上記の `+ .hincrby()`を呼び出したため、帽子56854717の開始数量は ` 199 `であることに注意してください。 「 quantity 」フィールドと「 npurchased +」フィールドを変更する3つの購入を模倣しましょう。

>>>

>>> buyitem(r, "hat:56854717")
>>> buyitem(r, "hat:56854717")
>>> buyitem(r, "hat:56854717")
>>> r.hmget("hat:56854717", "quantity", "npurchased")  # Hash multi-get
[b'196', b'4']

これで、より多くの購入を早送りして、在庫がゼロになるまで購入の流れを模倣できます。 繰り返しますが、これらは1つの `+ Redis +`インスタンスではなく、さまざまなクライアントの全体から来ていることを想像してください。

>>>

>>> # Buy remaining 196 hats for item 56854717 and deplete stock to 0
>>> for _ in range(196):
...     buyitem(r, "hat:56854717")
>>> r.hmget("hat:56854717", "quantity", "npurchased")
[b'0', b'200']

さて、一部の貧しいユーザーがゲームに遅れると、フロントエンドでエラーメッセージページをレンダリングするようにアプリケーションに指示する `+ OutOfStockError +`に出会う必要があります。

>>>

>>> buyitem(r, "hat:56854717")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 20, in buyitem
__main__.OutOfStockError: Sorry, hat:56854717 is out of stock!

在庫がありそうです。

キー有効期限の使用

Redisのもう1つの特徴的な機能である key expiry を紹介しましょう。 キーをhttps://redis.io/commands/expire[expire]すると、そのキーとそれに対応する値は、特定の秒数後または特定のタイムスタンプでデータベースから自動的に削除されます。

`+ redis-py `では、これを達成する1つの方法は ` .setex()+`を使用することです。これにより、基本的な_string:string_キーと値のペアに有効期限を設定できます。

>>>

 1 >>> from datetime import timedelta
 2
 3 >>> # setex: "SET" with expiration
 4 >>> r.setex(
 5 ...     "runner",
 6 ...     timedelta(minutes=1),
 7 ...     value="now you see me, now you don't"
 8 ... )
 9 True

上記の6行目のように、2番目の引数を秒単位の数値または `+ timedelta +`オブジェクトとして指定できます。 後者が好きなのは、あいまいさが少なく、より意図的だからです。

有効期限が設定されているキーの残りのライフタイム( time-to-live )を取得する方法(および対応するRedisコマンド)もあります。

>>>

>>> r.ttl("runner")  # "Time To Live", in seconds
58
>>> r.pttl("runner")  # Like ttl, but milliseconds
54368

以下では、有効期限までウィンドウを加速し、キーの有効期限を監視します。その後、「+ r.get()」は「 None 」を返し、「。exists()」は「+0」を返します:

>>>

>>> r.get("runner")  # Not expired yet
b"now you see me, now you don't"

>>> r.expire("runner", timedelta(seconds=3))  # Set new expire window
True
>>> # Pause for a few seconds
>>> r.get("runner")
>>> r.exists("runner")  # Key & value are both gone (expired)
0

以下の表は、キー値の有効期限に関連するコマンドの要約です。 説明は、 `+ redis-py +`メソッドのdocstringsから直接取得されます。

Signature Purpose

r.setex(name, time, value)

Sets the value of key name to value that expires in time seconds, where time can be represented by an int or a Python timedelta object

r.psetex(name, time_ms, value)

Sets the value of key name to value that expires in time_ms milliseconds, where time_ms can be represented by an int or a Python timedelta object

r.expire(name, time)

Sets an expire flag on key name for time seconds, where time can be represented by an int or a Python timedelta object

r.expireat(name, when)

Sets an expire flag on key name, where when can be represented as an int indicating Unix time or a Python datetime object

r.persist(name)

Removes an expiration on name

r.pexpire(name, time)

Sets an expire flag on key name for time milliseconds, and time can be represented by an int or a Python timedelta object

r.pexpireat(name, when)

Sets an expire flag on key name, where when can be represented as an int representing Unix time in milliseconds (Unix time * 1000) or a Python datetime object

r.pttl(name)

Returns the number of milliseconds until the key name will expire

r.ttl(name)

Returns the number of seconds until the key name will expire

PyHats.com、パート2

デビューから数日後、PyHats.comは非常に誇大広告を集めており、一部の進取の気性に優れたユーザーが数秒以内に数百個のアイテムを購入するボットを作成しています。

キーを期限切れにする方法を確認したので、PyHats.comのバックエンドで使用できるようにしましょう。

コンシューマ(またはウォッチャー)として機能し、着信IPアドレスのストリームを処理する新しいRedisクライアントを作成します。このIPアドレスは、Webサイトのサーバーへの複数のHTTPS接続から取得される場合があります。

ウォッチャーの目標は、複数のソースからのIPアドレスのストリームを監視し、疑わしいほど短い時間内に単一のアドレスからの大量のリクエストに注意することです。

Webサイトサーバー上の一部のミドルウェアは、すべての受信IPアドレスを `+ .lpush()+`を使用してRedisリストにプッシュします。 新鮮なRedisデータベースを使用して、着信IPを模倣する大まかな方法​​を次に示します。

>>>

>>> r = redis.Redis(db=5)
>>> r.lpush("ips", "51.218.112.236")
1
>>> r.lpush("ips", "90.213.45.98")
2
>>> r.lpush("ips", "115.215.230.176")
3
>>> r.lpush("ips", "51.218.112.236")
4

ご覧のとおり、 `+ .lpush()`はプッシュ操作が成功した後のリストの長さを返します。 ` .lpush()`を呼び出すたびに、文字列 `" ips "+`をキーとするRedisリストの先頭にIPが配置されます。

この単純化されたシミュレーションでは、リクエストはすべて技術的には同じクライアントからのものですが、潜在的に多くの異なるクライアントからのものであり、すべてが同じRedisサーバー上の同じデータベースにプッシュされると考えることができます。

次に、新しいシェルタブまたはウィンドウを開き、新しいPython REPLを起動します。 このシェルでは、他とは非常に異なる目的に役立つ新しいクライアントを作成します。このクライアントは、無限の「+ while True 」ループ内にあり、左のポップをブロックしますhttps://redis.io/commands/blpop [` BLPOP `]は ` ips +`リストを呼び出し、各アドレスを処理します:

 1 # New shell window or tab
 2
 3 import datetime
 4 import ipaddress
 5
 6 import redis
 7
 8 # Where we put all the bad egg IP addresses
 9 blacklist = set()
10 MAXVISITS = 15
11
12 ipwatcher = redis.Redis(db=5)
13
14 while True:
15     _, addr = ipwatcher.blpop("ips")
16     addr = ipaddress.ip_address(addr.decode("utf-8"))
17     now = datetime.datetime.utcnow()
18     addrts = f"{addr}:{now.minute}"
19     n = ipwatcher.incrby(addrts, 1)
20     if n >= MAXVISITS:
21         print(f"Hat bot detected!:  {addr}")
22         blacklist.add(addr)
23     else:
24         print(f"{now}:  saw {addr}")
25     _ = ipwatcher.expire(addrts, 60)

いくつかの重要な概念を見ていきましょう。

`+ ipwatcher `はhttps://realpython.com/intro-to-python-threading/#producer-consumer-threading[consumer]のように動作し、「」ipに新しいIPがプッシュされるのを待ちます。 「+ `Redisリスト。 b” 51.218.112.236”のような「+ bytes 」としてそれらを受け取り、より適切なhttps://docs.python.org/3/library/ipaddress.html#address-objects [アドレスオブジェクト]にします。 ` ipaddress +`モジュール:

15 _, addr = ipwatcher.blpop("ips")
16 addr = ipaddress.ip_address(addr.decode("utf-8"))

次に、アドレスと「+ ipwatcher 」がアドレスを見た時間を使用してRedis文字列キーを形成し、対応するカウントを「+1」でインクリメントし、プロセスで新しいカウントを取得します。

17 now = datetime.datetime.utcnow()
18 addrts = f"{addr}:{now.minute}"
19 n = ipwatcher.incrby(addrts, 1)

アドレスが「+ MAXVISITS +」よりも多く見られた場合、次のhttps://en.wikipedia.org/wiki/Tulip_mania[tulip bubbleを作成しようとしているPyHats.com Webスクレーパーを手に持っているように見えます]。 残念ながら、このユーザーに恐ろしい403ステータスコードのようなものを返すしかありません。

`+ ipwatcher.expire(addrts、60)+`を使用して、_(アドレス分)_の組み合わせを、最後に表示されてから60秒で期限切れにします。 これは、データベースが古い1回限りのページビューアーで詰まるのを防ぐためです。

このシェルを新しいシェルで実行すると、すぐに次の出力が表示されます。

2019-03-11 15:10:41.489214:  saw 51.218.112.236
2019-03-11 15:10:41.490298:  saw 115.215.230.176
2019-03-11 15:10:41.490839:  saw 90.213.45.98
2019-03-11 15:10:41.491387:  saw 51.218.112.236

これらの4つのIPは、 " ips "+`をキーとするキューのようなリストにあり、 `+ ipwatcher +`によって取り出されるのを待っているため、出力がすぐに表示されます。 `+ .blpop()(または + BLPOP +`コマンド)を使用すると、リスト内のアイテムが利用可能になるまでブロックされ、それからポップされます。 Pythonのhttps://docs.python.org/3/library/queue.html#queue.Queue.get [+ Queue.get()+`]のように動作し、アイテムが使用可能になるまでブロックします。

IPアドレスを吐き出すだけでなく、 `+ ipwatcher `には2番目の仕事があります。 1分(1分から60分)の間、 ` ipwatcher `は、その分に15個以上の ` GET +`リクエストを送信すると、IPアドレスをハットボットとして分類します。

最初のシェルに戻り、20リクエストで数ミリ秒でサイトを爆破するページスクレーパーを模倣します。

for _ in range(20):
    r.lpush("ips", "104.174.118.18")

最後に、 `+ ipwatcher +`を保持している2番目のシェルに戻ると、次のような出力が表示されます。

2019-03-11 15:15:43.041363:  saw 104.174.118.18
2019-03-11 15:15:43.042027:  saw 104.174.118.18
2019-03-11 15:15:43.042598:  saw 104.174.118.18
2019-03-11 15:15:43.043143:  saw 104.174.118.18
2019-03-11 15:15:43.043725:  saw 104.174.118.18
2019-03-11 15:15:43.044244:  saw 104.174.118.18
2019-03-11 15:15:43.044760:  saw 104.174.118.18
2019-03-11 15:15:43.045288:  saw 104.174.118.18
2019-03-11 15:15:43.045806:  saw 104.174.118.18
2019-03-11 15:15:43.046318:  saw 104.174.118.18
2019-03-11 15:15:43.046829:  saw 104.174.118.18
2019-03-11 15:15:43.047392:  saw 104.174.118.18
2019-03-11 15:15:43.047966:  saw 104.174.118.18
2019-03-11 15:15:43.048479:  saw 104.174.118.18
Hat bot detected!:  104.174.118.18
Hat bot detected!:  104.174.118.18
Hat bot detected!:  104.174.118.18
Hat bot detected!:  104.174.118.18
Hat bot detected!:  104.174.118.18
Hat bot detected!:  104.174.118.18

[+ key True]ループから[.keys]#Ctrl + C#を抜けると、問題のIPがブラックリストに追加されていることがわかります。

>>>

>>> blacklist
{IPv4Address('104.174.118.18')}

この検出システムの欠陥を見つけることができますか? フィルターは、last 60 seconds(ローリング分)ではなく、 `+ .minute +`として分をチェックします。 ローリングチェックを実装して、過去60秒間にユーザーが何回表示されたかを監視するのは難しいでしょう。 ClassDojoには、Redisの並べ替えセットを使用した巧妙なソリューションがあります。 Josiah Carlsonのhttps://realpython.com/asins/1617290858/[Redis in Action]は、IP-to-locationキャッシュテーブルを使用したこのセクションのより複雑で汎用的な例も示しています。

永続性とスナップショット

Redisが読み取り操作と書き込み操作の両方で非常に高速である理由の1つは、データベースがサーバー上のメモリ(RAM)に保持されていることです。 ただし、Redisデータベースは、https://redis.io/topics/persistence#snapshotting [snapshotting]というプロセスでディスクに保存(永続化)することもできます。 この背後にあるポイントは、サーバーの起動時など、必要なときにデータを再構築してメモリに戻すことができるように、バイナリ形式で物理バックアップを保持することです。

`+ save +`オプションを使用して、このチュートリアルの最初に基本設定をセットアップしたときに、知らずにスナップショットをすでに有効にしています:

#/etc/redis/6379.conf

port              6379
daemonize         yes
save              60 1
bind              127.0.0.1
tcp-keepalive     300
dbfilename        dump.rdb
dir               ./
rdbcompression    yes

形式は `+ save <seconds> <changes> `です。 これは、指定された秒数とデータベースに対する書き込み操作の両方が発生した場合、データベースをディスクに保存するようRedisに指示します。 この場合、60秒ごとに少なくとも1つの変更書き込み操作が発生すると、60秒ごとにデータベースをディスクに保存するようRedisに指示します。 これは、次の3つの「 save +」ディレクティブを使用するhttp://download.redis.io/redis-stable/redis.conf[Redis構成ファイルのサンプル]に対してかなり積極的な設定です。

# Default redis/redis.conf
save 900 1
save 300 10
save 60 10000
  • RDBスナップショット*は、データベースの完全な(増分ではなく)ポイントインタイムキャプチャです。 (RDBはRedisデータベースファイルを指します。)また、書き込まれる結果データファイルのディレクトリとファイル名を指定しました。

#/etc/redis/6379.conf

port              6379
daemonize         yes
save              60 1
bind              127.0.0.1
tcp-keepalive     300
dbfilename        dump.rdb
dir               ./
rdbcompression    yes

これは、 `+ redis-server `が実行された場所の現在の作業ディレクトリにある ` dump.rdb +`というバイナリデータファイルに保存するようにRedisに指示します。

$ file -b dump.rdb
data

また、Redisコマンドhttps://redis.io/commands/bgsave [+ BGSAVE +]を使用して手動で保存を呼び出すこともできます。

127.0.0.1:6379> BGSAVE
Background saving started

「+ BGSAVE 」の「BG」は、バックグラウンドで保存が行われることを示します。 このオプションは ` redis-py +`メソッドでも利用可能です:

>>>

>>> r.lastsave()  # Redis command: LASTSAVE
datetime.datetime(2019, 3, 10, 21, 56, 50)
>>> r.bgsave()
True
>>> r.lastsave()
datetime.datetime(2019, 3, 10, 22, 4, 2)

この例では、別の新しいコマンドとメソッド、 `+ .lastsave()`を紹介します。 Redisでは、最後のDBセーブのUnixタイムスタンプを返します。これはPythonが ` datetime `オブジェクトとして返します。 上記では、 ` r.lastsave()`の結果が ` r.bgsave()+`の結果として変化することがわかります。

`+ save `設定オプションで自動スナップショットを有効にすると、 ` r.lastsave()+`も変更されます。

これをすべて言い換えると、スナップショットを有効にする方法は2つあります。

  1. 明示的に、Redisコマンドの + BGSAVE +`または `+ redis-py +`メソッド `+ .bgsave()+

  2. 暗黙的に、 `+ save `設定オプション( ` redis-py `の ` .config_set()+`で設定することもできます)

RDBスナップショットは、親プロセスがhttp://man7.org/linux/man-pages/man2/fork.2.html [+ fork()+]システムコールを使用して時間のかかる書き込みを渡すため、高速です。ディスクを子プロセスに移動し、親プロセスが途中で続行できるようにします。 これは、 `+ BGSAVE +`の_background_が参照するものです。

https://redis.io/commands/save [+ SAVE +]( + redis-py +`の `+ .save()+)もありますが、これは `を使用するのではなく、同期(ブロック)保存を行います+ fork()+ `なので、特別な理由がない限り使用しないでください。

「+ .bgsave()」はバックグラウンドで発生しますが、コストがかかるわけではありません。 Redisデータベースがそもそも十分に大きい場合、 ` fork()+`自体が発生する時間は実際にはかなり長くなります。

これが懸念される場合、またはRDBスナップショットの定期的な性質により失われたデータの小さなスライスを見逃す余裕がない場合は、https://redis.io/topics/persistence#appendを調べる必要があります。 -only-file [append-only file](AOF)戦略はスナップショットに代わるものです。 AOFは、Redisコマンドをリアルタイムでディスクにコピーし、これらのコマンドを再生することにより、コマンドベースのリテラル再構築を実行できます。

シリアル化の回避策

Redisのデータ構造について話を始めましょう。 ハッシュデータ構造により、Redisは実質的に1レベルの深さのネストをサポートします。

127.0.0.1:6379> hset mykey field1 value1

Pythonクライアントの同等物は次のようになります。

r.hset("mykey", "field1", "value1")

ここでは、 +" field1 ":" value1 "+`がPython dictのキーと値のペアであると考えることができます。+ {"field1": "value1"} + で、 + mykey + `は最上位キー:

Redis Command Pure-Python Equivalent

r.set("key", "value")

r = {"key": "value"}

r.hset("key", "field", "value")

r = {"key": {"field": "value"}}

しかし、この辞書の値(Redisハッシュ)に、文字列以外の何か、たとえば `+ list +`や値として文字列を含むネストされた辞書を含める場合はどうでしょうか?

JSONのようなデータを使用して区別を明確にする例を次に示します。

restaurant_484272 = {
    "name": "Ravagh",
    "type": "Persian",
    "address": {
        "street": {
            "line1": "11 E 30th St",
            "line2": "APT 1",
        },
        "city": "New York",
        "state": "NY",
        "zip": 10016,
    }
}

キー `+ 484272 `と ` restaurant_484272 `のキーと値のペアに対応するフィールドと値のペアを使用してRedisハッシュを設定するとします。 ` restaurant_484272 +`はネストされているため、Redisはこれを直接サポートしていません。

>>>

>>> r.hmset(484272, restaurant_484272)
Traceback (most recent call last):
# ...
redis.exceptions.DataError: Invalid input of type: 'dict'.
Convert to a byte, string or number first.

実際、Redisを使用してこの作業を行うことができます。 `+ redis-py +`とRedisでネストされたデータを模倣する方法は2つあります:

  1. `+ json.dumps()+`のようなもので値を文字列にシリアル化します

  2. キー文字列で区切り文字を使用して、値のネストを模倣します

それぞれの例を見てみましょう。

オプション1:値を文字列にシリアル化する

`+ json.dumps()`を使用して、 ` dict +`をJSON形式の文字列にシリアル化できます。

>>>

>>> import json
>>> r.set(484272, json.dumps(restaurant_484272))
True

`+ .get()`を呼び出すと、返される値は ` bytes `オブジェクトになります。そのため、元のオブジェクトを取得するためにそれをデシリアライズすることを忘れないでください。 ` json.dumps()`と ` json.loads()+`は、それぞれデータのシリアライズとデシリアライズのために、互いに逆です:

>>>

>>> from pprint import pprint
>>> pprint(json.loads(r.get(484272)))
{'address': {'city': 'New York',
             'state': 'NY',
             'street': '11 E 30th St',
             'zip': 10016},
 'name': 'Ravagh',
 'type': 'Persian'}

これは、https://github.com/yaml/pyyaml [+ yaml +]である別の一般的な選択肢で、すべてのシリアル化プロトコルに適用されます。

>>>

>>> import yaml  # python -m pip install PyYAML
>>> yaml.dump(restaurant_484272)
'address: {city: New York, state: NY, street: 11 E 30th St, zip: 10016}\nname: Ravagh\ntype: Persian\n'

どのシリアル化プロトコルを選択しても、概念は同じです。Pythonに固有のオブジェクトを取得し、複数の言語間で認識および交換可能なバイト文字列に変換します。

*オプション2:キー文字列で区切り文字を使用する *

Pythonの `+ dict `で複数レベルのキーを連結することにより、「ネスト」を模倣する2番目のオプションがあります。 これは、ネストされた辞書をhttps://realpython.com/python-thinking-recursively/[recursion]を介して平坦化することで構成されているため、各キーはキーの連結文字列であり、値は元の辞書から最も深くネストされた値になります。 。 辞書オブジェクト ` restaurant_484272 +`を考えてみましょう:

restaurant_484272 = {
    "name": "Ravagh",
    "type": "Persian",
    "address": {
        "street": {
            "line1": "11 E 30th St",
            "line2": "APT 1",
        },
        "city": "New York",
        "state": "NY",
        "zip": 10016,
    }
}

次の形式に変換します。

{
    "484272:name":                     "Ravagh",
    "484272:type":                     "Persian",
    "484272:address:street:line1":     "11 E 30th St",
    "484272:address:street:line2":     "APT 1",
    "484272:address:city":             "New York",
    "484272:address:state":            "NY",
    "484272:address:zip":              "10016",
}

これは、以下の `+ setflat_skeys()`が行うことで、入力ディクショナリのコピーを返すのではなく、 ` Redis `インスタンス自体に ` .set()+`操作を配置する機能が追加されています。

 1 from collections.abc import MutableMapping
 2
 3 def setflat_skeys(
 4     r: redis.Redis,
 5     obj: dict,
 6     prefix: str,
 7     delim: str = ":",
 8    * ,
 9     _autopfix=""
10 ) -> None:
11     """Flatten `obj` and set resulting field-value pairs into `r`.
12
13     Calls `.set()` to write to Redis instance inplace and returns None.
14
15     `prefix` is an optional str that prefixes all keys.
16     `delim` is the delimiter that separates the joined, flattened keys.
17     `_autopfix` is used in recursive calls to created de-nested keys.
18
19     The deepest-nested keys must be str, bytes, float, or int.
20     Otherwise a TypeError is raised.
21     """
22     allowed_vtypes = (str, bytes, float, int)
23     for key, value in obj.items():
24         key = _autopfix + key
25         if isinstance(value, allowed_vtypes):
26             r.set(f"{prefix}{delim}{key}", value)
27         elif isinstance(value, MutableMapping):
28             setflat_skeys(
29                 r, value, prefix, delim, _autopfix=f"{key}{delim}"
30             )
31         else:
32             raise TypeError(f"Unsupported value type: {type(value)}")

この関数は、 `+ obj `のキーと値のペアを反復処理し、最初に値の型(25行目)をチェックして、さらに再帰を停止し、そのキーと値のペアを設定する必要があるように見えます。 そうでなければ、値が ` dict +`のように見える場合(27行目)、そのマッピングに再帰し、以前に見たキーをキープレフィックスとして追加します(28行目)。

職場で見てみましょう:

>>>

>>> r.flushdb()  # Flush database: clear old entries
>>> setflat_skeys(r, restaurant_484272, 484272)

>>> for key in sorted(r.keys("484272*")):  # Filter to this pattern
...     print(f"{repr(key):35}{repr(r.get(key)):15}")
...
b'484272:address:city'             b'New York'
b'484272:address:state'            b'NY'
b'484272:address:street:line1'     b'11 E 30th St'
b'484272:address:street:line2'     b'APT 1'
b'484272:address:zip'              b'10016'
b'484272:name'                     b'Ravagh'
b'484272:type'                     b'Persian'

>>> r.get("484272:address:street:line1")
b'11 E 30th St'

上記の最後のループは、 `+ r.keys(" 484272 ")`を使用します。ここで、 `" 484272 "`はパターンとして解釈され、データベース内の `" 484272 "+`で始まるすべてのキーに一致します。 。

また、プレーン_string:string_フィールドと値のペア、および484272 IDを使用しているため、 `+ setflat_skeys()`が ` .hset()`ではなく ` .set()+`を呼び出す方法にも注意してください。キーは各フィールド文字列の先頭に追加されます。

暗号化

夜よく眠れるようにするもう1つの方法は、Redisサーバーに何かを送信する前に対称暗号化を追加することです。 これは、リンクに適切な値を設定することで、確実に適切に配置する必要があるセキュリティのアドオンであると考えてください:#configuring-redis [Redis configuration]。 以下の例では、https://github.com/pyca/cryptography/[+ cryptography +]パッケージを使用しています。

$ python -m pip install cryptography

説明のために、どんなサーバーでも平文で座りたくない機密カード会員データ(CD)があると仮定します。 Redisにキャッシュする前に、データをシリアル化し、https://cryptography.io/en/latest/fernet/[Fernet]を使用してシリアル化された文字列を暗号化できます。

>>>

>>> import json
>>> from cryptography.fernet import Fernet

>>> cipher = Fernet(Fernet.generate_key())
>>> info = {
...     "cardnum": 2211849528391929,
...     "exp": [2020, 9],
...     "cv2": 842,
... }

>>> r.set(
...     "user:1000",
...     cipher.encrypt(json.dumps(info).encode("utf-8"))
... )

>>> r.get("user:1000")
b'gAAAAABcg8-LfQw9TeFZ1eXbi'  # ... [truncated]

>>> cipher.decrypt(r.get("user:1000"))
b'{"cardnum": 2211849528391929, "exp": [2020, 9], "cv2": 842}'

>>> json.loads(cipher.decrypt(r.get("user:1000")))
{'cardnum': 2211849528391929, 'exp': [2020, 9], 'cv2': 842}

`+ info `には ` list `の値が含まれているため、これをRedisで受け入れられる文字列にシリアル化する必要があります。 (これには、「 json 」、「 yaml 」、またはその他のシリアル化を使用できます。)次に、「 cipher 」オブジェクトを使用してその文字列を暗号化および復号化します。 ` json.loads()`を使用して復号化されたバイトをデシリアライズする必要があります。これにより、初期入力の型である ` dict +`に結果を戻すことができます。

:https://github.com/fernet/spec/blob/master/Spec.md#token-format[Fernet]は、CBCモードでAES 128暗号化を使用します。 AES 256の使用例については、https://cryptography.io/en/latest/hazmat/primitives/symmetric-encryption/[+ cryptography + docs]を参照してください。 どの方法を選択するにしても、「+ pycrypto 」(「 Crypto 」としてインポートされる)ではなく、「 cryptography +」を使用してください。

セキュリティが最重要である場合、ネットワーク接続を経由する前に文字列を暗号化することは決して悪い考えではありません。

圧縮

最後の簡単な最適化は圧縮です。 帯域幅が懸念される場合、またはコストを重視する場合は、Redisとデータを送受信するときにロスレス圧縮および圧縮解除スキームを実装できます。 bzip2圧縮アルゴリズムを使用した例を次に示します。この極端な場合、接続を介して送信されるバイト数が2,000倍以上に削減されます。

>>>

 1 >>> import bz2
 2
 3 >>> blob = "i have a lot to talk about" * 10000
 4 >>> len(blob.encode("utf-8"))
 5 260000
 6
 7 >>> # Set the compressed string as value
 8 >>> r.set("msg:500", bz2.compress(blob.encode("utf-8")))
 9 >>> r.get("msg:500")
10 b'BZh91AY&SY\xdaM\x1eu\x01\x11o\x91\[email protected]\x002l\x87\'  # ... [truncated]
11 >>> len(r.get("msg:500"))
12 122
13 >>> 260_000/122  # Magnitude of savings
14 2131.1475409836066
15
16 >>> # Get and decompress the value, then confirm it's equal to the original
17 >>> rblob = bz2.decompress(r.get("msg:500")).decode("utf-8")
18 >>> rblob == blob
19 True

ここでのシリアル化、暗号化、および圧縮の関係は、すべてクライアント側で発生することです。 文字列をサーバーに送信すると、Redisをより効率的に使用するクライアント側の元のオブジェクトに対して何らかの操作を行います。 最初にサーバーに送信したものを要求すると、クライアント側で逆の操作が再び発生します。

Hiredisを使用する

`+ redis-py `のようなクライアントライブラリは、ビルド方法が*プロトコル*に従うことが一般的です。 この場合、 ` redis-py +`はhttps://redis.io/topics/protocol[REdis Serialization Protocol]またはRESPを実装します。

このプロトコルを実現するためには、Pythonオブジェクトを生のバイト文字列に変換し、Redisサーバーに送信し、レスポンスを解析してわかりやすいPythonオブジェクトに戻すことが必要です。

たとえば、文字列応答「OK」は `" + OK \ r \ n "`として返され、整数応答1000は `":1000 \ r \ n "`として返されます。 これは、https://redis.io/topics/protocol#resp-arrays [RESP配列]などの他のデータ型ではより複雑になる可能性があります。

*パーサー*は、この生の応答を解釈し、クライアントが認識できるものに作り上げる要求応答サイクルのツールです。 + redis-py +`には独自のパーサークラスである `+ PythonParser +`が付属しており、純粋なPythonで解析を行います。 (興味がある場合は、https://github.com/andymccurdy/redis-py/blob/cfa2bc9/redis/connection.py#L289 [+ .read_response()+`]を参照してください。)

ただし、Cライブラリ(https://github.com/redis/hiredis[Hiredis])もあります。これには、「+ LRANGE +」などの一部のRedisコマンドの大幅な高速化を提供できる高速パーサーが含まれています。 Hiredisはオプションのアクセラレータと見なすことができ、ニッチなケースでは問題ありません。

`+ redis-py `でHiredisパーサーを使用できるようにするために必要なことは、 ` redis-py +`と同じ環境にPythonバインディングをインストールすることだけです。

$ python -m pip install hiredis

ここに実際にインストールしているのはhttps://github.com/redis/hiredis-py [+ hiredis-py +]で、これはhttps://github.com/redis/の一部のPythonラッパーですhiredis [+ hiredis +] Cライブラリ。

良い点は、自分で `+ hiredis `を呼び出す必要がないことです。 ただ ` pip install `し、これにより ` redis-py `が利用可能であることを確認し、 ` PythonParser `の代わりに ` HiredisParser +`を使用します。

内部的には、 `+ redis-py `は ` hiredis `のインポートを試み、 ` HiredisParser `クラスを使用してそれに一致しますが、代わりに ` PythonParser +`にフォールバックします。

# redis/utils.py
try:
    import hiredis
    HIREDIS_AVAILABLE = True
except ImportError:
    HIREDIS_AVAILABLE = False


# redis/connection.py
if HIREDIS_AVAILABLE:
    DefaultParser = HiredisParser
else:
    DefaultParser = PythonParser

エンタープライズRedisアプリケーションの使用

Redis自体はオープンソースで無料ですが、Redisをコアとするデータストアと、オープンソースのRedisサーバー上に構築されたいくつかの追加機能を提供するいくつかのマネージドサービスが生まれました。

  • *Redis用Amazon ElastiCache * これは、Redisサーバーをクラウド。AmazonEC2インスタンスから接続できます。 完全なセットアップ手順については、Amazonのhttps://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/GettingStarted.CreateCluster.html[ElastiCache for Redis]起動ページをご覧ください。

  • MicrosoftのRedis用Azureキャッシュ これは、カスタマイズ可能な安全なRedisをセットアップできる別のエンタープライズグレードサービスです。クラウド内のインスタンス。

2つのデザインにはいくつかの共通点があります。 通常、キャッシュのカスタム名を指定します。これは、 + demo.abcdef.xz.0009.use1.cache.amazonaws.com +(AWS)や `+ demo.redisなどのDNS名の一部として埋め込まれます。 cache.windows.net + `(Azure)。

設定が完了したら、接続方法に関する簡単なヒントをいくつか紹介します。

コマンドラインからは、前の例とほぼ同じですが、デフォルトのローカルホストを使用するのではなく、 `+ h +`フラグでホストを指定する必要があります。 Amazon AWS の場合、インスタンスシェルから次を実行します。

$ export REDIS_ENDPOINT="demo.abcdef.xz.0009.use1.cache.amazonaws.com"
$ redis-cli -h $REDIS_ENDPOINT
*Microsoft Azure* の場合、同様の呼び出しを使用できます。 Azureキャッシュfor Redis https://docs.microsoft.com/en-us/azure/azure-cache-for-redis/cache-how-to-redis-cli-tool[SSLを使用](ポート6380)ではなく、デフォルトでポート6379よりも、TCPとは言えないRedisとの暗号化通信が可能です。 さらに指定する必要があるのは、デフォルト以外のポートとアクセスキーのみです。
$ export REDIS_ENDPOINT="demo.redis.cache.windows.net"
$ redis-cli -h $REDIS_ENDPOINT -p 6380 -a <primary-access-key>

+ -h +`フラグはホストを指定します。これはあなたが見たようにデフォルトでは `+ 127.0.0.1 +(localhost)です。

Pythonで `+ redis-py +`を使用している場合は、常にPythonスクリプト自体に機密変数を含めないようにし、これらのファイルに許可する読み取りおよび書き込み権限に注意することをお勧めします。 Pythonバージョンは次のようになります。

>>>

>>> import os
>>> import redis

>>> # Specify a DNS endpoint instead of the default localhost
>>> os.environ["REDIS_ENDPOINT"]
'demo.abcdef.xz.0009.use1.cache.amazonaws.com'
>>> r = redis.Redis(host=os.environ["REDIS_ENDPOINT"])

それだけです。 別の `+ host `を指定する以外に、 ` r.get()+`などのコマンド関連のメソッドを通常どおり呼び出すことができます。

: `+ redis-py `とAWSまたはAzure Redisインスタンスの組み合わせのみを使用する場合は、Redisをローカルにインストールしてローカルに作成する必要はありません。 ` redis-cli `または ` redis-server +`が必要です。

Redisが重要な役割を果たす中規模から大規模の本番アプリケーションをデプロイする場合、AWSまたはAzureのサービスソリューションを使用することは、スケーラブルで費用対効果が高く、セキュリティを重視した運用方法です。

まとめ

これで、Redisサーバーに接続されたRedis REPLをインストールして使用し、実際の例で `+ redis-py +`を使用するなど、Pythonを介してRedisにアクセスする旋風ツアーを終了します。 学んだことは次のとおりです。

  • `+ redis-py +`を使用すると、直感的なPython APIを介してRedis CLIで実行できる(ほぼ)すべてを実行できます。

  • 永続性、シリアル化、暗号化、圧縮などのトピックをマスターすると、Redisを最大限に活用できます。

  • Redisのトランザクションとパイプラインは、より複雑な状況ではライブラリの重要な部分です。

  • エンタープライズレベルのRedisサービスは、実稼働環境でRedisをスムーズに使用するのに役立ちます。

Redisには、https://redis.io/commands/eval [server-side Lua scripting]、https://redis.io/topicsなど、ここでは説明しきれなかった機能の広範なセットがあります。/partitioning [sharding]、およびhttps://redis.io/topics/replication[master-slave replication]。 Redisがあなたの路地にいると思うなら、それがhttp://antirez.com/news/125 [更新されたプロトコル、RESP3]を実装するので、開発に必ず従ってください。

参考文献

詳細については、こちらをご覧ください。

本:

使用中のRedis:

その他