Python异步Redis客户端与通用缓存装饰器

前言

这里我将通过 redis-py 简易封装一个异步的Redis客户端,然后主要讲解设计一个支持各种缓存代理(本地内存、Redis等)的缓存装饰器,用于在减少一些不必要的计算、存储层的查询、网络IO等。

具体代码都封装在 HuiDBK/py-tools: 打造 Python 开发常用的工具,让Coding变得更简单 (github.com) 中,以大家便捷使用。

异步redis客户端

首先安装 redis-py 库

pip install redis

Redis 之前是不支持异步的,后面为了统一异步redis操作与python常用的redis.py 的api接口一致,aioredis的作者已经将 aioredis 加入了redis中维护,安装的版本大于 4.2.0rc1 就行。

  • aioredis:https://github.com/aio-libs-abandoned/aioredis-py
  • redis:https://github.com/redis/redis-py

BaseRedisManager 封装

#!/usr/bin/python3
# -*- coding: utf-8 -*-
# @Author: Hui
# @Desc: { redis连接处理模块 }
# @Date: 2023/05/03 21:13
from datetime import timedelta
from typing import Optional, Union

from redis import Redis
from redis import asyncio as aioredis

from py_tools import constants
from py_tools.decorators.cache import CacheMeta, cache_json, RedisCacheProxy, AsyncRedisCacheProxy


class BaseRedisManager:
    """Redis客户端管理器"""

client: Union[Redis, aioredis.Redis] = None
    cache_key_prefix = constants.CACHE_KEY_PREFIX

    @classmethod
    def init_redis_client(
            cls,
            async_client: bool = False,
            host: str = "localhost",
            port: int = 6379,
            db: int = 0,
            password: Optional[str] = None,
            max_connections: Optional[int] = None,
            **kwargs
    ):
        """
        初始化 Redis 客户端。

        Args:
        async_client (bool): 是否使用异步客户端,默认为 False(同步客户端)
        host (str): Redis 服务器的主机名,默认为 'localhost'
        port (int): Redis 服务器的端口,默认为 6379
        db (int): 要连接的数据库编号,默认为 0
        password (Optional[str]): 密码可选
        max_connections (Optional[int]): 最大连接数。默认为 None(不限制连接数)
        **kwargs: 传递给 Redis 客户端的其他参数

        Returns:
        None
        """
        if cls.client is None:
            redis_client_cls = Redis
            if async_client:
                redis_client_cls = aioredis.Redis

            cls.client = redis_client_cls(
                host=host, port=port, db=db, password=password, max_connections=max_connections, **kwargs
            )

        return cls.client

    @classmethod
    def cache_json(
            cls,
            ttl: Union[int, timedelta] = 60,
            key_prefix: str = None,
    ):
        """
        缓存装饰器(仅支持缓存能够json序列化的数据)
        缓存函数整体结果
        Args:
        ttl: 过期时间 默认60s
        key_prefix: 默认的key前缀, 再未指定key时使用

        Returns:
        """
        key_prefix = key_prefix or cls.cache_key_prefix
        if isinstance(ttl, timedelta):
            ttl = int(ttl.total_seconds())

        cache_proxy = RedisCacheProxy(cls.client)
        if isinstance(cls.client, aioredis.Redis):
            cache_proxy = AsyncRedisCacheProxy(cls.client)

        return cache_json(cache_proxy=cache_proxy, key_prefix=key_prefix, ttl=ttl)

还是跟之前封装客户端一样的简易封装,由类属性 client 维护真正操作的redis的客户端,通过 init_redis_client 方法进行初始化。这样封装的目的就是在系统中只初始化一份 redis 客户端,操作时可以直接使用类方法。BaseRedisManager 只实现一些通用的 redis 操作(有待挖掘),具体还是需要业务Manager来继承封装业务中操作redis的方法。目前只实现了一个redis的缓存装饰器,其实内部就是组织参数设置redis代理,然后调用另外一个通用的缓存装饰器,这样使用的时候不需要制定缓存代理了。

缓存装饰器

#!/usr/bin/python3
# -*- coding: utf-8 -*-
# @Author: Hui
# @Desc: { 缓存装饰器模块 }
# @Date: 2023/05/03 19:23
import asyncio
import functools
import hashlib
import json
from datetime import timedelta

import cacheout
from redis import Redis
from redis import asyncio as aioredis
from pydantic import BaseModel, Field
from typing import Union
from py_tools import constants

