PythonでRedisを使用する方法

PythonでRedisを使用する方法

このチュートリアルでは、PythonをRedis(RED-iss、またはREE-dissまたはRed-DEESと発音します)で使用する方法を学習します。これは非常に高速です。 AからZまでのすべてに使用できるメモリキー値ストア。 データベースで人気のある本であるSeven Databases in Seven WeeksがRedisについて言っていることは次のとおりです。

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

そして、スピードに関して言えば、Redisは他の追随を許しません。 一部のベンチマークでは、読み取りは高速で、書き込みはさらに高速で、1秒あたり100,000SET以上の操作を処理します。 (Source

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

redis-pyredisとしてインポートする)は、Redis用の多くのPythonクライアントの1つですが、Redis開発者自身によって“currently the way to go for Python”として請求されるという特徴があります。 PythonからRedisコマンドを呼び出して、見慣れたPythonオブジェクトを返すことができます。

In this tutorial, you’ll cover

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

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

  • redis-pyをマスターしながら、Redisのプロトコルをどのように実装するかを垣間見る

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

Free Bonus:Click here to get access to a chapter from Python Tricks: The Bookは、Pythonのベストプラクティスと、より美しい+ Pythonicコードをすぐに適用できる簡単な例を示しています。

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

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

Note:このセクションは、Mac OSXまたはLinuxへのインストールを対象としています。 Windowsを使用している場合は、WindowsサービスとしてインストールできるRedisのMicrosoftforkがあります。 プログラムとしての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で記述されているため、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は実際には、/usr/local/bin/に配置される少数の異なる実行可能ファイル(および1つのシンボリックリンク)につながります。

$ # 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*

これらはすべて使用目的がありますが、おそらく最も気になるのはredis-cliredis-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の構成は自己文書化されており、sample redis.conf fileはRedisソースにあります。 実稼働システムでRedisを使用している場合は、すべての注意散漫をブロックし、このサンプルファイルを完全に読んでRedisの詳細を理解し、セットアップを微調整するのに時間がかかります。

Redisのドキュメントの一部を含む一部のチュートリアルでは、redis/utils/install_server.shにあるシェルスクリプトinstall_server.shの実行を提案する場合もあります。 上記のより包括的な代替手段としてこれを実行することを歓迎しますが、install_server.shに関するいくつかの細かい点に注意してください。

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

  • 構成オプションのより完全なセットを/etc/redis/6379.confに挿入します。

  • System Vinit script/etc/init.d/redis_6379に書き込み、sudo service redis_6379 startを実行できるようにします。

Redisクイックスタートガイドにはmore proper Redis setupに関するセクションも含まれていますが、このチュートリアルと開始には、上記の構成オプションで十分です。

Security Note:数年前、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にはclient-server architectureがあり、request-response modelを使用します。 これは、ユーザー(クライアント)がデフォルトでポート6379でTCP接続を介してRedisサーバーに接続することを意味します。 何らかのアクション(何らかの形式の読み取り、書き込み、取得、設定、更新など)を要求すると、サーバーは応答を返します。

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

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

ただし、最初に、redis-serverを起動して、実行中のRedisサーバーと通信できるようにする必要があります。 開発でこれを行う一般的な方法は、サーバーをlocalhost(IPv4アドレス127.0.0.1)で起動することです。これは、Redisに特に指示しない限り、デフォルトです。 構成ファイルの名前を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に設定して、サーバーがバックグラウンドで実行されるようにします。 (それ以外の場合は、redis-serverのオプションとして--daemonize yesを使用します。)

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

127.0.0.1:6379>

これは、最も単純なRedisコマンドの1つであるPINGです。これは、サーバーへの接続をテストし、問題がなければ"PONG"を返します。

127.0.0.1:6379> PING
PONG

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

Note:別の健全性チェックとして、pgrepを使用してRedisサーバーのプロセスIDを検索できます。

$ pgrep redis-server
26983

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

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

Python辞書としてのRedis

