面试:使用 golang 设计一个对象池

115 阅读5分钟

Overview

对象池设计模式可用于设计对象池。它是一种创建型设计模式,其中预先初始化并创建对象池并将其保存在池中。当需要时,客户端可以从池中请求对象,使用完之后再将其放回到池中,池中的对象永远不会被销毁。

什么时候应该想到创建一个对象池?

When we want to create a object pool

当创建该类的对象的成本很高并且在特定时间需要的此类对象的数量不多时。

让我们以数据库连接为例,每次创建连接对象的成本都很高(因为涉及到网络调用),而且每次需要的连接可能不会超过一定数量(同时与数据库建立连接的对象存在上限),对象池设计模式完全适用于这种情况。

When the pool object is the immutable object

当池对象是不可变对象时

再以数据库连接为例,数据库连接是一个不可变对象,它的几乎所有属性都无需更改。

When want to boost the application performance

当想要提高程序性能时

由于对象池已经创建,它将显著高应用程序性能

UML Diagram

下面是对象池设计的 UML 类图

image.png

总体思路如下

  • 我们有一个 Pool 类来管理池对象。 Pool 类首先使用一组已创建的固定数量的池对象进行初始化,然后它支持池对象的借出、收回和删除(load、receive、remove)。
  • 有一个 iPoolOjbect 接口代表了将驻留在池中的对象类型。根据用例类型的不同,该接口会有不同的实现方式。例如,在 DB 连接的情况下,DB 连接将实现该 iPoolObject 接口。
  • 有一个 Client 类使用 Pool 类来借用 Pool 对象。当池对象使用完毕后,它会将其返回到池中。

Low–Level Design

下面是用 Go 编程语言表达的底层设计。稍后我们也会看到一个工作示例

iPoolObject Interface

type iPoolObject interface {
    getID() string
    doSomething()
}

所有实现了该接口的对象都可以由 Pool 进行管理

DBConnection Class

type dbconnection struct {
    id string
}

func (c *dbconnection) getID() string

func (c *dbconnection) doSomething()

pool Class

type pool struct {
    idle   []iPoolObject
    active []*iPoolObject
    capacity int
    mulock *sync.Mutex
}

// initPool initialize the pool
func initPool(poolObjects []iPoolObject) (*pool, error) {}

// implement Pool Interface
func (p *pool) loan() (iPoolObejct, error) {}
func (p *pool) receive(target iPoolObject) error {}
func (p *pool) remove(target iPoolObject) error {}
func (p *pool) setMaxCapacity(capacity int) {}

client class

type client struct {
    pool *pool
}

func (c *client) init() {}
func (c *client) doWork() {}

Program

下面是完整的分模块工作代码(基于 Golang 实现的)

iPoolObject.go

package main

type iPoolObject interface {
    getID() string // 这是任何可用于比较两个不同池对象的 id
    doSomething()
}

dbconnection.go

package main

import "fmt"

type dbconnection struct {
    id string
}

func (c *dbconnection) getID() string {
    return c.id
}

func (c *dbconnection) doSomething() {
    fmt.Printf("Connection with id %s in action\n", c.getID())
}

pool.go

package main

import (
    "fmt"
    "sync"
)

type pool struct {
	idle     []iPoolObject
	active   []iPoolObject
	capacity int
	inUse    int
	mulock   *sync.Mutex
}

// initPool initialize the poll
func initPool(poolObjects []iPoolObject) (*pool, error) {
    if len(poolObjects) == 0 {
        return nil, fmt.Errorf("Cannot create a pool of 0 length")
    }
    // 用于管理分配出去的对象
    active := make([]iPoolObject, 0)
    pool := &pool{
       idle:     poolObjects, // 初始时传入的对象数组就是未分配对象池
       active:   active,
       capacity: len(poolObjects),
       mulock:   new(sync.Mutex), // 保证并发安全性
    }
    return pool, nil
}

// Implement Pool Interface
func (p *pool) loan() (iPoolObject, error) {
    p.mulock.Lock()
    defer p.mulock.Unlock()
    if len(p.idle) == 0 {
        return nil, fmt.Errorf("No pool object free. Please request after sometime")
    }
    // 取出一个对象放入已分配对象池中进行管理
    obj := p.idle[0]
    p.idle = p.idle[1:]
    p.active = append(p.active, obj)
    fmt.Printf("Loan pool object with ID: %s\n", obj.getID())
    return obj, nil
}

func (p *pool) receive(target iPoolObject) error {
    p.mulock.Lock()
    defer p.mulock.Unlock()
    err := p.remove(target) // helper 根据 id 从 active 池中移到idle 池
    if err != nil {
        return err
    }
    p.idle = append(p.idle, target)
    fmt.Printf("return pool object with ID: %s\n", target.getID())
    return nil
}

func (p *pool) remove(target iPoolObject) error {
    currentActiveLength := len(p.active)
    // 遍历 active 池,找到待归还的对象
    for i, obj := range p.active {
        if obj.getID() == target.getID() {
        // 找到后,将该对象与 active 池中最后一个对象交换位置,便于从池中删除
            p.active[currentActiveLength-1], p.active[i] = p.active[i], p.active[currentActiveLength-1]
            p.active = p.active[:currentActiveLength-1]
            return nil
        }
    }
    return fmt.Errorf("target pool object doesn't belong to the pool")
}

func (p *pool) setMaxCapacity(capacity int) {
    p.capacity = capacity
}