MEMORY_PROXY = MemoryCacheProxy(cache_client=cacheout.Cache(maxsize=1024))


def cache_json(
        cache_proxy: BaseCacheProxy = MEMORY_PROXY,
        key_prefix: str = constants.CACHE_KEY_PREFIX,
        ttl: Union[int, timedelta] = 60,
):
    """
    缓存装饰器(仅支持缓存能够json序列化的数据)
    Args:
    cache_proxy: 缓存代理客户端, 默认系统内存
    ttl: 过期时间 默认60s
    key_prefix: 默认的key前缀

    Returns:
    """
    key_prefix = f"{key_prefix}:cache_json"
    if isinstance(ttl, timedelta):
        ttl = int(ttl.total_seconds())

    def _cache(func):

        def _gen_key(*args, **kwargs):
            """生成缓存的key"""

            # 根据函数信息与参数生成
            # key => 函数所在模块:函数名:函数位置参数:函数关键字参数 进行hash
            param_args_str = ",".join([str(arg) for arg in args])
            param_kwargs_str = ",".join(sorted([f"{k}:{v}" for k, v in kwargs.items()]))
            hash_str = f"{func.__module__}:{func.__name__}:{param_args_str}:{param_kwargs_str}"
            hash_ret = hashlib.sha256(hash_str.encode()).hexdigest()

            # 根据哈希结果生成key 默认前缀:函数所在模块:函数名:hash
            hash_key = f"{key_prefix}:{func.__module__}:{func.__name__}:{hash_ret}"
            return hash_key

        @functools.wraps(func)
        def sync_wrapper(*args, **kwargs):
            """同步处理"""

            # 生成缓存的key
            hash_key = _gen_key(*args, **kwargs)

            # 先从缓存获取数据
            cache_data = cache_proxy.get(hash_key)
            if cache_data:
                # 有直接返回
                print(f"命中缓存: {hash_key}")
                return json.loads(cache_data)

            # 没有,执行函数获取结果
            ret = func(*args, **kwargs)

            # 缓存结果
            cache_proxy.set(key=hash_key, value=json.dumps(ret), ttl=ttl)
            return ret

        @functools.wraps(func)
        async def async_wrapper(*args, **kwargs):
            """异步处理"""

            # 生成缓存的key
            hash_key = _gen_key(*args, **kwargs)

            # 先从缓存获取数据
            cache_data = await cache_proxy.get(hash_key)
            if cache_data:
                # 有直接返回
                return json.loads(cache_data)

            # 没有,执行函数获取结果
            ret = await func(*args, **kwargs)

            # 缓存结果
            await cache_proxy.set(key=hash_key, value=json.dumps(ret), ttl=ttl)
            return ret

        return async_wrapper if asyncio.iscoroutinefunction(func) else sync_wrapper

    return _cache

cache_json 是一个带单参数的缓存装饰器,可以指定一些缓存的代理、缓存key前缀、缓存ttl等。

内部实现了同步、异步函数的缓存处理,关键点其实就是如何构造唯一的缓存key,这里就是根据key前缀与函数的一些签名信息来构造的。

def _gen_key(*args, **kwargs):
    """生成缓存的key"""

    # 没有传递key信息,根据函数信息与参数生成
    # key => 函数所在模块:函数名:函数位置参数:函数关键字参数 进行hash
    param_args_str = ",".join([str(arg) for arg in args])
    param_kwargs_str = ",".join(sorted([f"{k}:{v}" for k, v in kwargs.items()]))
    hash_str = f"{func.__module__}:{func.__name__}:{param_args_str}:{param_kwargs_str}"
    hash_ret = hashlib.sha256(hash_str.encode()).hexdigest()

    # 根据哈希结果生成key 默认前缀:函数所在模块:函数名:hash
    hash_key = f"{key_prefix}:{func.__module__}:{func.__name__}:{hash_ret}"
    return hash_key

函数所在模块:函数名:函数位置参数:函数关键字参数 进行hash,在处理关键字参数的需要排个序,来保证相同的参数,顺序不同但缓存key一致。后面的逻辑就是常见的设置缓存操作。

image.png

缓存代理类

#!/usr/bin/python3
# -*- coding: utf-8 -*-
# @Author: Hui
# @Desc: { 缓存装饰器模块 }
# @Date: 2023/05/03 19:23
import asyncio
import functools
import hashlib
import json
from datetime import timedelta

