見出し画像

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()


Katayama Yutaの記事

__________________________________

執筆者プロフィール:Katayama Yuta
SaaS ERPパッケージベンダーにて開発を2年経験。 SHIFTでは、GUIテストの自動化やUnitテストの実装などテスト関係の案件に従事したり、DevOpsの一環でCICD導入支援をする案件にも従事。 最近開発部門へ移動し、再び開発エンジニアに。座学で読み物を読むより、色々手を動かして試したり学んだりするのが好きなタイプ。

お問合せはお気軽に
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/