RedisはRemote Dictionary Serviceの略です。

「つまり、Pythondictionaryのように?」あなたは尋ねるかもしれません。

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

  • Redisデータベースはkey:valueペアを保持し、GETSETDELなどのコマンド、およびseveral hundred追加コマンドをサポートします。

  • Rediskeysは常に文字列です。

  • Redisvaluesは、さまざまなデータ型である可能性があります。 このチュートリアルでは、stringlisthashes、およびsetsなどのより重要な値のデータ型について説明します。 一部の高度なタイプには、geospatial itemsタイプと新しいstreamタイプが含まれます。

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

Redisの作成者であるSalvatoreSanfilippoは、RedisデータベースとプレーンバニラのPythondictとの比較をおそらく気に入らないでしょう。 彼は、プロジェクトを「データ構造サーバー」(memcachedなどのKey-Valueストアではなく)と呼んでいます。これは、Redisがstring:string以外のkey:valueデータ型の追加タイプの保存をサポートしているためです。 )s。 しかし、ここでの目的のために、Pythonの辞書オブジェクトに精通している場合、これは便利な比較になります。

飛び入り、例で学びましょう。 最初のおもちゃデータベース(ID 0)は、country:capital cityのマッピングになります。ここでは、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のNoneに類似しています。

Redisでは、1つのコマンドで複数のキーと値のペア(それぞれMSETMGET)を設定および取得することもできます。

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番目の例として、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の機能の要約です。

この記事ですぐに説明するPythonRedisクライアントライブラリredis-pyは、動作が異なります。 これは、Redisサーバーへの実際のTCP接続をカプセル化し、REdis Serialization Protocol(RESP)を使用してシリアル化されたバイトとしてrawコマンドをサーバーに送信します。 次に、生の応答を受け取り、それを解析して、bytesint、さらにはdatetime.datetimeなどのPythonオブジェクトに戻します。

Note:これまで、インタラクティブなredis-cliREPLを介してRedisサーバーと通信してきました。 スクリプトの名前をpython myscript.pyなどのpython実行可能ファイルに渡すのと同じ方法で、issue commands directlyを実行することもできます。

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

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

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

hashは、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レベルの深さでネストされたPythondictにほぼ類似しています。

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

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

基本的なstring:stringのキーと値のペアにMSETがあるのと同様に、ハッシュ値オブジェクトに複数のペアを設定するためのハッシュ用の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で行われるように各ネストされたペアを設定するのではなく、HMSETを使用することは、上記のネストされたディクショナリにdataを割り当てた方法とおそらく似ています。

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

  • Hashes:ハッシュを操作するコマンドは、HSETHGETHMSETなどのHで始まります。

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

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

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

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

Type コマンド

Sets

SADDSCARDSDIFFSDIFFSTORESINTERSINTERSTORESISMEMBERSMEMBERSSMOVESPOPSRANDMEMBERSREMSSCANSUNIONSUNIONSTORE

ハッシュ

HDELHEXISTSHGETHGETALLHINCRBYHINCRBYFLOATHKEYSHLENHMGETHMSETHSCANHSETHSETNXHSTRLENHVALS

リスト

BLPOPBRPOPBRPOPLPUSHLINDEXLINSERTLLENLPOPLPUSHLPUSHXLRANGELREMLSETLTRIMRPOPRPOPLPUSHRPUSHRPUSHX

文字列

APPENDBITCOUNTBITFIELDBITOPBITPOSDECRDECRBYGETGETBITGETRANGEGETSETINCRINCRBYINCRBYFLOATMGETMSETMSETNXPSETEXSETSETBITSETEXSETNXSETRANGESTRLEN

この表は、Redisのコマンドとタイプの完全な図ではありません。 geospatial itemssorted setsHyperLogLogなどのより高度なデータ型のスモーガスボードがあります。 Rediscommandsページでは、データ構造グループでフィルタリングできます。 data types summaryintroduction to Redis data typesもあります。

