redis 秒杀 分布式 锁

用大白话的方式,带你彻底搞懂Redis分布式锁!

大家好!今天我想通过一个程序员小徐的故事,给大家讲讲分布式锁的那些事儿。

故事开始:初出茅庐的程序员小徐

小徐刚工作不久,公司让他开发一个秒杀功能。他分析后发现很简单:

  • 检查商品库存是否大于0
  • 如果大于0,就扣减库存
  • 否则秒杀失败

代码写完后,小徐信心满满地上线了。结果悲剧了------有个商品库存只有1件,却卖出了好几份!

因为这个bug,小徐这个月的KPI直接扣光了。

问题分析

大家应该都看出来了,在多线程并发的情况下,如果同时对一个共享资源进行读写,会造成数据错乱的问题。

小徐也意识到了这个问题,于是他对代码进行了改造------加了一把同步锁

上线前他专门做了压测,这次终于不会超卖了!上线后也确实运行正常。

新的挑战:性能瓶颈

我们知道,在多线程情况下加了同步锁,其他线程需要排队等待,只有持有锁的线程处理完后,其他线程才能处理。

但随着用户越来越多,小徐发现服务器压力越来越大,性能达到了瓶颈。

不过他不慌,因为他学过Nginx负载均衡 技术。他把服务器进行了水平扩容,通过Nginx进行分布式集群部署。

压测时发现吞吐量确实上来了,但秒杀功能又出现了超卖问题!

分布式环境的困境

经过研究,小徐发现问题出在同步锁上。因为同步锁是JVM级别的,只能锁住单个进程。经过分布式部署后,每台服务器在并发情况下只能锁住一个线程。

解决方案:分布式锁

经过一晚上的研究,小徐发现可以通过分布式锁技术来解决。

目前主流的分布式锁技术有Redis和ZooKeeper。由于他的系统已经用到了Redis,为了不增加额外服务器的成本,他决定用Redis来实现分布式锁。

他发现通过Redis的SETNX命令就可以简单实现分布式锁:

  • 当一个线程通过SETNX去存储一个key时,如果key不存在,就会存入并返回true(加锁成功)
  • 另一个线程通过SETNX存储同一个key时,发现有值,就会返回false(加锁失败)

通过这个特性,就可以实现分布式锁。

重要细节:避免死锁

但小徐在测试时发现了一个问题:如果用户在请求过程中,服务器挂掉了,那么其他服务器的正常请求会出现阻塞,因为key会一直存在,造成死锁现象。

所以一定要加过期时间!这样如果服务器挂掉了,经过这个时间后锁会自动释放,不影响其他服务器的正常请求。

新的问题:锁过期时间

随着业务扩展,又暴露出了新问题:

  • 当业务处理时间超过了锁的过期时间,业务还没处理完,锁就自动释放了
  • 其他线程就会趁机涌入
  • 而原线程处理完后,释放的其实是线程二的锁
  • 其他线程又会趁机涌入...

这样又会造成超卖问题!

终极解决方案

小徐针对这两个问题分别做了处理:

问题一:锁过期时间不足

  • 增加了锁的过期时间
  • 增加了守护方案:在业务代码中添加子线程,每10秒去确认主线程是否还在运行,如果在运行就延长过期时间(给锁续命)

问题二:释放了其他线程的锁

  • 给锁增加了唯一ID(UID),保证每把锁的key与当前线程绑定,这样就不会释放其他线程的锁

使用Redis官方组件

小徐发现自己的实现代码很复杂,还要保证健壮性,否则某个细节不注意就会出现bug。

于是他想,Redis有没有提供相关的组件来完成这些功能呢?

还真有!Redis提供了Redisson组件来实现分布式锁,使用起来非常简单:

  1. 添加Redisson依赖
  2. 通过Redis客户端自动装配
  3. 通过lock.lock()就可以实现Redis分布式锁

Redis分布式锁原理

多个线程同时请求竞争锁,只有一个线程能获取到锁。

获取到锁的线程,其key由UID和线程ID组合,保证key与当前线程绑定。

处理业务时,内部有一个看门狗任务,每10秒检查当前线程是否还持有锁,如果持有就延长过期时间(续命)。

如果没有获取到锁,会进行自旋,一直尝试获取锁直到超时为止。

集群环境的问题

但使用Redis集群时还有个问题:如果Redis使用主从集群模式,主节点挂掉了,因为Redis采用AP模式(保证高可用和高性能,但不能保证强一致性),设置锁时只会往主节点设置,设置完就告诉你成功,然后内部同步。

如果你设置锁时主节点正好挂掉了,从节点没有同步到这把锁,此时依然会发生线程不安全、超卖等问题。

最终方案:Redlock

Redis提供了Redlock来解决这个问题。Redlock针对Redis所有节点进行同步,保证所有节点都存储完成后才会返回存储成功,这样就保证了强一致性。