import cacheout
from redis import Redis
from redis import asyncio as aioredis
from pydantic import BaseModel, Field
from typing import Union
from py_tools import constants


class CacheMeta(BaseModel):
    """缓存元信息"""

    key: str = Field(description="缓存的key")
    ttl: Union[int, timedelta] = Field(description="缓存有效期")
    cache_client: str = Field(description="缓存的客户端(Redis、Memcached等)")
    data_type: str = Field(description="缓存的数据类型(str、list、hash、set)")


class BaseCacheProxy(object):
    """缓存代理基类"""

    def __init__(self, cache_client):
        self.cache_client = cache_client  # 具体的缓存客户端,例如Redis、Memcached等

    def set(self, key: str, value: str, ttl: int):
        raise NotImplemented

    def get(self, key):
        cache_data = self.cache_client.get(key)
        return cache_data


class RedisCacheProxy(BaseCacheProxy):
    """同步redis缓存代理"""

    def __init__(self, cache_client: Redis):
        super().__init__(cache_client)

    def set(self, key, value, ttl):
        self.cache_client.setex(name=key, value=value, time=ttl)


class AsyncRedisCacheProxy(BaseCacheProxy):
    """异步Redis缓存代理"""

    def __init__(self, cache_client: aioredis.Redis):
        super().__init__(cache_client)

    async def set(self, key, value, ttl):
        await self.cache_client.setex(name=key, value=value, time=ttl)

    async def get(self, key):
        cache_data = await self.cache_client.get(key)
        return cache_data


class MemoryCacheProxy(BaseCacheProxy):
    """系统内存缓存代理"""

    def __init__(self, cache_client: cacheout.Cache):
        super().__init__(cache_client)

    def set(self, key, value, ttl):
        self.cache_client.set(key=key, value=value, ttl=ttl)


MEMORY_PROXY = MemoryCacheProxy(cache_client=cacheout.Cache(maxsize=1024))

这里设置一个缓存代理抽象类是用于封装屏蔽不同缓存客户端的操作不一致性。统一成如下入口

def set(self, key: str, value: str, ttl: int):
    raise NotImplemented

def get(self, key):
    cache_data = self.cache_client.get(key)
    return cache_data

让具体的缓存客户端重写(实现)这两个方法,以达到缓存装饰器的通用性。目前只实现了同步、异步redis缓存代理以及通过 cacheout 库实现的本地内存缓存代理,后面接入其他的缓存代理(例如Memcached等)就不用动cache_json函数了,只要继承 BaseCacheProxy,实现具体的 set、get 操作即可。

pip install python-memcached
import memcache

class MemcacheCacheProxy(BaseCacheProxy):

    def __init__(self, cache_client: memcache.Client):
        super().__init__(cache_client)

    def set(self, key, value, ttl):
        self.cache_client.set(key, value, time=ttl)

由于获取缓存的方法逻辑一致,故而直接复用就行。

测试Demo

#!/usr/bin/python3
# -*- coding: utf-8 -*-
# @Author: Hui
# @File: cache.py
# @Desc: { cache demo 模块 }
# @Date: 2024/04/23 11:11
import asyncio
import time
from datetime import timedelta

import cacheout

from py_tools.connections.db.redis_client import BaseRedisManager
from py_tools.decorators.cache import cache_json, MemoryCacheProxy, RedisCacheProxy, AsyncRedisCacheProxy


class RedisManager(BaseRedisManager):
    client = None


class AsyncRedisManager(BaseRedisManager):
    client = None


RedisManager.init_redis_client(async_client=False)
AsyncRedisManager.init_redis_client(async_client=True)

memory_proxy = MemoryCacheProxy(cache_client=cacheout.Cache())
redis_proxy = RedisCacheProxy(cache_client=RedisManager.client)
aredis_proxy = AsyncRedisCacheProxy(cache_client=AsyncRedisManager.client)


@cache_json(key_prefix="demo", ttl=3)
def memory_cache_demo_func(name: str, age: int):
    return {"test_memory_cache": "hui-test", "name": name, "age": age}


@cache_json(cache_proxy=redis_proxy, ttl=10)
def redis_cache_demo_func(name: str, age: int):
    return {"test_redis_cache": "hui-test", "name": name, "age": age}


