likes
comments
collection
share

Python实现一个简单的响应缓存模块

作者站长头像
站长
· 阅读数 66

引言

在某个自动化测试项目中,需要缓存查询设备返回的响应以备后续查询,吞吐量为每秒50个左右,并且有多个不同的请求需要对其响应加以区分。

方案

缓存响应的话,肯定得考虑缓存的方式,一种是远端缓存,比如redis,一种是本地缓存,而本地缓存也可以细分为内存与本地持久化两种。 因为对写入和查询的时延要求较高,且只需要在程序运行时可以写入与查询即可,无需持久化,故我选择直接在本地内存中缓存响应信息。(redis属于外部依赖,会增加程序复杂性,并且读写效率不及本地内存,而本地持久化,读写相对耗时,且不具备必要性)

具体设计

缓存的数据容器

缓存的容器,我选择dict类型,而key值为请求的头部信息,value为一个列表或者队列,用于存储过往的响应

缓存内容的类型选择

value的类型,我比较了一下,在元素个数固定,并且需要频繁对头部和尾部进行删除和写入的情况下,deque明显比list性能更优,所以我最终决定使用deque。

缓存的个数限制

需要对value存储的响应个数上限进行限制,达到上限即清除时间最久的那个响应,否则占用内存会不断膨胀,并且影响程序读写性能。

记录响应时间

又因为需要考虑响应的即时性,需要对响应的写入时间进行记录,否则无法判断,响应是否最新写入的,在自动化测试查询响应并加以判断时,会产生误判,所以,deque的元素类型设定为一个tuple,tuple的元素为响应的值与响应的写入时间。

多线程安全性

考虑到高并发场景,需要对读写进行加锁。

是否封装为响应缓存类

本身功能不是那么复杂的情况下,封装为响应缓存类会存在过度封装的问题,并且性能上也可能会有影响,所以暂不进行封装。

代码实现

from collections import deque
from datetime import datetime
from decimal import Decimal
from threading import Lock
from typing import Optional, Deque, Tuple
from settings import test_logger

# 全局锁,用于保护共享资源rsp_dict的并发访问
lock = Lock()
# 缓存响应的个数
rsp_cache_size = 50

# 共享响应字典,用于存储键值对及其时间戳
rsp_dict: dict[str, Deque[Tuple[str, str]]] = {}

time_fmt = "%Y-%m-%d %H:%M:%S.%f"


# @timeit
def set_rsp(k: str, rsp: str) -> None:
    """
    设置响应值到缓存中,如果超过最大缓存大小则移除最早的数据。

    :param k: 键
    :param rsp: 响应字符串
    :return:
    """
    # 减少锁的竞争: 确保在锁内部的操作尽可能快,考虑是否可以将一些不直接影响数据一致性的逻辑移出锁块。
    ts = get_current_time()
    with lock:
        current = rsp_dict.setdefault(k, deque(maxlen=rsp_cache_size))
        current.append((rsp, ts))
        test_logger.info(F"set rsp, K: {k}, v: {rsp}, timestamp: {ts}, rsp cache size: {len(current)}")


def get_current_time() -> str:
    """
    获取当前时间
    :return:
    """
    return datetime.now()


def get_current_time_str() -> str:
    """
    获取当前时间的格式化时间戳字符串
    :return:
    """
    return datetime.now().strftime(time_fmt)


# @timeit
def get_rsp(k: str):
    """
    获取指定键的最新响应及时间戳。

    :param k: 键
    :return: 最新响应与时间戳元组,如果键不存在则返回(None, None)
    """
    with lock:
        record = rsp_dict.get(k, deque())
        rsp, ts = record[-1] if record else (None, None)
        test_logger.info(F"get rsp,K: {k}, v: {rsp}, timestamp: {ts}")
        return rsp, ts


# @timeit
def clacc_time_diff(t1: datetime, t2: datetime) -> Decimal:
    """
    计算两个时间戳之间的时间差,单位为秒,精确到小数点后3位。
    """
    ts_cost = (t2 - t1).total_seconds()
    ts_cost = Decimal(ts_cost).quantize(Decimal("0.000"))
    return ts_cost


def get_rsp_with_timeout(k: str, timeout: float = 0.1) -> Optional[str]:
    """
    获取指定键的最新响应,如果响应时间超过指定超时时间则返回None。
    :param k:
    :param timeout:
    :return:
    """
    rsp, ts = get_rsp(k)
    if ts:
        if clacc_time_diff(get_current_time(), ts) <= timeout:
            test_logger.info(F"get rsp,K: {k}, v: {rsp}, timestamp: {ts}")
            return rsp
    else:
        return None

后续扩展

这只是简单实现了一个本地的响应缓存模块,可内嵌在测试框架内。后续如果随着测试项目的复杂化与分布式执行,也可以考虑将该模块加以扩展,响应缓存在远端数据库,并封装为工具类,支持不同的缓存策略。

转载自:https://juejin.cn/post/7379151898568261672
评论
请登录