동시성-redis-분산락

00_8_3·2023년 1월 4일
0

redis & express 구성

깃헙링크 : https://github.com/ehgks0000/redis-lock-example.git

  • redis cluster 모드로 3 마스터, 3 슬레이브

  • express 1

    실행방법 : docker-compose up --build

import express, { NextFunction, Request, Response } from "express";
import Client from "ioredis";
import Redlock, { ResourceLockedError, ExecutionError } from "redlock";

const main = () => {
  const app = express();

  // Check this, https://stackoverflow.com/questions/64374113/replyerror-moved-error-after-connecting-to-redis-cluster-aws
  const cluster = new Client.Cluster([
    { host: "redis-node1", port: 6300 },
    { host: "redis-node2", port: 6301 },
    { host: "redis-node3", port: 6302 },
    { host: "redis-node4", port: 6303 },
    { host: "redis-node5", port: 6304 },
    { host: "redis-node6", port: 6305 },
  ]);

  cluster.once("connect", () => {
    console.log("connected");
  });

  cluster.on("reconnecting", () => {
    console.log("connected");
  });

  cluster.on("error", (e) => {
    console.log("error: ", e);
  });

  const redlock = new Redlock([cluster], {
    driftFactor: 0.01, // multiplied by lock ttl to determine drift time
    retryCount: 10,
    retryDelay: 2000, // time in ms
    retryJitter: 200, // time in ms
    automaticExtensionThreshold: 500, // time in ms
  });

  redlock.on("error", (e) => {
    if (e instanceof ResourceLockedError) {
      console.log("ResourceLockedError :", e);
      return;
    }
    if (e instanceof ExecutionError) {
      console.log("ExecutionError :", e);
      return;
    }
  });

  app.get("/ping", async (req, res) => {
    console.log("pong");
    
    res.json({ msg: "pong"});
  });

  app.get("/set", async (req, res) => {
    try {
      console.log("test set");
      const a = await cluster.get("a");
      console.log("a :", a);
      if(!a){
        await cluster.set("a", "testA");
      }

      const counter = await cluster.get("bbb");
      if(!counter){
        await cluster.set("bbb", 100);
      }

      res.json({ msg: "Test", a, counter });
    } catch (error) {
      res.json({ msg: "Test", error });
    }
  });

  app.get("/count", async (req, res) => {
    const counter = await cluster.get("bbb");
    console.log("a :", counter);

    if (Number(counter) <= 0) {
      res.json({ msg: "0 됨." });
      return;
    }

    const rv = await cluster.decrby("bbb", 1);
    res.json({ msg: "Test", counter, rv });
  });

  app.get("/lock", async (req, res) => {
    console.log("in lock");
    try {
      let lock = await redlock.acquire(["aASDF"], 5000);
      try {
        const counter = await cluster.get("bbb");

        if (Number(counter) <= 0) {
          console.log("재고소진.");
          res.json({ msg: "0 됨." });
          return;
        }

        // Something do to db

        const rv = await cluster.decrby("bbb", 1);

        console.log("rv :", rv);
        res.json({ msg: "Test", counter, rv });
      } catch (error) {
        console.log("error :", error);
      } finally {
        await lock.release();
        console.log("unlock");
      }
    } catch (error) {
      res.json({ msg: " lock error", error });
    }
  });

  app.use((err: unknown, req: Request, res: Response, next: NextFunction) => {
    res.status(400).json({ msg: err });
  });

  const PORT = 3000;
  const HOST = "0.0.0.0";
  app.listen(PORT, HOST, () => {
    console.log(`conccuruncy distributed redis example`);
  });
};

main();

레디스 테스트

docker exec -it redis-node1 redis-cli -c -h 173.17.0.2 -p 6300 // 마스터 1

set a "testA"

docker exec -it redis-node4 redis-cli -c -h 173.17.0.5 -p 6303 // 슬레이브 1

get a

# 3 nGrinder

Ngrinder

설치

docker pull ngrinder/controller

docker run -d -v ~/ngrinder-controller:/opt/ngrinder-controller --name controller -p 80:80 -p 16001:16001 -p 12000-12009:12000-12009 ngrinder/controller

//
docker pull ngrinder/agent

docker run -d -v ~/ngrinder-agent:/opt/ngrinder-agent --name agent ngrinder/agent 192.168.0.37:80

// 기존 docker network에 연결 시켜주기
// 네트워크 확인, docker network ls

docker network connect redis-lock-example_redis_cluster controller
docker network connect redis-lock-example_redis_cluster agent

실행 스크립트

localhost:80 접속
user: admin
password: admin

import static net.grinder.script.Grinder.grinder
import static org.junit.Assert.*
import static org.hamcrest.Matchers.*
import net.grinder.script.GTest
import net.grinder.script.Grinder
import net.grinder.scriptengine.groovy.junit.GrinderRunner
import net.grinder.scriptengine.groovy.junit.annotation.BeforeProcess
import net.grinder.scriptengine.groovy.junit.annotation.BeforeThread
// import static net.grinder.util.GrinderUtils.* // You can use this if you're using nGrinder after 3.2.3
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith

import org.ngrinder.http.HTTPRequest
import org.ngrinder.http.HTTPRequestControl
import org.ngrinder.http.HTTPResponse
import org.ngrinder.http.cookie.Cookie
import org.ngrinder.http.cookie.CookieManager

/**
* A simple example using the HTTP plugin that shows the retrieval of a single page via HTTP.
*
* This script is automatically generated by ngrinder.
*
* @author admin
*/
@RunWith(GrinderRunner)
class TestRunner {

	public static GTest test
	public static HTTPRequest request
	public static Map<String, String> headers = [:]
	public static Map<String, Object> params = [:]
	public static List<Cookie> cookies = []
	

	@BeforeProcess
	public static void beforeProcess() {
		HTTPRequestControl.setConnectionTimeout(300000)
		test = new GTest(1, "192.168.0.37")
		request = new HTTPRequest()
		grinder.logger.info("before process.")
	}

	@BeforeThread
	public void beforeThread() {
		test.record(this, "test")
		grinder.statistics.delayReports = true
		grinder.logger.info("before thread.")
	}

	@Before
	public void before() {
		request.setHeaders(headers)
		CookieManager.addCookies(cookies)
		grinder.logger.info("before. init headers and cookies")
	}

	@Test
	public void test() {
		
		HTTPResponse response = request.GET("http://192.168.0.37:3001/lock")

		if (response.statusCode == 301 || response.statusCode == 302) {
			grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", response.statusCode)
		} else {
			assertThat(response.statusCode, is(200))
		}
	}
}

참고

http://redisgate.kr/redis/command/sadd.php // sAdd
https://redis.io/commands/sadd/

http://redisgate.kr/redis/command/append.php // append
https://redis.io/commands/append/

찾아볼거리

https://www.npmjs.com/package/redlock
https://www.npmjs.com/package/redis-lock

https://github.com/errorception/redis-lock
https://blog.dennisokeeffe.com/blog/2021-10-04-locking-redis-transactions-in-nodejs

레디스 클러스터와 분산락

https://redis.io/docs/manual/patterns/distributed-locks/

0개의 댓글