@cache_json(cache_proxy=aredis_proxy, ttl=timedelta(minutes=1))
async def aredis_cache_demo_func(name: str, age: int):
    return {"test_async_redis_cache": "hui-test", "name": name, "age": age}


@AsyncRedisManager.cache_json(ttl=30)
async def aredis_manager_cache_demo_func(name: str, age: int):
    return {"test_async_redis_manager_cache": "hui-test", "name": name, "age": age}


def memory_cache_demo():
    print("memory_cache_demo")
    ret1 = memory_cache_demo_func(name="hui", age=18)
    print("ret1", ret1)
    print()

    ret2 = memory_cache_demo_func(name="hui", age=18)
    print("ret2", ret2)
    print()

    time.sleep(3)
    ret3 = memory_cache_demo_func(age=18, name="hui")
    print("ret3", ret3)
    print()

    assert ret1 == ret2 == ret3


def redis_cache_demo():
    print("redis_cache_demo")
    ret1 = redis_cache_demo_func(name="hui", age=18)
    print("ret1", ret1)
    print()

    ret2 = redis_cache_demo_func(name="hui", age=18)
    print("ret2", ret2)

    assert ret1 == ret2


async def aredis_cache_demo():
    print("aredis_cache_demo")
    ret1 = await aredis_cache_demo_func(name="hui", age=18)
    print("ret1", ret1)
    print()

    ret2 = await aredis_cache_demo_func(name="hui", age=18)
    print("ret2", ret2)

    assert ret1 == ret2


async def aredis_manager_cache_demo():
    print("aredis_manager_cache_demo")
    ret1 = await aredis_manager_cache_demo_func(name="hui", age=18)
    print("ret1", ret1)
    print()

    ret2 = await aredis_manager_cache_demo_func(name="hui", age=18)
    print("ret2", ret2)

    assert ret1 == ret2


async def main():
    memory_cache_demo()

    redis_cache_demo()

    await aredis_cache_demo()

    await aredis_manager_cache_demo()


if __name__ == '__main__':
    asyncio.run(main())

输出结果

memory_cache_demo
ret1 {'test_memory_cache': 'hui-test', 'name': 'hui', 'age': 18}

命中缓存: demo:cache_json:__main__:memory_cache_demo_func:46c6a618a88eb5067a00915c10c97c6c72d5073ecf9b04060433de75b2d21f51
ret2 {'test_memory_cache': 'hui-test', 'name': 'hui', 'age': 18}

ret3 {'test_memory_cache': 'hui-test', 'name': 'hui', 'age': 18}

redis_cache_demo
ret1 {'test_redis_cache': 'hui-test', 'name': 'hui', 'age': 18}

命中缓存: py-tools:cache_json:__main__:redis_cache_demo_func:a00b13aa2e1e56ad328d1956bc3c3fb8e89b7007453a780e866cc3ccafb51d73
ret2 {'test_redis_cache': 'hui-test', 'name': 'hui', 'age': 18}
aredis_cache_demo
ret1 {'test_async_redis_cache': 'hui-test', 'name': 'hui', 'age': 18}

ret2 {'test_async_redis_cache': 'hui-test', 'name': 'hui', 'age': 18}
aredis_manager_cache_demo
ret1 {'test_async_redis_manager_cache': 'hui-test', 'name': 'hui', 'age': 18}

ret2 {'test_async_redis_manager_cache': 'hui-test', 'name': 'hui', 'age': 18}

Redis 缓存情况

缓存信息还是挺清晰的就是有点长。由于是从主入口调用的函数,所以 func.__module__ 是 __main__。

这缓存装饰器一般适用于一些参数相同, 结果经常不变的情况下,以及允许短时间内出现数据不一致的场景。如下是一些典型的应用场景

  1. API Token缓存:对于需要使用API Token进行身份验证的API请求,通常API Token具有一定的有效期。在这种情况下,你可以使用缓存装饰器来缓存API Token,以避免在每次请求时重新生成或从后端服务获取。这样可以降低对后端服务的负载,并提高系统的响应速度。
  2. OSS Sign URL缓存:当需要生成签名URL来访问对象存储服务(如AWS S3、阿里云OSS)中的资源时,通常需要对URL进行签名以确保安全性。在这种情况下,你可以使用缓存装饰器来缓存已签名的URL,在一定时间内重复使用相同的签名URL,而不必重新计算签名。这样可以降低对签名计算资源的消耗,并减少重复的签名请求。
  3. 频繁查询的数据缓存:对于一些数据不经常变化但是频繁被查询的情况,比如一些静态配置信息、全局参数等,可以使用缓存装饰器将查询结果缓存起来,减少数据库查询次数,提高系统的响应速度。
  4. 外部API响应结果缓存:当你调用外部API获取数据时,有时这些数据在一段时间内不会发生变化。在这种情况下,你可以使用缓存装饰器来缓存外部API的响应结果,以避免频繁地向外部API发出请求。这不仅可以提高系统的性能,还可以降低对外部服务的依赖性。