Pythonでの作業に切り替えるので、FLUSHDBを使用しておもちゃのデータベースをクリアし、redis-cliREPLを終了できます。

127.0.0.1:6379> FLUSHDB
OK
127.0.0.1:6379> QUIT

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

redis-pyの使用:PythonでのRedis

Redisの基本をいくつかマスターしたので、次はredis-pyにジャンプします。これは、ユーザーフレンドリーなPythonAPIからRedisと通信できるPythonクライアントです。

最初のステップ

redis-pyは、Python呼び出しを介してRedisサーバーと直接通信できるようにする定評のあるPythonクライアントライブラリです。

$ python -m pip install redis

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

それでは、Python中心の部分に取り掛かりましょう。 これがredis-pyの「HelloWorld」です。

>>>

 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')は、strではなくPythonのbytesタイプであることに注意してください。 redis-py全体で最も一般的な戻り値の型はstrではなくbytesであるため、返された値を実際に処理する方法によっては、r.get("Bahamas").decode("utf-8")を呼び出す必要がある場合がありますバイト文字列。

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

これは、HGETALLr.hgetall()になり、PINGr.ping()になることも意味します。 fewの例外がありますが、このルールは大多数のコマンドに当てはまります。

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

引数なしでRedisインスタンスrを構築しましたが、必要に応じていくつかのparametersがバンドルされています。

# 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では、bytesstrint、または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()(ハッシュマルチセット)を使用して、辞書ごとに呼び出します。 「multi」は、複数のフィールドと値のペアを設定するための参照です。この場合の「field」は、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>>
 6 Pipeline>>
 7 Pipeline>>
 8 [True, True, True]
 9
10 >>> r.bgsave()
11 True

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

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

Note:Redisドキュメントはredis-cliでこれと同じことを行うexampleを提供します。ここでは、ローカルファイルの内容をパイプして大量挿入を行うことができます。

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

NoteHINCRBYは引き続き文字列であるハッシュ値を操作しますが、操作を実行するために文字列を基数10の64ビット符号付き整数として解釈しようとします。

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

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

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

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

  • Step 3:最初の2つのステップ(race condition)の間にインベントリを変更する変更に注意してください。

ステップ1は比較的簡単です。利用可能な数量を確認するための.hget()で構成されます。

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

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

redis-pyでは、Pipelineはデフォルトでtransactional 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のジレンマに対する巧妙な答えがあります。これはoptimistic lockingと呼ばれ、PostgreSQLなどのRDBMSでの一般的なロックの動作とは異なります。 楽観的ロックとは、一言で言えば、呼び出し元の関数(クライアント)がロックを取得せず、during the time it would have held a lockに書き込んでいるデータの変更を監視することを意味します。 その間に競合が発生した場合、呼び出し関数は単にプロセス全体を再試行します。

check-and-set動作を提供するWATCHコマンド(redis-py.watch())を使用して、楽観的ロックを実行できます。

大きなコードの塊を導入し、その後、ステップごとに見ていきましょう。 ユーザーが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-pyWatchErrorを発生させます(30行目)。 つまり、itemidが指すハッシュのいずれかが、.hget()呼び出しの後、20行目と21行目で後続の.hincrby()呼び出しの前に変更された場合、全体を再実行します。結果として、while Trueループの別の反復で処理します。

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

ここで重要なのは、client-side操作とserver-side操作の違いを理解することです。

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

このPython割り当ては、r.hget()のクライアント側の結果をもたらします。 逆に、pipeで呼び出すメソッドは、すべてのコマンドを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)の結果をすぐにuseできないことを意味します。 この時点では、サーバーに何も尋ねていません。 通常、.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であることに注意してください。 3つの購入を模倣してみましょう。これにより、quantityフィールドとnpurchasedフィールドが変更されます。

>>>

>>> 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 "", line 1, in 
  File "", line 20, in buyitem
__main__.OutOfStockError: Sorry, hat:56854717 is out of stock!

在庫がありそうです。

