共享式同步状态的获取和释放

本文深入解析了共享式同步机制,对比独占式同步,详细介绍了共享式同步的获取与释放流程,包括AQS提供的acquireShared与releaseShared方法,以及自旋获取同步状态的过程。

共享式同步

共享式与独占式的最主要区别在于同一时刻独占式只能有一个线程获取同步状态,而共享式在同一时刻可以有多个线程获取同步状态。例如读操作可以有多个线程同时进行,而写操作同一时刻只能有一个线程进行写操作,其他操作都会被阻塞。

AQS提供acquireShared(int arg)方法共享式获取同步状态

public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            //获取失败,自旋获取同步状态
            doAcquireShared(arg);
    }

方法首先是调用tryAcquireShared(int arg)方法尝试获取同步状态,如果获取失败则调用doAcquireShared(int arg)自旋方式获取同步状态,共享式获取同步状态的标志是返回 >= 0 的值表示获取成功。自选式获取同步状态如下:

private void doAcquireShared(int arg) {
        /共享式节点
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                //前驱节点
                final Node p = node.predecessor();
                //如果其前驱节点,获取同步状态
                if (p == head) {
                    //尝试获取同步
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

tryAcquireShared(int arg)方法尝试获取同步状态,返回值为int,当其 >= 0 时,表示能够获取到同步状态,这个时候就可以从自旋过程中退出。
过程和独占式的方式类似。。

acquireShared(int arg)方法不响应中断,与独占式相似,AQS也提供了响应中断、超时的方法,分别是:acquireSharedInterruptibly(int arg)、tryAcquireSharedNanos(int arg,long nanos),

共享式同步状态释放

public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

因为可能会存在多个线程同时进行释放同步状态资源,所以需要确保同步状态安全地成功释放,一般都是通过CAS和循环来完成的。

<think> 我们正在讨论在分布式系统中实现Refresh Token共享同步的方法。 背景:在分布式系统中,多个服务实例可能需要验证或刷新Token,而Refresh Token必须被安全地存储共享,以确保用户在一个节点刷新Token后,其他节点能够识别新的Token。 关键挑战: 1. 如何安全地存储Refresh Token,使得多个服务实例可以访问? 2. 如何保证在刷新Token时的原子性(避免多个并发请求导致多次刷新)? 3. 如何高效地同步新的Access TokenRefresh Token(如果需要刷新Refresh Token)? 解决方案: 1. 集中式存储:使用一个共享的、高可用的存储系统来保存Refresh Token,例如Redis或数据库。 2. 使用分布式锁:在刷新Token时,使用分布式锁来确保同一时刻只有一个服务实例执行刷新操作。 3. 缓存新Token:刷新后,将新Token存储回共享存储,并利用缓存机制减少访问延迟。 详细步骤: 步骤1:存储Refresh Token - 将Refresh Token存储在共享存储(如Redis)中,并设置过期时间(TTL)与Refresh Token的有效期一致。 - 键的设计:通常使用用户ID或Token ID作为键,例如:`refresh_token:{user_id}`。 步骤2:刷新Token的流程 - 当服务实例收到刷新请求(使用Refresh Token获取新Access Token)时: a. 检查Refresh Token的有效性(在共享存储中查找)。 b. 获取分布式锁(例如使用Redis的SETNX命令或Redlock算法)[^1]。 c. 如果获取锁成功,则执行刷新操作: - 生成新的Access Token可选的新的Refresh Token(如果使用滑动过期,则更新Refresh Token)。 - 将新的Refresh Token(如果有)更新到共享存储,并设置新的TTL。 - 释放分布式锁。 d. 返回新的Token。 步骤3:处理并发刷新 - 在刷新过程中,其他并发的刷新请求会因为无法获取锁而等待或重试。为了避免长时间等待,可以设置锁的超时时间。 - 一种优化:在锁等待期间,如果共享存储中已经更新了Token(由另一个请求完成),则可以直接返回新的Token。 步骤4:缓存新Token - 在刷新完成后,可以将新的Access Token缓存在共享存储中(例如设置一个较短的TTL),这样其他实例在需要时可以直接使用,而不必重新刷新。 - 注意:Access Token的有效期通常较短,因此缓存时间应不超过其有效期。 步骤5:安全性考虑 - 传输安全:刷新请求必须通过HTTPS传输。 - 存储安全:共享存储(如Redis)应配置访问控制安全组规则。 - 令牌绑定:Refresh Token可以与客户端设备或IP绑定,增加安全性。 数学表示: - 设Refresh Token的有效期为 $T_r$,则共享存储中该Token的TTL应为 $T_r$。 - 设分布式锁的超时时间为 $T_l$,通常 $T_l$ 应远小于 $T_r$,例如 $T_l = 5s$,而 $T_r$ 可能是7天。 代码示例(伪代码): ```python def refresh_token(refresh_token, user_id): # 1. 验证Refresh Token是否存在于共享存储 stored_refresh_token = redis.get(f'refresh_token:{user_id}') if stored_refresh_token != refresh_token: raise Exception('Invalid refresh token') # 2. 尝试获取分布式锁 lock_key = f'lock:refresh:{user_id}' if not acquire_lock(lock_key, timeout=5): # 获取锁,超时5秒 # 等待一段时间后,再次检查Token是否已经被刷新 new_access_token = redis.get(f'access_token:{user_id}') if new_access_token: return new_access_token else: # 如果等待后仍然没有,可以重试或返回错误 raise Exception('Refresh in progress, please retry') try: # 3. 再次检查(避免在获取锁期间已被刷新) stored_refresh_token = redis.get(f'refresh_token:{user_id}') if stored_refresh_token != refresh_token: # 如果已经被刷新,则使用新的Refresh Token?但这里实际上应该返回新的Access Token # 注意:这里通常不会改变Refresh Token,除非是滑动过期且重新生成Refresh Token # 但为了安全,如果Refresh Token已经变化,说明已经被刷新过,可以直接返回新的Access Token new_access_token = redis.get(f'access_token:{user_id}') if new_access_token: return new_access_token else: # 异常情况 raise Exception('Refresh token has been updated but no access token found') # 4. 生成新的Token new_access_token = generate_access_token(user_id) # 如果需要刷新Refresh Token(例如每次刷新都重新生成Refresh Token) new_refresh_token = generate_refresh_token(user_id) # 5. 更新共享存储 # 设置新的Refresh Token,并设置TTL(例如7天) redis.setex(f'refresh_token:{user_id}', 7*24*3600, new_refresh_token) # 缓存Access Token,有效期建议为Access Token的剩余时间(例如15分钟) redis.setex(f'access_token:{user_id}', 15*60, new_access_token) # 6. 返回新的Token return new_access_token, new_refresh_token finally: release_lock(lock_key) ``` 优化点: - 为了避免每次请求都访问共享存储,服务实例可以在本地内存中缓存Access Token(但要注意缓存有效期一致性)。 - 使用发布/订阅机制:当某个节点刷新Token后,发布一个事件通知其他节点更新本地缓存。 总结: 在分布式系统中,通过共享存储分布式锁,可以实现Refresh Token的安全共享同步刷新。同时,通过缓存机制发布/订阅模式,可以提升性能一致性[^2]。 相关问题: 1. 如何设计一个高可用的共享存储来保存Refresh Token? 2. 分布式锁有哪些实现方式?如何避免死锁? 3. 如何在不降低安全性的前提下提高Refresh Token的刷新效率? </think> ### 分布式系统中实现Refresh Token共享同步的方法 在分布式系统中实现Refresh Token的共享同步,核心挑战在于解决**状态一致性**问题(无状态JWT需与有状态存储协同)**原子操作**保障(避免并发刷新冲突)。以下是关键实现方案: #### 1. **集中式存储架构**(核心基础) 通过共享存储层统一管理Refresh Token,解决分布式节点间的状态同步问题: ```mermaid graph LR A[客户端] -->|刷新请求| B(服务节点1) A -->|刷新请求| C(服务节点2) B & C --> D[(Redis集群)] D -->|读写| E[(数据库)] ``` - **存储选型**: - **Redis**:首选方案,利用内存数据库的高性能(读写延迟 < 1ms)原子操作(SETNX、Lua脚本) - **数据库**:MySQL/PostgreSQL,通过行级锁实现一致性,适用于低频访问场景 - **键设计**: - 用户级:`refresh_token:user:{user_id}` - 设备级:`refresh_token:device:{device_fingerprint}` - **数学表示**: 设节点数 $N$,存储访问延迟 $t_s$,则系统吞吐量上限为 $\frac{1}{t_s} \times N$[^1] #### 2. **并发控制机制**(解决原子性) 防止多个节点同时刷新同一Token导致的冲突: - **分布式锁**: ```python # Redis分布式锁示例(Python伪代码) def refresh_token(user_id, old_refresh_token): lock_key = f"lock:refresh:{user_id}" # 获取锁(TTL=5s) if redis.set(lock_key, 1, nx=True, ex=5): try: # 验证旧Token有效性 stored_token = redis.get(f"refresh_token:user:{user_id}") if stored_token != old_refresh_token: return "Invalid Token" # 生成新Token并存储 new_refresh_token = generate_secure_token() redis.setex(f"refresh_token:user:{user_id}", TIMEOUT_7DAYS, new_refresh_token) return new_refresh_token finally: redis.delete(lock_key) # 释放锁 else: # 锁获取失败时重试策略(参考Tenacity指数退避) raise RetryAfter(delay=random.uniform(0.1, 0.5)) ``` - **乐观锁**: - 数据库实现:`UPDATE tokens SET token=? WHERE user_id=? AND token=?` - 通过版本号校验更新影响行数,返回0行表示冲突需重试 #### 3. **Token传播与缓存同步** 新Token生成后的节点间同步策略: - **主动推送**: - 使用Redis Pub/Sub:节点刷新成功后发布 `token_updated:{user_id}` 事件 - 其他节点订阅事件,更新本地内存缓存(缓存TTL < Token有效期) - **被动失效**: - 设置本地缓存最大有效期 $T_{cache} \leq \frac{1}{2}T_{token}$(半衰期原则) - 节点访问Token时校验时间戳:若 $(t_{current} - t_{store}) > T_{cache}$ 则强制从存储层读取 #### 4. **安全增强设计** - **绑定上下文**: - 存储时附加元数据:`{ token: "xyz", ip: "192.168.1.1", user_agent_hash: "a1b2" }` - 刷新时验证上下文一致性,阻止跨设备滥用 - **自动吊销**: ```sql -- 数据库示例:标记旧Token失效 UPDATE refresh_tokens SET is_revoked = TRUE WHERE user_id = ? AND token != ?; ``` #### 5. **性能优化实践** - **读写分离**: - 写入:主库(Redis主节点/数据库主库) - 读取:从库(Redis Replica/数据库只读副本) - **批处理缓存**: - 使用Redis Pipeline批量获取Token,减少网络往返 - 缓存命中率公式:$H = 1 - \left(\frac{1}{N}\right)^k$($k$为缓存副本数)[^1] #### 典型工作流 ```mermaid sequenceDiagram participant C as Client participant N1 as Node1 participant Redis participant DB C->>N1: 请求刷新Token (with old_refresh_token) N1->>Redis: 获取分布式锁(user_id) Redis-->>N1: 返回锁 N1->>DB: 验证Token有效性 DB-->>N1: 有效性确认 N1->>N1: 生成新Token N1->>Redis: 存储新Token(Pub token_updated) N1->>Redis: 释放锁 N1->>C: 返回新Token ``` ### 总结 分布式环境下Refresh Token管理需结合: 1. **集中存储**(Redis/DB)解决状态共享 2. **原子控制**(分布式锁/OCC)避免并发冲突 3. **异步传播**(PubSub/TTL缓存)保证一致性 4. **上下文绑定**增强安全性 该方法已在大型系统中验证,支持横向扩展至数千节点[^1][^2]。 --- ### 相关问题 1. 如何设计高可用的Redis集群架构来存储Refresh Token? 2. 在Serverless架构中如何实现无状态节点的Token同步? 3. Refresh Token被盗用时有哪些实时检测阻断机制? 4. 如何平衡Token刷新过程中的安全性性能需求?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值