# RedisXsync

![PyPI](https://img.shields.io/pypi/v/redis-xsync) ![Python](https://img.shields.io/pypi/pyversions/redis-xsync) ![License](https://img.shields.io/badge/license-MIT-green)

RedisXsync 是一个 **同步 / 异步统一 Redis 客户端**，专为 **分布式锁** 和 **爬虫账号池限流** 场景设计，支持多实例、多
DB，线程安全和协程安全。

---

## ✨ 核心特性

| 特性          | 描述                               |
|-------------|----------------------------------|
| 同步 / 异步统一接口 | 同一套 API 支持 `with` 和 `async with` |
| 多实例 / 多 DB  | 支持不同 Redis 实例与数据库同时管理            |
| 分布式锁        | 单键锁、组合锁，阻塞 / 非阻塞模式               |
| 账号池限流       | 基于 Lua 原子操作的令牌桶限流，可等待可用 token    |
| 线程 & 协程安全   | 避免多线程 / 协程冲突                     |
| 自动连接管理      | Context manager 自动释放连接           |

---

## 🏗 架构概览

| RedisXsync               |
|--------------------------|
| Sync / Async Interfaces  |
| Distributed Lock Manager |
| Rate Limiter / TokenPool |

- 分布式锁：通过单键或多键原子操作保证并发安全
- 限流/账号池：Lua 脚本保证跨进程/跨机器的原子性，精确到毫秒

---

## 📦 安装

```bash
pip install redis-xsync

---

## 🚀 快速开始

### 1️⃣ 注册 Redis

​```python
from redis_xsync import register_redis, resolve_redis, redisXsync

redis1 = register_redis(
    label="redis_xsync",
    host="127.0.0.1",
    port=6379,
    db=0,
    password=None,
    decode_responses=True
)

# 通过 label 获取
redis2 = resolve_redis("redis_xsync")


# 使用方式说明：
# 1️⃣ 全局默认实例 redisXsync
#    - 在首次调用 register_redis 注册任意 Redis 时，首次注册会自动赋值给 redisXsync，非并发安全
#    - 后续可以直接使用 redisXsync，无需通过 label 获取
#
# 2️⃣ 通过 label 显式管理 Redis
#    - register_redis(label=..., ...) 注册 Redis 实例
#    - resolve_redis(label) 根据 label 获取对应 Redis 实例
#
# 3️⃣ 独立实例
#    - 可以直接创建 RedisXsync() 实例，不必参与 register_redis 、resolve_redis、 redisXsync 全局管理
#    - 适合临时或隔离使用场景
```

## 🔄 同步 / 异步使用

### ✅ 同步

```python
with redis1(db=1) as r:
    r.setnx("key", "value")
    print(r.get("key"))
```

---

### ✅ 异步

```python
import asyncio


async def main():
    async with redis1(db=1) as r:
        await r.setnx("key", "value")
        print(await r.get("key"))


asyncio.run(main())
```

---

## 🔐 分布式锁（支持组合锁）

### 特性

* 多 key 原子加锁
* 所有 key 释放才算解锁
* 支持阻塞等待

---

### 示例（多线程 + 协程混合）

```python
import threading
import asyncio
from redis_xsync import register_redis

cx = 0
redis = register_redis(label="redis_xsync", host="127.0.0.1", port=6379)
Lock = redis.AsyncRedisAtomicMultiLock


def sync_worker():
    global cx
    for _ in range(5):
        with Lock("lock_key", db=0, ttl_ms=10, blocking=True):
            with redis(db=2) as r:
                r.setnx("db2", str(cx))
                cx += 1
                print(f"[SYNC] {cx=}")


async def async_worker():
    global cx
    for _ in range(5):
        with Lock("lock_key", db=0, ttl_ms=10, blocking=True):
            async with redis(db=3) as r:
                await r.setnx("db3", str(cx))
                cx += 1
                print(f"[ASYNC] {cx=}")
        await asyncio.sleep(0.2)


# 启动线程
threading.Thread(target=sync_worker).start()

# 启动协程
asyncio.run(async_worker())
```

---

## 🚦 爬虫账号池限流

基于 Redis + Lua，实现**分布式限流 + 自动等待可用账号**

📦 Limited — Redis 容器操作方法对照表

| 方法                                                  | 异步 | 阻塞 | 遵循限流 | 数量 | 描述                     |
|-----------------------------------------------------|----|----|------|----|------------------------|
| `set_available_containers(*containers)`             | ✅  | ❌  | ✅    | N  | 存储/更新容器配置（生产者）         |
| `ask_available_containers(quantity)`                | ✅  | ❌  | ✅    | N  | 获取 N 个可用容器，若无返回 `None` |
| `ask_available_container()`                         | ✅  | ❌  | ✅    | 1  | 获取单个可用容器，若无返回 `None`   |
| `wait_ask_available_containers(quantity, timeout)`  | ✅  | ✅  | ✅    | N  | 阻塞等待 N 个可用容器           |
| `wait_ask_available_container(timeout)`             | ✅  | ✅  | ✅    | 1  | 阻塞等待单个可用容器             |
| `acquire_random_containers(quantity)`               | ✅  | ❌  | ❌    | N  | 随机获取 N 个容器，忽略限流        |
| `acquire_random_container()`                        | ✅  | ❌  | ❌    | 1  | 随机获取单个容器，忽略限流          |
| `wait_acquire_random_containers(quantity, timeout)` | ✅  | ✅  | ❌    | N  | 阻塞等待 N 个随机容器，忽略限流      |
| `wait_acquire_random_container(timeout)`            | ✅  | ✅  | ❌    | 1  | 阻塞等待单个随机容器，忽略限流        |

---

### 写入账号（生产者）

## LimitedModel 说明

`LimitedModel` 用于表示受限访问（rate-limited）的容器条目，每条数据包含唯一标识、时间戳、TTL、访问间隔和访问计数等信息。

### 字段说明

| 字段                    | 类型    | 默认值       | 描述                  |
|-----------------------|-------|-----------|---------------------|
| `id`                  | `str` | —         | 数据唯一标识符，必须提供        |
| `ct`                  | `int` | 当前时间戳（毫秒） | 记录条目的收集时间，无需设置      |
| `ttl_ms`              | `int` | `0`       | 条目的生存时间（毫秒），0 表示无限期 |
| `interval_ms`         | `int` | `0`       | 访问间隔（毫秒），0 表示不限速    |
| `next_time_available` | `int` | 当前时间戳（毫秒） | 下次可访问时间，用于限流控制，无需设置 |
| `containers`          | `str` | `None`    | 存储容器数据，可为空          |
| `usage_count`         | `int` | `0`       | 条目被访问的次数，无需设置       |
### 使用说明

- `ct` 和 `next_time_available` 都以毫秒为单位，便于高精度限流。
- `interval_ms > 0` 时，访问条目会更新 `next_time_available` 为当前时间 + 间隔。
- 每次访问条目时，都会通过 `usage_count` 记录被访问次数，以统计和调度。
- `ttl_ms` 可配合 Redis 等存储设置过期时间，实现自动清理。

```python
import random
from redis_xsync import register_redis
from redis_xsync.rtypes import LimitedModel

redis = register_redis(label="redis_xsync", host="127.0.0.1", port=6379)
Limited = redis.Limited


async def produce_container():
    async with Limited(redis_key="crawler:identity:flow_limit") as lt:
        for i in range(10):
            token = LimitedModel(
                id=f"user_{i}",
                ttl_ms=random.randint(30000, 60000),
                interval_ms=random.randint(1000, 6000),
                containers="cookies"
            )
            await lt.set_available_containers(token)
```

---

### 获取账号（消费者，阻塞等待）

```python
async def consume_token():
    async with Limited(redis_key="crawler:identity:flow_limit") as lt:
        # 等待可用 token，超时 10 秒
        token = await lt.wait_ask_available_containers(quantity=1, timeout=10)
        print("获取到账号:", token)
```

---

### 同步调用

```python
def consume_sync():
    with Limited(redis_key="crawler:identity:flow_limit") as lt:
        token = lt.wait_ask_available_containers(quantity=1, timeout=10)
        print("获取到账号:", token)
```

## ⚡ 说明：

- produce_container 写入 token 到 Redis，带过期时间和间隔限流
- consume_token 或 consume_sync 从 Redis 中获取可用 token，如果当前没有可用，会等待直到 timeout
- 支持异步和同步，且保证跨进程 / 跨机器原子性

---

## 🧠 设计说明

### 🔐 分布式锁

* 基于 Redis 原子操作
* 支持多 key（组合锁）
* 防止并发冲突

---

### 🚦 限流模型

* 每个 token：

    * 独立 TTL
    * 独立间隔（interval）
* Lua 保证原子性
* 支持多进程 / 多机器共享

---

## ⚠️ 注意事项

### Windows 建议

```python
import asyncio

asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
```

---

## 📌 适用场景

* 单机非集群
* 爬虫账号池
* 接口限流
* 分布式任务调度
* 多服务并发控制

---

## 🤝 贡献

欢迎 Issue / PR！

---

## 📄 License

MIT License