总结

从小徐的故事中,我们学到了:

  1. 单机同步锁解决单机并发问题
  2. 分布式锁解决分布式环境并发问题
  3. 过期时间避免死锁
  4. 锁续命解决业务执行时间过长问题
  5. 唯一标识避免释放其他线程的锁
  6. Redisson简化分布式锁实现
  7. Redlock解决集群环境的一致性问题

Redis分布式锁完整代码实现

1. 线程不安全的原始代码

java 复制代码
@Service
public class UnsafeSeckillService {
    
    @Autowired
    private ProductRepository productRepository;
    
    /**
     * 线程不安全的秒杀实现
     */
    public boolean seckill(Long productId) {
        // 查询商品库存
        Product product = productRepository.findById(productId);
        if (product.getStock() > 0) {
            // 模拟业务处理时间
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 扣减库存
            product.setStock(product.getStock() - 1);
            productRepository.update(product);
            System.out.println("秒杀成功,剩余库存:" + product.getStock());
            return true;
        }
        System.out.println("秒杀失败,库存不足");
        return false;
    }
}

2. 单机同步锁解决方案

java 复制代码
@Service
public class SynchronizedSeckillService {
    
    @Autowired
    private ProductRepository productRepository;
    
    private final Object lock = new Object();
    
    /**
     * 使用synchronized保证线程安全
     */
    public boolean seckill(Long productId) {
        synchronized (lock) {
            Product product = productRepository.findById(productId);
            if (product.getStock() > 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                product.setStock(product.getStock() - 1);
                productRepository.update(product);
                System.out.println(Thread.currentThread().getName() + " 秒杀成功,剩余库存:" + product.getStock());
                return true;
            }
            System.out.println("秒杀失败,库存不足");
            return false;
        }
    }
}

3. Redis原生分布式锁实现

3.1 分布式锁工具类

java 复制代码
@Component
public class RedisDistributedLock {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    private static final String LOCK_PREFIX = "DISTRIBUTED_LOCK:";
    private static final long DEFAULT_EXPIRE_TIME = 30000; // 30秒
    private static final long DEFAULT_WAIT_TIME = 5000; // 5秒等待时间
    
    /**
     * 获取分布式锁(简单版本 - 有问题)
     */
    public boolean tryLockSimple(String lockKey) {
        String key = LOCK_PREFIX + lockKey;
        Boolean result = redisTemplate.opsForValue().setIfAbsent(key, "1");
        return Boolean.TRUE.equals(result);
    }
    
    /**
     * 获取分布式锁(带过期时间)
     */
    public boolean tryLock(String lockKey) {
        return tryLock(lockKey, DEFAULT_EXPIRE_TIME);
    }
    
    public boolean tryLock(String lockKey, long expireTime) {
        String key = LOCK_PREFIX + lockKey;
        String value = generateLockValue();
        
        Boolean result = redisTemplate.opsForValue().setIfAbsent(key, value, expireTime, TimeUnit.MILLISECONDS);
        return Boolean.TRUE.equals(result);
    }
    
    /**
     * 释放分布式锁
     */
    public boolean unlock(String lockKey) {
        String key = LOCK_PREFIX + lockKey;
        redisTemplate.delete(key);
        return true;
    }
    
    /**
     * 安全的释放锁 - 检查锁的value
     */
    public boolean unlockSafely(String lockKey, String lockValue) {
        String key = LOCK_PREFIX + lockKey;
        
        // 使用Lua脚本保证原子性
        String luaScript = 
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "    return redis.call('del', KEYS[1]) " +
            "else " +
            "    return 0 " +
            "end";
        
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptText(luaScript);
        redisScript.setResultType(Long.class);
        
        Long result = redisTemplate.execute(redisScript, Collections.singletonList(key), lockValue);
        return result != null && result == 1;
    }
    
    /**
     * 生成锁的唯一值
     */
    private String generateLockValue() {
        return Thread.currentThread().getId() + ":" + UUID.randomUUID().toString();
    }
    
    /**
     * 阻塞式获取锁
     */
    public boolean lock(String lockKey) throws InterruptedException {
        return lock(lockKey, DEFAULT_WAIT_TIME, DEFAULT_EXPIRE_TIME);
    }
    
    public boolean lock(String lockKey, long waitTime, long expireTime) throws InterruptedException {
        String lockValue = generateLockValue();
        long endTime = System.currentTimeMillis() + waitTime;
        
        while (System.currentTimeMillis() < endTime) {
            if (tryLock(lockKey, expireTime)) {
                return true;
            }
            Thread.sleep(100); // 休眠100ms后重试
        }
        return false;
    }
}

3.2 带锁续命的分布式锁