キー有効期限の使用

Redisのもう1つの際立った機能であるkey expiryを紹介しましょう。 キーを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メソッドのdocstringから直接取得されます。

署名 目的

r.setex(name, time, value)

キーnameの値をtime秒で期限切れになるvalueに設定します。ここで、timeintまたはPythontimedeltaで表すことができます。オブジェクト

r.psetex(name, time_ms, value)

キーnameの値をtime_msミリ秒で期限切れになるvalueに設定します。ここで、time_msintまたはPythontimedeltaで表すことができます。オブジェクト

r.expire(name, time)

キーnametime秒間有効期限フラグを設定します。ここで、timeintまたはPythontimedeltaオブジェクトで表すことができます。

r.expireat(name, when)

キーnameに有効期限フラグを設定します。whenは、Unix時間またはPythondatetimeオブジェクトを示すintとして表すことができます。

r.persist(name)

nameの有効期限を削除します

r.pexpire(name, time)

キーnametimeミリ秒の間、期限切れフラグを設定します。timeは、intまたはPythontimedeltaオブジェクトで表すことができます。

r.pexpireat(name, when)

キーnameに有効期限フラグを設定します。whenは、Unix時間をミリ秒単位で表すint(Unix時間* 1000)またはPythondatetimeオブジェクトとして表すことができます。

r.pttl(name)

キーnameが期限切れになるまでのミリ秒数を返します

r.ttl(name)

キーnameが期限切れになるまでの秒数を返します

PyHats.com, Part 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ループ内にあり、ipsで左ポップBLPOPの呼び出しをブロックします。 )sリスト、各アドレスの処理:

 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)

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

ipwatcherconsumerのように機能し、周りに座って、新しいIPが"ips"Redisリストにプッシュされるのを待ちます。 それらをb” 51.218.112.236”などのbytesとして受け取り、ipaddressモジュールを使用してより適切なaddress objectに変換します。

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を超えて表示されている場合は、次のtulip bubbleを作成しようとしているPyHats.comWebスクレイパーが手元にあるように見えます。 残念ながら、このユーザーに恐ろしい403ステータスコードのようなものを返すしかありません。

ipwatcher.expire(addrts, 60)を使用して、最後に表示されたときから60秒後に(address minute)の組み合わせを期限切れにします。 これは、データベースが古い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の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

これで、[。keys]#Ctrl [.kbd .key-c]#C ##が `+ while True`ループから抜け出し、問題のIPがブラックリストに追加されたことがわかります。

>>>

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

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

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

Redisが読み取り操作と書き込み操作の両方で非常に高速である理由の1つは、データベースがサーバー上のメモリ(RAM)に保持されていることです。 ただし、Redisデータベースは、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ディレクティブを使用するsample Redis config fileに対して、かなり積極的な設定です。

# Default redis/redis.conf
save 900 1
save 300 10
save 60 10000

RDB snapshotは、データベースの(増分ではなく)完全なポイントインタイムキャプチャです。 (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コマンド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.bgsave()の結果としてr.lastsave()の結果が変化することがわかります。

save構成オプションを使用して自動スナップショットを有効にすると、r.lastsave()も変更されます。

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

  1. 明示的に、RedisコマンドBGSAVEまたはredis-pyメソッド.bgsave()を介して

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

親プロセスがfork()システムコールを使用して、時間のかかるディスクへの書き込みを子プロセスに渡し、親プロセスがその途中で続行できるため、RDBスナップショットは高速です。 これは、BGSAVEbackgroundが参照するものです。

SAVEredis-py.save())もありますが、これはfork()を使用するのではなく、同期(ブロッキング)保存を行うため、特別な理由なしに使用しないでください。 。

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

これが懸念される場合、またはRDBスナップショットの定期的な性質のために失われたデータのごく一部を見逃す余裕がない場合は、代替手段である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コマンド 純粋なPython相当

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,
    }
}

キー484272restaurant_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'}

