Python实现与MySQL长连接的客户端

  下面的代码是使用Python建立的和MySQL长连接的简单客户端示例。

  当和MySQL的连接断开后,会自动进行重连(被动式的重连,即只有调用增self.execute()、删self.execute()、改self.execute()、查self.query()方法出现异常的时候,才会触发重连)。可以修改“self.__check_exception_type()”方法,在该方法中完善对应的异常信息,来完善代码。

import datetime
import logging
import time
import traceback
import pymysql
import threading

LOG_FORMAT = "%(asctime)s - %(levelname)s [%(filename)s-%(funcName)s] Line: %(lineno)s] - %(message)s"
logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT)
_logger = logging.getLogger()

__all__ = ["MySQLClient"]


class MySQLClient:
    """
        self.__database_status:  用来判断当前的数据库连接是否正常。True代表数据库状态正常
        self.__has_reconnect_thread:  用来判断是否已经开启了重连数据库的后台线程。True代表已经有启动线程在进行数据库的重连了

        每次对数据库执行增、删、改、查等操作的时候,都要先判断下 self.__database_status 的值。
            1、当值为True的时候,代表数据库连接正常,正常执行相关的增、删、改、查等操作
            2、当值为False的时候,代表数据库已经是连接异常状态,那么就再检查下 self.__has_reconnect_thread 的值:
                2.1、当 self.__has_reconnect_thread 的值是False的时候,说明还没有启动后台重连数据库的线程,那么会启动一个线程
                    进行数据库重连。后台线程重连成功后,会把 self.__database_status 的值设置成True,
                    再把 self.__has_reconnect_thread 变量设置为False,并退出该重连数据库的线程。
                2.2、当 self.__has_reconnect_thread 的值是True的时候,说明已经有一个后台线程进行数据库的重连,就不再启动新的
                    重连数据库的线程了。
            3、在执行增、删、改、查等操作的时候,如果碰到了数据库连接异常,那么就会把 self.__database_status 的值设置成False
    """
    def __init__(self, host, username, password, database="", table_name="", port=3306, logger=None):
        """
        :param host:
        :param username:
        :param password:
        :param port:
        :param logger:  使用自定义的logger
        """
        self.__host = host
        self.__username = username
        self.__password = password
        self.__port = port
        self.__database = database
        self.__table_name = table_name
        self.__logger = logger if logger else _logger

        self.__database_status = False  # 数据库连接状态标识位
        self.__has_reconnect_thread = False  # 是否已经有重连数据库的后台线程标识位

        self.__connection = None  # 对数据库的连接
        self.__cursor = None  # 所有的增、删、改、查都是使用游标即该变量进行的操作

        # 实例化对象的时候就进行和数据库的连接,连接成功则可以直接调用对象的query()或者insert()等方法,连接失败则抛出异常
        self.connect_to_db()

        # 开启定时检查数据库状态的线程
        self.__thread_check_db_status()

    def __get_database_status(self):
        return self.__database_status

    def __set_database_status(self, status: bool):
        self.__database_status = status

    def __get_has_reconnect_thread(self):
        return self.__has_reconnect_thread

    def __set_has_reconnect_thread(self, status: bool):
        self.__has_reconnect_thread = status

    def __thread_reconnect_to_db(self, timespan=5):
        """
            如果类变量 __has_reconnect_thread 是False,则启动一个线程进行数据库的重连,并且设置 __has_reconnect_thread 变量
            为True

            如果类变量 __has_reconnect_thread 是True,说明已经有对数据库进行重连的线程了,此时 __has_reconnect_thread 值为True

            在对数据库重连的后台线程中,如果重连成功,则设置类变量 __database_status 为True并且退出该线程,同时设置
                __has_reconnect_thread 为False。

        :param timespan: 两次重连的时间间隔。
        :return:
        """
        def __thread_connect_to_database():
            self.__set_has_reconnect_thread(True)  # 设置全局变量,表示已经有线程进行重连数据库了。
            while True:  # 在while循环中一直进行重连,直到重新连上数据库
                if not self.__connect_to_db_once():
                    self.__logger.error("Re-connect to database failed !!! Will retry after [%s] seconds" % timespan)
                    time.sleep(timespan)
                else:
                    self.__set_database_status(True)  # 设置数据库连接状态为True
                    self.__set_has_reconnect_thread(False)  # 重连线程要退出了,所以设置该状态为False
                    self.__logger.info("Re-connect to database successfully @_@")
                    break  # 退出循环,即退出重连线程

        if not self.__get_has_reconnect_thread():
            self.__set_has_reconnect_thread(True)
            t = threading.Thread(target=__thread_connect_to_database, name="Thread-reconnect-to-db")
            t.start()
        # else:
        #     print("Already has thread connect to database")

    def __thread_check_db_status(self):
        """
        以后台线程的形式,检查数据库连接状态
        :return:
        """
        def __inner_thread_check_db_status():
            while True:
                try:
                    if not self.__get_database_status():
                        self.__thread_reconnect_to_db()
                except Exception:
                    self.__logger.error(str(traceback.format_exc()))
                finally:
                    time.sleep(1)
        t = threading.Thread(target=__inner_thread_check_db_status)
        t.setDaemon(True)
        t.start()

    def connect_to_db(self, timeout=60):
        """
        调用该方法,进行数据库的连接,如果超过设置的时间还没有连接的话,则抛出异常
        :param timeout:
        :return: 该类对象(self)或者抛出异常
        """
        # 如果数据库已经处于连接状态,则直接返回。因此该方法可以被重复调用执行。
        if self.__get_database_status():
            return self

        # 此时还没有连接到数据库,就需要进行对数据库的连接。
        start_timestamp = time.time()
        while True:
            try:
                if self.__connect_to_db_once(log=True):
                    self.__set_database_status(True)
                    break
                else:
                    end_timestamp = time.time()
                    if (end_timestamp - start_timestamp) < timeout:
                        self.__logger.error("Connect to database failed, will re-connect after 1 seconds ...")
                        self.__set_database_status(False)
                        time.sleep(1)
                    else:
                        break
            except Exception:
                self.__logger.error("Connect to database exception !!! " + traceback.format_exc())
                break

        # 连接数据库成功,则返回该类对象
        if self.__get_database_status():
            self.__logger.info("Connect to database successfully @_@")
            return self
        else:  # 连接数据库失败,则返回None
            message = "After [%s] seconds, still can not connect to database !!!" % timeout
            self.__logger.error(message)
            raise Exception(message)

    def __check_exception_type(self, e, trac):
        """
        根据不同的异常,进行分别处理。比如有的是SQL异常,有的是数据库连接异常。
            如果是数据库连接异常等,需要设置数据库连接状态,以便进行重连。
        :param e: 简短的异常信息
        :param trac: traceback.format_exc() 捕获到的详细信息
        :return:
        """

        # 1、SQL语法错误的异常,不属于数据库连接异常,不需要重连
        if "pymysql.err.ProgrammingError" in str(trac):
            # pymysql.err.ProgrammingError: (1064, "You have an error in your SQL syntax;
            # check the manual that corresponds to your MySQL server version for the right syntax to use near 'order by domain' at line 1")
            self.__logger.error("SQL syntax exception !!! " + str(e))
            raise Exception(e) from None

        # 2、SQL语法错误的异常,不属于数据库连接异常,不需要重连
        if "pymysql.err.InternalError" in str(trac):
            # pymysql.err.InternalError: (1054, "Unknown column 'names' in 'field list'")
            self.__logger.error("SQL syntax exception !!! " + str(e))
            raise Exception(e) from None

        # 3、连接不上数据库服务器,属于数据库连接异常,需要重连
        if "pymysql.err.OperationalError" in str(trac) and "Can't connect to MySQL server on" in str(trac):
            # pymysql.err.OperationalError: (2003, "Can't connect to MySQL server on '10.88.65.5' (timed out)")
            self.__set_database_status(False)  # 设置数据库连接状态为异常状态
            self.__logger.error("Connect to MySQL server exception !!! " + str(e))
            raise Exception(e) from None

        # 4、连接断开,属于数据库连接异常,需要重连
        if "pymysql.err.OperationalError" in str(trac) and "Lost connection to MySQL server" in str(trac):
            # pymysql.err.OperationalError: (2013, 'Lost connection to MySQL server during query ([WinError 10060]
            # 由于连接方在一段时间后没有正确答复或连接的主机没有反应,连接尝试失败。)')
            self.__set_database_status(False)  # 设置数据库连接状态为异常状态
            self.__logger.error("Lost connection to MySQL server exception !!! " + str(e))
            raise Exception(e) from None

        # 5、连接持续中断,属于数据库连接异常,需要重连
        if "pymysql.err.InterfaceError" in str(trac):
            # pymysql.err.InterfaceError: (0, '')
            self.__set_database_status(False)  # 设置数据库连接状态为异常状态
            self.__logger.error("Disconnect to MySQL server exception !!! " + str(e))
            raise Exception(e) from None

        # 6、未知即未被统计到的异常,待完善,统一归入这里,需要重连
        else:
            self.__set_database_status(False)  # 设置数据库连接状态为异常状态
            self.__logger.error("Unknown exception !!! " + str(trac))
            raise Exception(e) from None

    def __connect_to_db_once(self, log=False) -> bool:
        """
        进行一次数据库的连接。
        连接成功,则把获取到的连接对象设置到变量 self.__db_client 中,并返回True。同时设置全局变量 self.__connection 和 self.__cursor
        连接失败,则返回False
        :return:
        """
        try:
            connection = pymysql.connect(host=self.__host,
                                         port=self.__port,
                                         user=self.__username,
                                         password=self.__password,
                                         database=self.__database,
                                         )
            self.__connection = connection
            self.__cursor = connection.cursor()
            return True
        except Exception as e:
            try:
                # 如果在单次连接的时候,出现异常并经过 self.__check_exception_type()方法处理后,该方法仍然抛出异常,那么说明
                # 该次连接失败,在这个单次连接的方法中,捕获下即可
                self.__check_exception_type(e, traceback.format_exc())
            except Exception:
                return False

    def query(self, sql: str) -> list:
        """
        查询部分

        :return:  返回列表或者抛出异常(异常是因为客户提供的SQL格式错误)
        """
        ret_list = []

        # 数据库连接状态异常,则调用 self.__thread_reconnect_to_db() 方法进行重连,在该方法中进行实际判断,是否启动重连的线程。
        if not self.__get_database_status():
            self.__thread_reconnect_to_db()
            return ret_list
        try:
            self.__cursor.execute(query=sql)
            ret_list = list(self.__cursor.fetchall())
            return ret_list
        except Exception as e:
            self.__check_exception_type(e, traceback.format_exc())

    def execute(self, sql: str, auto_commit=True) -> bool:
        """
        增、删、改部分
        :return: 返回布尔值True|False或者抛出异常
        """
        ret = False

        # 数据库连接状态异常,则调用 self.__thread_reconnect_to_db() 方法进行重连,在该方法中进行实际判断,是否启动重连的线程。
        if not self.__get_database_status():
            self.__thread_reconnect_to_db()
            return ret

        try:
            self.__cursor.execute(sql)
            if auto_commit:
                self.__connection.commit()
            return True
        except Exception as e:
            self.__check_exception_type(e, traceback.format_exc())

    def manual_commit(self):
        # 数据库连接状态异常,则调用 self.__thread_reconnect_to_db() 方法进行重连,在该方法中进行实际判断,是否启动重连的线程。
        if not self.__get_database_status():
            self.__thread_reconnect_to_db()
            return False
        try:
            self.__connection.commit()
            return True
        except Exception as e:
            self.__check_exception_type(e, traceback.format_exc())

    def test(self):
        """
        测试代码部分
        :return:
        """
        while True:
            try:
                sql = "SELECT domain, account FROM {tablename} limit 10".format(tablename=self.__table_name)
                # sql = "SELECT COUNT(*) FROM {tablename}".format(tablename=self.__table_name)
                sql = "update {tablename} set username='JCL10231024' where id='007e7571-c74a-47c1-99d5-a3f868bd7dd7'".format(tablename=self.__table_name)
                print(sql)
                ret_list = self.execute(sql, auto_commit=False)
                self.manual_commit()
                print("ret_list: ", ret_list)
            except Exception:
                pass
            finally:
                print("Sleep 5 seconds ...")
                time.sleep(5)


if __name__ == '__main__':

    obj = MySQLClient(host="localhost",
                      port=3306,
                      username="username",
                      password="password",
                      database="database",
                      table_name="table_name"
                      )
    obj.test()

 

posted @ 2023-09-06 09:16  JCL_1023  阅读(320)  评论(0编辑  收藏  举报