java 复制代码
@Component
public class RedisLockWithRenewal {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    private static final String LOCK_PREFIX = "DISTRIBUTED_LOCK:";
    private static final long DEFAULT_EXPIRE_TIME = 30000; // 30秒
    private final Map<String, Timer> renewalTimers = new ConcurrentHashMap<>();
    
    /**
     * 获取分布式锁(带自动续期)
     */
    public boolean tryLockWithRenewal(String lockKey) {
        String key = LOCK_PREFIX + lockKey;
        String lockValue = generateLockValue();
        
        Boolean result = redisTemplate.opsForValue().setIfAbsent(key, lockValue, DEFAULT_EXPIRE_TIME, TimeUnit.MILLISECONDS);
        
        if (Boolean.TRUE.equals(result)) {
            // 启动续期任务
            startRenewalTask(lockKey, lockValue);
            return true;
        }
        return false;
    }
    
    /**
     * 启动锁续期任务
     */
    private void startRenewalTask(String lockKey, String lockValue) {
        Timer timer = new Timer("LockRenewal-" + lockKey, true);
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                if (!renewLock(lockKey, lockValue)) {
                    // 续期失败,取消任务
                    this.cancel();
                    renewalTimers.remove(lockKey);
                }
            }
        }, 10000, 10000); // 10秒后开始执行,每10秒执行一次
        
        renewalTimers.put(lockKey, timer);
    }
    
    /**
     * 续期锁
     */
    private boolean renewLock(String lockKey, String lockValue) {
        String key = LOCK_PREFIX + lockKey;
        
        String luaScript = 
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "    return redis.call('expire', KEYS[1], ARGV[2]) " +
            "else " +
            "    return 0 " +
            "end";
        
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptText(luaScript);
        redisScript.setResultType(Long.class);
        
        Long result = redisTemplate.execute(redisScript, 
            Collections.singletonList(key), 
            lockValue, 
            String.valueOf(DEFAULT_EXPIRE_TIME / 1000));
        
        return result != null && result == 1;
    }
    
    /**
     * 释放锁(停止续期任务)
     */
    public boolean unlockWithRenewal(String lockKey, String lockValue) {
        // 停止续期任务
        Timer timer = renewalTimers.remove(lockKey);
        if (timer != null) {
            timer.cancel();
        }
        
        // 释放锁
        return unlockSafely(lockKey, lockValue);
    }
    
    private boolean unlockSafely(String lockKey, String lockValue) {
        String key = LOCK_PREFIX + lockKey;
        
        String luaScript = 
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "    return redis.call('del', KEYS[1]) " +
            "else " +
            "    return 0 " +
            "end";
        
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptText(luaScript);
        redisScript.setResultType(Long.class);
        
        Long result = redisTemplate.execute(redisScript, Collections.singletonList(key), lockValue);
        return result != null && result == 1;
    }
    
    private String generateLockValue() {
        return Thread.currentThread().getId() + ":" + UUID.randomUUID().toString();
    }
}

3.3 使用Redis分布式锁的秒杀服务

java 复制代码
@Service
public class RedisLockSeckillService {
    
    @Autowired
    private ProductRepository productRepository;
    
    @Autowired
    private RedisLockWithRenewal redisLock;
    
    private static final String SECKILL_LOCK_PREFIX = "SECKILL:";
    
    /**
     * 使用Redis分布式锁的秒杀实现
     */
    public boolean seckill(Long productId) {
        String lockKey = SECKILL_LOCK_PREFIX + productId;
        String lockValue = null;
        
        try {
            // 尝试获取锁
            if (!redisLock.tryLockWithRenewal(lockKey)) {
                System.out.println(Thread.currentThread().getName() + " 获取锁失败");
                return false;
            }
            
            lockValue = Thread.currentThread().getId() + ":" + "当前锁值需要从Redis获取"; // 实际使用时需要保存lockValue
            
            // 执行业务逻辑
            Product product = productRepository.findById(productId);
            if (product.getStock() > 0) {
                // 模拟业务处理时间
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                
                product.setStock(product.getStock() - 1);
                productRepository.update(product);
                System.out.println(Thread.currentThread().getName() + " 秒杀成功,剩余库存:" + product.getStock());
                return true;
            }
            
            System.out.println("秒杀失败,库存不足");
            return false;
            
        } finally {
            // 释放锁
            if (lockValue != null) {
                redisLock.unlockWithRenewal(lockKey, lockValue);
            }
        }
    }
}

4. 使用Redisson实现分布式锁

4.1 Redisson配置

java 复制代码
@Configuration
public class RedissonConfig {
    
    @Value("${spring.redis.host:localhost}")
    private String redisHost;
    
    @Value("${spring.redis.port:6379}")
    private String redisPort;
    
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer()
              .setAddress("redis://" + redisHost + ":" + redisPort)
              .setDatabase(0);
        return Redisson.create(config);
    }
}

