클라이언트 <-> 서버 <-> DB 관계에서는 비동기 쿼리 호출응답을 사용하여야 하는데, 멀티쓰레드 환경에서 오류가 발생하였고, 이를 해결하여 정리해봤습니다.

 

📺 오류

  • This MySqlConnection is already in use. 오류가 나오며 현재 이 Connection이 사용중이라고 나옵니다.

 

💬 오류 스크립트

using MySqlConnector;
using Server.Game;
using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

/// <summary>
/// MySQL 쿼리 매니저
/// </summary>
public class MySqlManager
{
    private static MySqlConnection _Connection; //mySQL connection
    private static string _SqlConnection = null; //SQL 접속 명령인자

    static MySqlManager() 
    {
        // 접속 명령인자 생성
        StringBuilder builder = new StringBuilder();

        ...

        _SqlConnection = builder.ToString();

        Console.WriteLine($"MySQL 준비 완료 {0}", System.DateTime.Now);
    }

    private static void MySqlConnect()
    {
        try
        {
            _Connection = new MySqlConnection(_SqlConnection);
            _Connection.Open();

            if (_Connection.State != ConnectionState.Open)
            {
                throw new InvalidOperationException("Connection failed to open.");
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex);
        }
    }

    private static void MySqlDisconnect()
    {
        _Connection.Close();
    }

    public static MySqlErrorCode Command(string command)
    {
        MySqlConnect(); //접속

        try
        {
            MySqlCommand dbcmd = new MySqlCommand(command, _Connection); //명령어를 커맨드에 입력
            dbcmd.ExecuteNonQuery(); //명령어를 SQL에 보냄

            return MySqlErrorCode.None;
        }
        catch (MySqlException e) //SQL 오류 
        {
            Console.WriteLine(e);

            return e.ErrorCode;
        }
        finally
        {
            MySqlDisconnect(); //접속해제
        }
    }

    public static DataTableReader Select(string command)
    {
        MySqlConnect(); //접속

        try
        {
            MySqlDataAdapter adapter = new MySqlDataAdapter(command, _Connection);
            DataTable table = new DataTable(); //테이블 생성
            adapter.Fill(table); //데이터 테이블 채우기

            return table.CreateDataReader(); //성공적으로 select를 했다면, 데이터 리더를 생성
        }
        catch (MySqlException e) //SQL 오류 발생
        {
            Console.WriteLine(e);

            return null;
        }
        finally
        {
            MySqlDisconnect(); //접속 해제
        }
    }
}
  • 현재 이 스크립트는 싱글턴 방식을 사용합니다.
  • 싱글턴 방식에서 하나의 Connector을 사용하며, 멀티쓰레드 환경에 대응하지 못한 상태입니다.
  • 동시 다발적으로 Sql 쿼리가 발생하면 비동기로 각 쓰레드가 쿼리를 요청하는데, 동시에 하나의 Connector에 접근하여 발생한 오류로 생각됩니다.

 

📖 오류 해결 스크립트

  • 쓰레드 로컬 스토리지를 이용하여 해결하였습니다.
 

[C# 서버] Thread Local Storage

📄 TLS TLS(스레드 로컬 스토리지)는 스레드에 로컬인 정적 또는 전역 메모리를 사용하는 컴퓨터 프로그래밍 방법입니다. 이 기능은 정적, 전역변수를 각 스레드에게 독립적인 형태로 만들어주고

bonnate.tistory.com

 

using MySqlConnector;
using Server.Game;
using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

/// <summary>
/// MySQL 쿼리 매니저
/// </summary>
public class MySqlManager
{
    private static ThreadLocal<MySqlConnection> _Connection = new ThreadLocal<MySqlConnection>(); // SQL Connection

    private static string _SqlConnection = null; //SQL 접속 명령인자

    static MySqlManager()
    {
        // 접속 명령인자 생성
        StringBuilder builder = new StringBuilder();

        ...

        _SqlConnection = builder.ToString();

        Console.WriteLine($"MySQL 준비 완료 {0}", System.DateTime.Now);
    }

    private static void MySqlConnect()
    {
        try
        {
            _Connection.Value = new MySqlConnection(_SqlConnection);
            _Connection.Value.Open();

            if (_Connection.Value.State != ConnectionState.Open)
            {
                throw new InvalidOperationException("Connection failed to open.");
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex);
        }
    }

    private static void MySqlDisconnect()
    {
        if (_Connection.Value != null && _Connection.Value.State != ConnectionState.Closed)
        {
            _Connection.Value.Close();
        }
    }

    public static MySqlErrorCode Command(string command)
    {
        MySqlConnect(); // 접속

        try
        {
            MySqlCommand dbcmd = new MySqlCommand(command, _Connection.Value); // 명령어를 커맨드에 입력
            dbcmd.ExecuteNonQuery(); // 명령어를 SQL에 보냄

            return MySqlErrorCode.None;
        }
        catch (MySqlException e) // SQL 오류
        {
            Console.WriteLine(e);

            return e.ErrorCode;
        }
        finally
        {
            MySqlDisconnect(); // 접속 해제
        }
    }

    public static DataTableReader Select(string command)
    {
        MySqlConnect(); // 접속

        try
        {
            MySqlDataAdapter adapter = new MySqlDataAdapter(command, _Connection.Value);
            DataTable table = new DataTable(); // 테이블 생성
            adapter.Fill(table); // 데이터 테이블 채우기

            return table.CreateDataReader(); // 성공적으로 select를 했다면, 데이터 리더를 생성
        }
        catch (MySqlException e) // SQL 오류 발생
        {
            Console.WriteLine(e);

            return null;
        }
        finally
        {
            MySqlDisconnect(); // 접속 해제
        }
    }
}

 

private static ThreadLocal<MySqlConnection> _Connection = new ThreadLocal<MySqlConnection>(); // SQL Connection
  • 각 쓰레드별로 고유한 Connection을 생성합니다.
  • 이 Connection은 쓰레드별로 연결되거나 해제되기때문에 동시다발적으로 쓰레드에서 각 쿼리문을 요청하여도 서로 스페이스를 침범하지 않습니다.

 

MySqlCommand dbcmd = new MySqlCommand(command, _Connection.Value); // 명령어를 커맨드에 입력
  • _Connection을 사용하기위해서는 로컬쓰레드의 레퍼런스를 가져와야합니다.
  • _Connection의 Value를 이용하여 실제 레퍼런스를 사용합니다.

 

 

bonnate