总的来说,缓存装饰器可以应用于许多场景,特别是在需要提高性能、减少资源消耗和避免重复请求数据的情况下。通过合理地设置缓存时间,可以权衡数据的新鲜度和系统性能,从而实现更好的用户体验。

源代码

源代码已上传到了Github,里面也有具体的使用Demo,欢迎大家一起体验、贡献。

HuiDBK/py-tools: 打造 Python 开发常用的工具,让Coding变得更简单 (github.com)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/582423.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Ubuntu TeamViewer安装与使用

TeamViewer是一款跨平台的专有应用程序,允许用户通过互联网连接从全球任何地方远程连接到工作站、传输文件以及召开在线会议。它适用于多种设备,例如个人电脑、智能手机和平板电脑。 TeamViewer可以派上用场,尤其是在排除交通不便或偏远地区…

Open SUSE 安装MySQL

前言 看了一圈网上关于SUSE的教程实在是太少了,毕竟太小众了。这两天在安装MySQL的时候老是出问题,踩了一晚上的坑,发现其实很简单,网上看了方法大概有这几种 通过Yast software management安装,但是我尝试了&#x…

Linux下的基本指令(1)

嗨喽大家好呀!今天阿鑫给大家带来Linux下的基本指令(1),下面让我们一起进入Linux的学习吧! Linux下的基本指令 ls 指令pwd命令cd 指令touch指令mkdir指令(重要)rmdir指令 && rm 指令(重要)man指令(重要)cp指…

海外三大AI图片生成器对比(Stable Diffusion、Midjourney、DALL·E 3)

Stable Diffusion DreamStudio 是Stable Diffusion 的官方网页,价格便宜,对图片的操作性强,但同时编辑页面不太直观,对使用者的要求较高。 与 DALLE 和 Midjourney 不同,Stable Diffusion 是开源的。这也意味着&…

[图解]领域驱动设计伪创新-为什么互联网是重灾区-02

0 00:00:00,000 --> 00:00:04,737 我们并没有说,微信或者是美团用了领域驱动设计 1 00:00:04,737 --> 00:00:06,632 没有做这个暗示 2 00:00:06,632 --> 00:00:08,290 我只是说什么 3 00:00:09,350 --> 00:00:12,700 针对用户量很大的系统 4 00:00:…

CANoe如何实现TLS协议

TLS,Transport Layer Security,传输层安全协议。是在传输层和应用层之间,为了保证应用层数据能够安全可靠地通过传输层传输且不会泄露的安全防护。 TLS安全协议的实现逻辑,在作者本人看来,大致分为三个部分&#xff1…

minio主从同步和双机热备

文章目录 1. 安装2. 测试3. 双机热备 环境说明 服务器IPminio-slb10.10.xxx.251minio-01/02/03/0410.10.xxx.25/206/207/208minio-backup10.10.xxx.204 1. 安装 下载地址: http://dl.minio.org.cn/client/mc/release/linux-amd64/mc 安装 只有一个二进制文件&…

Spring Security OAuth2 统一登录

介绍 Spring Security OAuth2 是一个在 Spring Security 框架基础上构建的 OAuth2 授权服务器和资源服务器的扩展库。它提供了一套功能强大的工具和组件,用于实现 OAuth2 协议中的授权流程、令牌管理和访问控制。 Git地址:yunfeng-boot3-sercurity: Sp…

贴片OB2500POPA OB2500POP SOP-8 电源开关控制器IC芯片

OB2500POPA电源管理芯片被广泛应用于各种低功耗AC-DC充电器和适配器中。以下是该芯片的一些典型应用案例: 手机充电器:OB2500POPA可以用于设计高效、小巧的手机充电器,提供稳定的输出电压和电流。 USB充电器:在USB充电器中&…