4.2 使用Redisson的秒杀服务

java 复制代码
@Service
public class RedissonSeckillService {
    
    @Autowired
    private ProductRepository productRepository;
    
    @Autowired
    private RedissonClient redissonClient;
    
    /**
     * 使用Redisson分布式锁的秒杀实现
     */
    public boolean seckill(Long productId) {
        String lockKey = "SECKILL_LOCK:" + productId;
        RLock lock = redissonClient.getLock(lockKey);
        
        try {
            // 尝试加锁,最多等待5秒,上锁后30秒自动解锁
            boolean isLocked = lock.tryLock(5, 30, TimeUnit.SECONDS);
            if (!isLocked) {
                System.out.println(Thread.currentThread().getName() + " 获取锁失败");
                return false;
            }
            
            // 执行业务逻辑
            Product product = productRepository.findById(productId);
            if (product.getStock() > 0) {
                // 模拟业务处理时间
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                
                product.setStock(product.getStock() - 1);
                productRepository.update(product);
                System.out.println(Thread.currentThread().getName() + " 秒杀成功,剩余库存:" + product.getStock());
                return true;
            }
            
            System.out.println("秒杀失败,库存不足");
            return false;
            
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

5. 测试代码

java 复制代码
@SpringBootTest
class SeckillTest {
    
    @Autowired
    private UnsafeSeckillService unsafeSeckillService;
    
    @Autowired
    private SynchronizedSeckillService synchronizedSeckillService;
    
    @Autowired
    private RedisLockSeckillService redisLockSeckillService;
    
    @Autowired
    private RedissonSeckillService redissonSeckillService;
    
    /**
     * 测试线程不安全的秒杀
     */
    @Test
    void testUnsafeSeckill() throws InterruptedException {
        int threadCount = 100;
        CountDownLatch latch = new CountDownLatch(threadCount);
        
        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                unsafeSeckillService.seckill(1L);
                latch.countDown();
            }).start();
        }
        
        latch.await();
        System.out.println("线程不安全测试完成");
    }
    
    /**
     * 测试Redis分布式锁
     */
    @Test
    void testRedisLockSeckill() throws InterruptedException {
        int threadCount = 100;
        CountDownLatch latch = new CountDownLatch(threadCount);
        
        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                redisLockSeckillService.seckill(1L);
                latch.countDown();
            }).start();
        }
        
        latch.await();
        System.out.println("Redis分布式锁测试完成");
    }
    
    /**
     * 测试Redisson分布式锁
     */
    @Test
    void testRedissonSeckill() throws InterruptedException {
        int threadCount = 100;
        CountDownLatch latch = new CountDownLatch(threadCount);
        
        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                redissonSeckillService.seckill(1L);
                latch.countDown();
            }).start();
        }
        
        latch.await();
        System.out.println("Redisson分布式锁测试完成");
    }
}

6. Maven依赖

xml 复制代码
<dependencies>
    <!-- Spring Boot Starter Data Redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
    <!-- Redisson -->
    <dependency>
        <groupId>org.redisson</groupId>
        <artifactId>redisson-spring-boot-starter</artifactId>
        <version>3.17.0</version>
    </dependency>
    
    <!-- 测试依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

关键点总结

  1. 基础版本问题:简单的SETNX没有过期时间,会导致死锁
  2. 改进版本:添加过期时间,但可能业务未完成锁已过期
  3. 完整版本:锁续命 + 唯一标识,保证安全性和可靠性
  4. 生产推荐:直接使用Redisson,它已经解决了所有边界情况

这个完整的代码实现展示了从线程不安全到各种分布式锁方案的演进过程,每个版本都解决了特定的问题。在实际生产环境中,推荐直接使用Redisson,因为它经过了充分的测试,处理了各种边界情况。

相关推荐
AAA修煤气灶刘哥3 小时前
Spring AI 通关秘籍:从聊天到业务落地,Java 选手再也不用馋 Python 了!
后端·spring·openai
自由的疯3 小时前
Java Jenkins+Docker部署jar包
java·后端·架构
37手游后端团队3 小时前
揭秘ChatGPT“打字机”效果:深入理解SSE流式传输技术
人工智能·后端
自由的疯3 小时前
Java Jenkins、Dockers和Kubernetes有什么区别
java·后端·架构
aiopencode3 小时前
tcpdump 抓包内容分析实战,快速定位到结论的工程化套路(含真机抓包)
后端
用户673398017513 小时前
Docker部署单机版NacosV3.0版本并使用Nginx代理
后端
京东零售技术3 小时前
浅析cef在win和mac上的适配
后端
Java水解3 小时前
MySQL 中 ROW_NUMBER() 函数详解
后端·mysql
美团技术团队3 小时前
从0到1建设美团数据库容量评估系统
后端