これはすべてのシリアル化プロトコルに適用され、別の一般的な選択肢は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:キー文字列で区切り文字を使用する

Pythondictで複数レベルのキーを連結することにより、「ネスト」を模倣する2番目のオプションがあります。 これは、ネストされたディクショナリを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サーバーに何かを送信する前に対称暗号化を追加することです。 これは、Redis configurationに適切な値を設定して、適切に配置されていることを確認する必要があるセキュリティへのアドオンと見なしてください。 以下の例では、cryptographyパッケージを使用しています。

$ python -m pip install cryptography

説明のために、どんなサーバーでも平文で座りたくない機密カード会員データ(CD)があると仮定します。 Redisにキャッシュする前に、データをシリアル化してから、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で受け入れ可能な文字列にシリアル化する必要があります。 (これには、jsonyaml、またはその他のシリアル化を使用できます。)次に、cipherオブジェクトを使用してその文字列を暗号化および復号化します。 結果を最初の入力のタイプであるdictに戻すことができるように、json.loads()を使用して復号化されたバイトを逆シリアル化する必要があります。

NoteFernetは、CBCモードでAES128暗号化を使用します。 AES 256の使用例については、cryptography docsを参照してください。 何を選択する場合でも、pycryptoCryptoとしてインポート)ではなく、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などのクライアントライブラリは、構築方法でprotocolに従うのが一般的です。 この場合、redis-pyREdis Serialization ProtocolまたはRESPを実装します。

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

たとえば、文字列応答「OK」は"+OK "として返されますが、整数応答1000は":1000 "として返されます。 これは、RESP arraysなどの他のデータ型ではより複雑になる可能性があります。

parserは、この生の応答を解釈し、クライアントが認識できるものに作成する要求/応答サイクルのツールです。 redis-pyには、純粋なPythonで解析を行う独自のパーサークラスPythonParserが付属しています。 (興味がある場合は、.read_response()を参照してください。)

ただし、LRANGEなどの一部のRedisコマンドを大幅に高速化できる高速パーサーを含むCライブラリHiredisもあります。 Hiredisはオプションのアクセラレータと見なすことができ、ニッチなケースでは問題ありません。

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

$ python -m pip install hiredis

ここで実際にインストールしているのはhiredis-pyです。これは、hiredisCライブラリの一部のPythonラッパーです。

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

内部的には、redis-pyhiredisのインポートを試み、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サーバー上に構築されたいくつかの追加機能を提供するいくつかのマネージドサービスが生まれました。

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

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

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

$ export REDIS_ENDPOINT="demo.abcdef.xz.0009.use1.cache.amazonaws.com"
$ redis-cli -h $REDIS_ENDPOINT

Microsoft Azureの場合、同様の呼び出しを使用できます。 デフォルトでは、ポート6379ではなくRedisuses SSL(ポート6380)用のAzure Cacheにより、TCPとは言えないRedisとの間の暗号化された通信が可能になります。 さらに指定する必要があるのは、デフォルト以外のポートとアクセスキーのみです。

$ export REDIS_ENDPOINT="demo.redis.cache.windows.net"
$ redis-cli -h $REDIS_ENDPOINT -p 6380 -a 

-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()などのコマンド関連のメソッドを呼び出すことができるようになりました。

Noteredis-pyとAWSまたはAzure Redisインスタンスの組み合わせのみを使用する場合は、Redis自体をマシンにローカルにインストールして作成する必要はありません。 tにはredis-cliまたはredis-serverのいずれかが必要です。

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

まとめ

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

  • redis-pyを使用すると、直感的なPythonAPIを介してRedisCLIで実行できる(ほぼ)すべてを実行できます。

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

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

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

Redisには広範な機能セットがあり、server-side Lua scriptingshardingmaster-slave replicationなど、ここでは実際には取り上げなかった機能もあります。 Redisがあなたの路地にあると思う場合は、updated protocol, RESP3を実装しているので、必ず開発に従ってください。

参考文献

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

本:

使用中のRedis:

その他