第5篇:创建Nios II工程之Hello_World<四>

Q:最后我们在DE2-115开发板上演示运行Hello_World程序。 A:先烧录编译Quartus硬件工程时生成的.sof文件,在FPGA上成功配置Nios II系统;然后在Nios II Eclipse窗口右键点击工程名hello_world,选择Run As-->Nios II …

Node.js 版本升级方法

在构建vue项目时,依赖npm(Node Package Manager)工具,类似于Java项目需要maven管理。而npm是node.js的管理工具,npm依赖node.js环境才能执行。 有时候使用voscode或者其他工具安装vue项目依赖,显示一直处于…

Apollo共创生态:共筑未来智能出行新篇章

目录 引言Apollo七周年大会回顾心路历程企业生态计划 个人心得与启发技术革新的引领者展望 结语 引言 在科技飞速发展的今天,智能出行已经成为全球关注的焦点。Apollo开放平台,作为智能出行领域的先行者,已经走过了七个春秋。七年磨一剑&…

vue3+antv+ts实现勾选同意协议复选框之后才能继续注册登录

效果如下&#xff1a; 勾选复选框之前 勾选复选框之后 这里偷懒了&#xff0c;没有把登录和注册按钮分开控制&#xff0c;自己实操的时候可以去细化一下功能 代码如下&#xff1a; <script setup lang"ts"> import { ref, defineProps, reactive } from &qu…

【Spring Boot 源码学习】SpringApplication 的 run 方法监听器

《Spring Boot 源码学习系列》 SpringApplication 的 run 方法监听器 一、引言二、主要内容2.1 SpringApplicationRunListeners2.2 SpringApplicationRunListener2.3 实现类 EventPublishingRunListener2.3.1 成员变量和构造方法2.3.2 成员方法2.3.2.1 不同阶段的事件处理2.3.2…

分享一些常用的内外网文件传输工具

内外网隔离后的文件传输是网络安全领域中一个常见而又重要的问题。随着信息技术的快速发展&#xff0c;网络安全问题日益凸显&#xff0c;内外网隔离成为了许多企业和组织保护内部信息安全的重要手段。然而&#xff0c;内外网隔离后如何有效地进行文件传输&#xff0c;成为了摆…

Redis__数据类型

文章目录 &#x1f60a; 作者&#xff1a;Lion J &#x1f496; 主页&#xff1a; https://blog.csdn.net/weixin_69252724 &#x1f389; 主题&#xff1a;Redis__数据类型 ⏱️ 创作时间&#xff1a;2024年04月28日 ———————————————— 这里写目录标题 文…

Virtualbox7.0.10--创建虚拟机

前言 下载Virtualbox7.0.10&#xff0c;可参考《Virtualbox–下载指定版本》 Virtualbox7.0.10具体安装步骤&#xff0c;可参考《Virtualbox7.0.10的安装步骤》 创建虚拟机 1.双击打开Virtualbox 后&#xff0c;单击“新建”&#xff0c;进入新建虚拟电脑页面 2. 设置虚拟电脑…

如何在Flask应用程序中使用JSON Web Tokens进行安全认证

密码、信用卡信息、个人识别号码&#xff08;PIN&#xff09;——这些都是用于授权和认证的关键资产。这意味着它们需要受到未经授权的用户的保护。 作为开发者&#xff0c;我们的任务是保护这些敏感信息&#xff0c;并且在我们的应用程序中实施强大的安全措施非常重要。 现在…

24.4.28(板刷dp,拓扑判环,区间dp+容斥算回文串总数)

星期一&#xff1a; 昨晚cf又掉分&#xff0c;小掉不算掉 补ABC350 D atc传送门 思路&#xff1a;对每个连通块&#xff0c;使其成为一个完全图&#xff0c;完全图的边数为 n*(n-1)/2 , 答案加上每个连通块成为完全图后的…

Anti-BAFF (mouse), mAb (blocking) (Sandy-2)

Adipogen开发了可以用于体内实验作为阻断剂的小鼠的抗BAFF单克隆抗体&#xff0c;为肿瘤学、免疫学、免疫治疗、自身免疫性疾病&#xff0c;小鼠模型体内实验等研究领域的实验者们对BAFF通路的研究提供了强有力的工具。 。 B细胞激活因子&#xff08;BAFF&#xff09;又称肿瘤坏…