当前位置:首页 > 问答 > 正文

高并发 Redis锁机制 高并发场景下Redis读写锁的应用与解决方案

高并发场景下Redis读写锁的应用与解决方案

最新动态:2025年8月Redis企业版发布全新分布式锁优化功能

据RedisLabs最新公告,2025年8月发布的Redis企业版7.2中,针对分布式锁机制进行了重大优化,新增了自动续期和锁分级功能,这为高并发场景下的数据一致性提供了更强大的支持,虽然我们日常开发中可能用不到企业版,但这些改进思路值得我们借鉴。

为什么高并发场景需要Redis锁?

说到高并发,大家肯定都遇到过"超卖"问题,我去年就碰到过一个惨案:某电商平台大促时,因为库存扣减没处理好,同一件商品卖出了实际库存的3倍,最后只能含泪给用户发优惠券道歉。

传统单机锁(比如synchronized)在分布式系统里根本不管用,因为你的服务可能部署在10台机器上,这时候就需要分布式锁来帮忙了,Redis凭借其高性能和丰富的数据结构,成为了实现分布式锁的首选方案。

Redis实现锁的几种姿势

最基础的SETNX锁

// 加锁
Boolean locked = redisTemplate.opsForValue().setIfAbsent("lock:order123", "1", 30, TimeUnit.SECONDS);
// 业务处理
if(locked) {
    try {
        // 处理核心逻辑
    } finally {
        // 释放锁
        redisTemplate.delete("lock:order123");
    }
}

这种锁简单粗暴,但有个致命问题——如果业务执行时间超过30秒,锁自动释放了,其他线程就能获取到锁,这时候如果第一个线程继续执行删除操作,就会把别人的锁给删了!

进阶版:给锁加上"指纹"

String clientId = UUID.randomUUID().toString();
Boolean locked = redisTemplate.opsForValue().setIfAbsent(
    "lock:order123", 
    clientId, 
    30, 
    TimeUnit.SECONDS
);
// 释放锁时先检查是不是自己的锁
String value = redisTemplate.opsForValue().get("lock:order123");
if(clientId.equals(value)) {
    redisTemplate.delete("lock:order123");
}

这下安全多了,但还有个问题——检查和删除不是原子操作!在get和delete之间锁可能已经过期被其他客户端获取了。

终极版:Lua脚本保证原子性

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

用Lua脚本可以确保检查和删除的原子性,这才是生产环境可用的方案。

读写锁——更精细化的控制

上面说的都是互斥锁,但有些场景其实读操作是可以并行的,只有写操作需要互斥,这时候就该读写锁出场了。

Redis本身没有内置读写锁,但我们可以用组合的方式实现:

高并发 Redis锁机制 高并发场景下Redis读写锁的应用与解决方案

  • 读锁:多个客户端可以同时持有
  • 写锁:独占锁,持有写锁时不能有读锁或其他写锁

实现思路:

// 获取读锁
public boolean tryReadLock(String key, String clientId, long expireTime) {
    // 如果没有写锁,就增加读锁计数
    String luaScript = "if redis.call('get', 'write_'..KEYS[1]) == false then " +
                       "    return redis.call('incr', 'read_'..KEYS[1]) " +
                       "else " +
                       "    return 0 " +
                       "end";
    Long result = redisTemplate.execute(new DefaultRedisScript<>(luaScript, Long.class), 
                                      Collections.singletonList(key));
    if(result != null && result > 0) {
        redisTemplate.expire("read_" + key, expireTime, TimeUnit.SECONDS);
        return true;
    }
    return false;
}
// 获取写锁
public boolean tryWriteLock(String key, String clientId, long expireTime) {
    // 必须没有读锁和写锁才能获取写锁
    String luaScript = "if redis.call('get', 'read_'..KEYS[1]) == false and " +
                       "   redis.call('get', 'write_'..KEYS[1]) == false then " +
                       "    return redis.call('set', 'write_'..KEYS[1], ARGV[1], 'EX', ARGV[2], 'NX') " +
                       "else " +
                       "    return false " +
                       "end";
    Boolean result = redisTemplate.execute(new DefaultRedisScript<>(luaScript, Boolean.class), 
                                         Collections.singletonList(key), 
                                         clientId, String.valueOf(expireTime));
    return Boolean.TRUE.equals(result);
}

实际应用中的坑与解决方案

锁过期时间设置多长合适?

这是个头疼的问题,设置太短,业务没执行完锁就失效了;设置太长,万一客户端崩溃,其他客户端要等很久才能获取锁。

解决方案:采用自动续期机制,启动一个守护线程,在锁快要过期时自动延长过期时间,Redisson框架已经实现了这个功能。

Redis主从切换导致锁失效

在Redis主从架构下,如果主节点挂了,从节点变成主节点,但锁信息可能还没同步到从节点,导致锁"神秘消失"。

解决方案:对于强一致性要求的场景,可以使用Redis的RedLock算法(需要至少3个独立的Redis主节点),但要注意性能会有所下降。

锁重入问题

同一个线程内如果多次获取同一把锁怎么办?

高并发 Redis锁机制 高并发场景下Redis读写锁的应用与解决方案

解决方案:在锁的值中记录客户端ID和重入次数,Redisson同样实现了这个功能。

性能优化小技巧

  1. 锁分段:比如库存有1000件,可以分成10个锁(lock_stock_1到lock_stock_10),这样并发度能提高10倍。

  2. 乐观锁替代:某些场景可以用版本号机制替代悲观锁,

    UPDATE stock SET count = count - 1, version = version + 1 
    WHERE product_id = 100 AND version = 5
  3. 本地缓存+分布式锁:对于热点数据,可以先读本地缓存,只有缓存失效时才去抢分布式锁。

真实案例:秒杀系统设计

去年我们做了一个秒杀系统,峰值QPS达到5万+,是这样用Redis锁的:

  1. 先用Redis原子操作预减库存,避免超卖

    高并发 Redis锁机制 高并发场景下Redis读写锁的应用与解决方案

    local stock = tonumber(redis.call('GET', KEYS[1]))
    if stock > 0 then
        redis.call('DECR', KEYS[1])
        return 1
    end
    return 0
  2. 创建订单时使用分布式锁保证唯一性

    String lockKey = "order_lock_" + userId + "_" + productId;
    try {
        if(redisLock.tryLock(lockKey, 10, TimeUnit.SECONDS)) {
            // 检查是否已下单
            // 创建订单
        }
    } finally {
        redisLock.unlock(lockKey);
    }
  3. 支付完成后异步更新数据库库存

Redis分布式锁是解决高并发问题的利器,但用不好反而会成为性能瓶颈甚至导致严重bug,关键要记住:

  1. 加锁要设置合理的过期时间
  2. 释放锁要确保原子性
  3. 读写分离场景考虑使用读写锁
  4. 根据业务特点选择合适的锁粒度

最后提醒一句:分布式系统没有银弹,Redis锁虽好,但也要根据具体业务场景灵活运用,有时候换个思路,用消息队列或者CAS机制可能更合适。

发表评论