Redisの動きを確認してみた
はじめに
こんにちは、SHIFT の開発部門に所属しているKatayamaです。今期から転属になり、開発を担当していくことになりました。
現在、基本的な事から学ぶ研修中です。開発部門では新しく学ぶことがたくさんあり、それらを自身の振り返りアウトプットとして発信していけたらと思います。記事が溜まったら、noteのマガジンにもまとめる予定です。
今回はRedisの動き(多重アクセスとトランザクション)について学んだことをまとめてみたいと思います。
Redisの特徴をつかむために色々試してみる
Redisの準備
docker-composeで作成する。
version: "3.9"
services:
redis:
image: redis:6.2.5-alpine3.14
container_name: redis
environment:
TZ: Asia/Tokyo
ports:
- "6379:6379"
volumes:
- "./data/redis:/data"
作成したRedisサーバへはredis-cliでアクセスできるが、RedisサーバをLinux環境にinstallせずにCLIを使うには、How to Get Redis-cli Without Installing Redis Server (even on Windows)に書かれている手順でできる。
※ただし、今回はdockerなのでわざわざ↑を入れずとも、
docker container exec -it {コンテナ名} redis-cli get {key}
でコマンドは実行できる。
・参考:docker hub redis
雑なアクセスカウンタを作成してインクリメントする
今回はあえて雑なアクセスカウンタという事で、get()してset()するという実装でインクリメントを実装した。この実行結果をテストするコードも合わせて作成した。それぞれのソースコードは以下。
// src/index.js
const incrementRds = async (client) => {
try {
const id = await client.get('id');
return await client.set('id', Number(id) + 1);
} catch (error) {
return { msg: error.message };
}
};
const getIdRd = async (client) => {
try {
return await client.get('id');
} catch (error) {
return error.message;
}
};
// step3.test.js
import Redis from 'ioredis';
import { getIdRd, incrementRds } from '../src/index';
describe('雑なアクセスカウンタを作成してインクリメントする', () => {
let expId;
let redis;
beforeAll(async () => {
redis = new Redis();
});
afterAll(async () => {
redis.disconnect();
});
describe('Set Up', () => {
test('generate expect id', async () => {
const idRd = await getIdRd(redis);
expId = Number(idRd) + 1;
});
});
describe('Test Block', () => {
test('insert data with transaction', async () => {
const res = await incrementRds(redis);
expect(res).toBe('OK');
});
test('confirm result', async () => {
const idRd = await getIdRd(redis);
expect(idRd).toBe(expId.toString());
});
});
});
試しに実行した結果は以下のようになり、これはうまく動いている。
# yarn test
yarn run v1.22.15
$ npx jest --maxWorkers=1 step3.test.js
PASS tests/step3.test.js
省略
Test Suites: 1 passed, 1 total
※const client = new Redis();
Connect to Redisに書かれている通り、デフォルトではlocalhost:6379に接続しに行くみたいなので、今回は何もオプションを設定する必要はない
・参考:Clients Node.js
雑なアクセスカウンタを10000回実行して多重アクセスが期待通りにならない事を確認
インクリメントをする部分は雑なアクセスカウンタを作成してインクリメントすると同じで、多重アクセスを実行するためのテストコードを新規で作成した。
// step4.test.js
// 省略
describe('雑なアクセスカウンタを10000回実行して期待通りにならない事を確認', () => {
// 省略
describe('Test Block', () => {
test('insert data with transaction', async () => {
for (let index = 0; index < 10000; index += 1) {
// eslint-disable-next-line no-await-in-loop
const res = await incrementRds(redis);
expect(res).toBe('OK');
}
});
});
});
多重アクセスなので2つのターミナル上で上記のテストを実行してみると、、、
## ターミナルA
# yarn test
yarn run v1.22.15
$ npx jest --maxWorkers=1 step4.test.js
PASS tests/step4.test.js (32.064 s)
省略
## ターミナルBの実行が終わったのを確認してから・・・
# docker container exec -it redis redis-cli get id
"10297"
## ターミナルB
# yarn test
yarn run v1.22.15
$ npx jest --maxWorkers=1 step4.test.js
PASS tests/step4.test.js (33.345 s)
省略
という結果の通り、トランザクションがちゃんと機能していないので期待値と一致しなかった。これは途中でターミナルAが値を読みに行っている時に、ターミナルBも同様に値を見に行き、ターミナルAのインクリメントした値をターミナルBが読み込むのではなく、ターミナルBはインクリメント前の値をインクリメントしてそれをset()するため、本来+2されるべきが+1になるために起きている。
※この動きはMySQLの行ロックの動きを確認してみた#雑なアクセスカウンタを1000回実行して多重アクセスが期待通りにならない事を確認と同じような動きなのでそちらも参照。
INCRを使って10000回インクリメントを実行して多重アクセスが期待通りにならない事を確認
今度はINCR keyを使ってインクリメントするようにした時にどうなるか?を見てみる。多重アクセスのコードは上記と変わらない。
const incrementByIncr = async (client) => {
try {
return await client.incr('id');
} catch (error) {
return { msg: error.message };
}
};
同じように多重アクセスをさせるために、2つのターミナル上で上記のテストを実行してみると、、、
## ターミナルA
# yarn test
yarn run v1.22.15
$ npx jest --maxWorkers=1 step4.test.js
PASS tests/step4.test.js (17.356 s)
省略
## ターミナルBの実行が終わったのを確認してから・・・
# docker container exec -it redis redis-cli get id
"20000"
## ターミナルB
# yarn test
yarn run v1.22.15
$ npx jest --maxWorkers=1 step4.test.js
PASS tests/step4.test.js (17.41 s)
省略
という結果の通り、トランザクションが機能して期待値と同じ結果が得られた。これはThe counter pattern is the most obvious thing you can do with Redis atomic increment operations.と書かれているように、INCRもatomicな性質を持つコマンドとして実装されているため。つまり、ターミナルAでインクリメントを実行している時に、ターミナルBもインクリメントを実行するみたいなことが発生する事はなく、これにより期待通りの結果が得られた。
・参考:入門 : REDIS のデータ構造と概念
・参考:Either all of the commands or none are processed, so a Redis transaction is also atomic.
まとめとして
多重アクセスの制御(排他制御)は基本的な部分だがここを適切に理解して設計・実装しないと思わぬ動きになってしまう事が体感できた。今後もRedisのトランザクションの仕組みについてなど、追加で学習していきたい。
おまけ
Redisの処理速度
Redisの処理速度を見てみると、、、
describe('Test Block', () => {
test('insert data with transaction', async () => {
console.time('loop time');
for (let index = 0; index < 10000; index += 1) {
// eslint-disable-next-line no-await-in-loop
await incrementByIncr(redis);
}
console.timeEnd('loop time');
});
});
# yarn test
yarn run v1.22.15
$ npx jest --maxWorkers=1 step5.test.js
console.time
loop time: 4719 ms
at Object.<anonymous> (tests/step5.test.js:25:12)
at runMicrotasks (<anonymous>)
PASS tests/step5.test.js (6.13 s)
省略
※パフォーマンスを測定する方法としては、[【 time 】コマンド(外部コマンド)]https://atmarkit.itmedia.co.jp/ait/articles/1810/25/news022.html)などを使うこともできる。
・参考:Console.time()
__________________________________
お問合せはお気軽に
https://service.shiftinc.jp/contact/
SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/
SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/
SHIFTの導入事例
https://service.shiftinc.jp/case/
お役立ち資料はこちら
https://service.shiftinc.jp/resources/
SHIFTの採用情報はこちら
https://recruit.shiftinc.jp/career/