异步日志; 数据库连接池

作用 #

异步日志和数据库连接池的存在都是为了优化业务线程的性能, 对于日志系统,写入日志文件时的 IO 操作的阻塞会影响业务线程的性能, 在数据库方面,当数据库请求很多时,每次连接时的 TCP 握手和断开时的 TCP 挥手,以及连接时的验证,是造成性能损耗的主要原因。

异步日志和数据库连接池的相似点 #

  1. 都使用了单例模式
  • Logger 使用饿汉式单例,这是考虑到 Logger 是服务端的必须模块,在程序初始时就要实例化。
  • ConnectionPool 使用懒汉式单例,因为数据库访问是不一定必定发生的行为,也许在整个程序运行中,用户都没有使用数据库访问的业务,于是需要避免资源的过早分配。
  1. 都使用了生产者,消费者模式
  • 对于 Logger, 生产者是所有的 Logger::write() 调用,消费者是一个用于 IO 操作的守护线程。
  • 对于 ConnectionPool, 生产者是一个按需建立更多数据库连接的守护线程,消费者是所有的 ConnectionPool::getConnection() 调用。
  1. 都需要使用线程同步机制
  • 对共享资源访问的同步:使用 mutex
    • Logger 需要同步对共享队列的访问,和对共享的日志文件句柄的访问。
    • ConnectionPool 需要同步对连接队列的访问。
  • 线程间的通知机制:使用 condition_variable
    • 对于 Logger 而言,当队列满时,push() 操作需要 wait() 等待被唤醒,负责唤醒它的是 pop() 函数。当队列为空时,pop() 操作需要 wait() 等待被唤醒,负责唤醒它的是 push() 操作。
    • 对于 ConnectionPool 而言,当队列不为空时,ConnectionPool::producerThreadFunc() 会 wait() 等待被唤醒,负责唤醒它的是 getConnection()函数,也就是说,生产者守护线程只有在连接不够用时,才开始建立更多连接,也就是动态扩容。当队列为空时,getConnectoin() 函数会 wait() 等待被唤醒,负责唤醒它的是 ConnectionPool::producerThreadFunc() 函数。

异步日志 #

  • 使用阻塞队列这种线程安全的队列数据结构,它在多线程环境的特点是:
    队列为空时,从队列中取数据的线程会被阻塞,直到有新数据进入队列;
    队列已满时,向队列中插入数据的线程会被阻塞,直到有线程取走数据腾出空间。

  • 利用阻塞队列,异步日志系统的实现方式是: 业务线程调用 log(),日志内容被快速压入阻塞队列,之后业务线程就可以继续执行业务代码,而不必等待磁盘 IO;
    后台消费者线程从阻塞队列中取出日志,并进行磁盘 IO,将日志数据写入文件。 这样一来,将磁盘 IO 从业务线程转移到了消费者线程,避免了业务逻辑的阻塞。

数据库连接池 #

  • 单例,通过私有化构造函数,暴露一个公共的getConnectionPool接口获取static的ConnectionPool实例
  • 生产者消费者模型,生产者为守护线程,消费者为getConnection()函数
  • 处理定时过期连接的守护线程
  • 通过 shared_ptr 管理连接,通过 RAII 机制,在连接池被销毁时,调用所有的 Connection 的析构函数,进而 mysql_close()
  • 对于从连接池 pop() 出来的Connection, 自定义shared_ptr的释放资源的方式,也就是Connection直接归还到queue当中_
  • 线程间的通知方式: 互相通知,每次 getConnection 后,可能变为 empty(), 于是通知 producer, 每次 produce 变为 !empty(), 于是通知所有的 consumer