client.go

package main

import (
    "fmt"
    "log"
    "strconv"
)

type client struct {
    pool *pool
}

func (c *client) init() {
    connections := make([]*iPoolObject, 0)
    for i := 0; i < 3; i++ {
        // client 负责初始化连接对象
        c := &dbconnection{id: strconv.Itoa(i)}
        connections = append(connections, c)
    }
    var err error
    // client 将初始化的连接对象交给 pool 管理
    c.pool, err = initPool(connections)
    if err != nil {
        log.Fatalf("Init Pool Error: %s", err)
    }
}

func (c *client) doWork() {
    fmt.Printf("Capacity: %d\n\n", c.pool.capacity)
    
    conn1, err := c.pool.loan()
    if err != nil {
        log.Fatalf("Pool loan Error: %s", err)
    }
    conn1.doSomething()
    
    fmt.Printf("InUse: %d\n\n", c.pool.inUse)
    conn2, err := c.pool.loan()
    if err != nil {
        log.Fatalf("Pool loan Error: %s", err)
    }
    fmt.Printf("InUse: %d\n\n", c.pool.inUse)
    
    c.pool.receive(conn1)
    fmt.Printf("InUse: %d\n\n", c.pool.inUse)
    
    c.pool.receive(conn2)
    fmt.Printf("InUse: %d\n\n", c.pool.inUse)
}

main.go

func main() {
    client := &clinet{}
    client.init()
    client.doWork()
}

Output

Capacity: 3

Loan Pool Object with ID: 0
Connection with id 0 in action
InUse: 1

Loan Pool Object with ID: 1
Connection with id 1 in action
InUse: 2

Return Pool Object with ID: 0
InUse: 1

Return Pool Object with ID: 1
InUse: 0

Full Working Code

这是一个文件中的完整工作代码

package main

import (
	"fmt"
	"log"
	"strconv"
	"sync"
)

type iPoolObject interface {
	getID() string
	doSomething()
}

type connection struct {
	id string
}

func (c *connection) getID() string {
	return c.id
}

func (c *connection) doSomething() {
	fmt.Printf("Connection with id %s in action\n", c.getID())
}

type pool struct {
	mulock   *sync.Mutex // 注意传递的是指针(锁是有状态的)
	idle     []iPoolObject
	active   []iPoolObject
	capacity int
	inUse    int
}

func initPool(poolObjects []iPoolObject) (*pool, error) {
	if len(poolObjects) == 0 {
		return nil, fmt.Errorf("can't init pool by 0 pool object")
	}

	active := make([]iPoolObject, 0)
	pool := &pool{
		idle:     poolObjects,
		active:   active,
		capacity: len(poolObjects),
		mulock:   new(sync.Mutex),
	}
	return pool, nil
}

func (p *pool) getObject() (iPoolObject, error) {
	p.mulock.Lock()
	defer p.mulock.Unlock()
	if len(p.idle) == 0 {
		return nil, fmt.Errorf("no idle object can acquire...")
	}
	obj := p.idle[0]
	p.idle = p.idle[1:]
	p.active = append(p.active, obj)
	p.inUse++
	fmt.Printf("Loan pool object with ID: %s\n", obj.getID())
	return obj, nil
}

func (p *pool) backObject(target iPoolObject) error {
	p.mulock.Lock()
	defer p.mulock.Unlock()
	err := p.remove(target)
	if err != nil {
		return err
	}
	p.idle = append(p.idle, target)
	fmt.Printf("return pool object with ID: %s\n", target.getID())
	return nil
}

func (p *pool) remove(target iPoolObject) error {
	activeLen := len(p.active)
	for i, obj := range p.active {
		if obj.getID() == target.getID() {
			p.active[activeLen-1], p.active[i] = p.active[i], p.active[activeLen-1]
			p.active = p.active[:activeLen-1]
			p.inUse--
			return nil
		}
	}
	return fmt.Errorf("can't find object with ID: %s, target pool object doesn't belong to the pool", target.getID())
}

// 客户端必须有指向 pool 的指针,因为 client 需要向 pool 申请、返回 objet
type client struct {
	pool *pool
}

func (c *client) init() {
	connections := make([]iPoolObject, 0)
	for i := 0; i < 3; i++ {
		c := &connection{id: strconv.Itoa(i)}
		connections = append(connections, c)
	}
	var err error
	c.pool, err = initPool(connections)
	if err != nil {
		log.Fatalf("Init Pool Error: %s", err)
	}
}

// 模拟缓冲池对象的获取和放回
func (c *client) doWork() {
	fmt.Printf("Capacity: %d\n\n", c.pool.capacity)

	conn1, err := c.pool.getObject()
	if err != nil {
		log.Fatalf("get pool object failed, err: %s", err)
	}
	conn1.doSomething()
	fmt.Printf("InUse: %d\n\n", c.pool.inUse)

	conn2, err := c.pool.getObject()
	if err != nil {
		log.Fatalf("get pool object failed, err: %s", err)
	}
	conn2.doSomething()
	fmt.Printf("InUse: %d\n\n", c.pool.inUse)

	c.pool.backObject(conn1)
	fmt.Printf("InUse: %d\n\n", c.pool.inUse)

	c.pool.backObject(conn2)
	fmt.Printf("InUse: %d\n\n", c.pool.inUse)
}

func main() {
	c := &client{}
	c.init()
	c.doWork()
}

image.png