Redis锁有哪些挑战?怎么解决常见陷阱?

文章导读
Redis 分布式锁在实际应用中面临诸多挑战,主要包括非原子操作导致的死锁风险、锁未及时释放造成的资源阻塞、业务执行时间超过锁过期时间引发的并发安全问题,以及主从切换导致的锁丢失等。解决这些常见陷阱的关键在于使用原子命令如 SET NX PX 替代分离的 SETNX 和 EXPIRE,确保加锁与过期设置一次性完成;引入看门狗机制自动续期以防业务超时;使用 Redisson 等成熟客户端处理重入性和
📋 目录
  1. redis 分布式锁的 5 个坑,真是又大又深
  2. 聊聊 redis 分布式锁的 8 大坑
  3. Redis 分布式锁及其常见问题解决方案
  4. Redis 常见坑及解决方案
  5. FAQ
A A

Redis 分布式锁在实际应用中面临诸多挑战,主要包括非原子操作导致的死锁风险、锁未及时释放造成的资源阻塞、业务执行时间超过锁过期时间引发的并发安全问题,以及主从切换导致的锁丢失等。解决这些常见陷阱的关键在于使用原子命令如 SET NX PX 替代分离的 SETNX 和 EXPIRE,确保加锁与过期设置一次性完成;引入看门狗机制自动续期以防业务超时;使用 Redisson 等成熟客户端处理重入性和原子释放;同时在解锁时校验锁标识防止误删他人锁,并在高可用场景下考虑 RedLock 算法或结合数据库乐观锁兜底,从而保障分布式系统的数据一致性与稳定性。

redis 分布式锁的 5 个坑,真是又大又深

由于是做商城业务,要频繁的对商品库存进行扣减,应用是集群部署,为避免并发造成库存超买超卖等问题,采用 redis 分布式锁加以控制。本以为给扣库存的代码加上锁 lock.tryLock 就万事大吉了 代码语言:txtI 代码解释 /** * @author xiaofu * @description 扣减库存 * @date 2020/4/21 12:10 */ public String stockLock() { RLock lock = redissonClient.getLock("stockLock"); try { /** * 获取锁 */ if (lock.tryLock(10, TimeUnit.SECONDS)) { /** * 查询库存数 */ Integer stock = Integer.valueOf(stringRedisTemplate.opsForValue().get("stockCount")); /** * 扣减库存 */ if (stock > 0) { stock = stock - 1; stringRedisTemplate.opsForValue().set("stockCount", stock.toString()); LOGGER.info("库存扣减成功,剩余库存数量:{}", stock); } else { LOGGER.info("库存不足~"); } } else { LOGGER.info("未获取到锁业务结束.."); } } catch (Exception e) { LOGGER.info("处理异常", e); } finally { lock.unlock(); } return "ok"; } 结果业务代码执行完以后我忘了释放锁 lock.unlock(),导致 redis 线程池被打满,redis 服务大面积故障,造成库存数据扣减混乱,被领导一顿臭骂,这个月绩效~哎·~。随着 使用 redis 锁的时间越长,我发现 redis 锁的坑远比想象中要多。就算在面试题当中 redis 分布式锁的出镜率也比较高,比如:“用锁遇到过哪些问题?”,“又是如何解决的?”基本都是一套连招问出来的。今天就分享一下我用 redis 分布式锁的踩坑日记,以及一些解决方案,和大家一起共勉。一、锁未被释放 这种情况是一种低级错误,就是我上边犯的错,由于当前线程 获取到 redis 锁,处理完业务后未及时释放锁,导致其它线程会一直尝试获取锁阻塞,例如:用 Jedis 客户端会报如下的错误信息 代码语言:txt AI 代码解释 redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool redis 线程池已经没有空闲线程来处理客户端命令。解决的方法也很简单,只要我们细心一点,拿到锁的线程处理完业务及时释放锁,如果是重入锁未拿到锁后,线程可以释放当前连接并且 sleep 一段时间。

聊聊 redis 分布式锁的 8 大坑

1 非原子操作 使用 redis 的分布式锁,我们首先想到的可能是 setnx 命令。代码语言:javascript ai 代码解释复制 if ( jedis . setnx ( lockkey , val ) == 1 ) { jedis . expire ( lockkey , timeout ) ; } 容易,三下五除二,我们就可以把代码写好。这段代码确实可以加锁成功,但你有没有发现什么问题?加锁操作和后面的设置超时时间是分开的,并非原子操作。假如加锁成功,但是设置超时时间失败了,该 lockkey 就变成永不失效。假如在高并发场景中,有大量的 lockkey 加锁成功了,但不会失效,有可能直接导致 redis 内存空间不足。那么,有没有保证原子性的加锁命令呢?答案是:有,请看下面。2 忘了释放锁 上面说到使用 setnx 命令加锁操作和设置超时时间是分开的,并非原子操作。而在 redis 中还有 set 命令,该命令可以指定多个参数。代码语言:javascript ai 代码解释复制 string result = jedis . set ( lockkey , requestid , "nx" , "px" , expiretime ) ; if ( "ok" . equals ( result ) ) { return true ; } return false ; 其中:lockkey :锁的标识 requestid :请求 id nx :只在键不存在时,才对键进行设置操作。PX:设置键的过期时间为 millisecond 毫秒。expireTime:过期时间 set 命令是原子操作,加锁和设置超时时间,一个命令就能轻松搞定。nice 使用 set 命令加锁,表面上看起来没有问题。但如果仔细想想,加锁之后,每次都要达到了超时时间才释放锁,会不会有点不合理?加锁后,如果不及时释放锁,会有很多问题。

Redis 分布式锁及其常见问题解决方案

Redis 分布式锁及其常见问题解决方案 Redis 是一种内存中的数据结构存储系统,它可以用作数据库、缓存和消息代理。由于其高性能和灵活的数据结构,Redis 被广泛应用在各种场景中,包括实现分布式锁。分布式锁是一种在分布式系统中实现互斥访问的技术。在许多实际应用场景中,我们需要确保某些操作在同一时间只能被一个节点执行,例如更新共享资源、处理任务队列等。这时,我们就需要使用到分布式锁。Redis 提供了一种简单有效的分布式锁实现方式。其基本思想是使用 Redis 的 SETNX 命令,这个命令可以在键不存在时设置值,如果键已存在则不做任何操作。通过这个原子操作,我们可以实现在多个节点之间的互斥访问。然而,虽然 Redis 分布式锁的实现相对简单,但在实际使用中还需要考虑很多问题,例如锁的超时和续期问题、锁的公平性问题、网络分区的问题等。在接下来的文章中,我们将详细介绍这些问题以及解决方案。1、Redis 分布式锁简介 1.1、关于分布式锁 在一个分布式系统中,当一个线程去读取数据并修改的时候,因为读取和更新保存不是一个原子操作,在并发时就很容易遇到并发问题,进而导致数据的不正确。这种场景很常见,比如电商秒杀活动,库存数量的更新就会遇到。如果是单机应用,直接使用本地锁就可以避免。如果是分布式应用,本地锁派不上用场,这时就需要引入分布式锁来解决。一般来说,实现分布式锁的方式有以下几种:使用 MySQL:这种方式是通过在数据库中创建一个唯一索引的表,然后通过插入一条数据来获取锁,如果插入成功则获取锁成功,否则获取锁失败。释放锁的操作就是删除这条数据。这种方式的优点是实现简单,缺点是性能较低,因为涉及到数据库的操作。使用 ZooKeeper:ZooKeeper 提供了一个原生的分布式锁实现。其基本思想是创建一个临时有序节点,然后判断自己是否是所有子节点中序号最小的,如果是则获取锁成功,否则监听比自己序号小的节点,当该节点删除时再次尝试获取锁。这种方式的优点是能够保证公平性,缺点是实现较为复杂。使用 Redis:这种方式是通过 Redis 的 SETNX 命令来实现的,这个命令可以在键不存在时设置值,如果键已存在则不做任何操作。通过这个原子操作,我们可以实现在多个节点之间的互斥访问。这种方式的优点是性能高,实现简单,缺点是需要处理锁的超时和续期问题。

Redis锁有哪些挑战?怎么解决常见陷阱?

Redis 常见坑及解决方案

非原子操作 (setNx+expire) 代码语言:javascript AI 代码解释 //加锁 if(jedis.setnx(lock_key,lock_value)==1){//设置过期时间 jedis.expire(lock_key,timeout);//业务逻辑处理 doBusiness} 以上使用 Redis 的 setNx() 命令和 expire 命令实现了加锁,但是本方案是分成了两步完成的加锁操作,并不是原子操作,可能会出现未给该 key 设置过期时间的问题,因此该问题的解决方案推荐使用 Redisson 的分布式锁。解决方案-Redission Redisson 提供的分布式锁功能是原子操作的。Redisson 内部使用了 Redis 的 Lua 脚本来执行获取锁和释放锁的操作,确保这些操作的原子性。自动续期:Redisson 实现了分布式锁的自动续期功能。在获取锁时,Redisson 会设置一个过期时间,并周期性地对锁进行续期。这样可以确保在锁的持有者在任务执行期间出现阻塞或延迟时,锁不会过期而被其他进程获取,从而保证任务的完整性和准确性。可重入性支持:Redisson 支持分布式锁的可重入性。即同一个线程可以多次获取同一个锁,而不会产生死锁。Redisson 使用锁的持有者标识和计数来判断锁的重入次数,从而确保同一个线程能够正确地释放锁。高可用支持:Redission 可以与 Redis 的主从复制或集群模式配合使用,当主节点发生故障时,Redission 可以自动切换到可用的从节点或其他节点上,确保分布式锁的可用性和稳定性。代码语言:javascript AI 代码解释 org.redissonredisson-spring-boot-starter3.16.1 代码实现:代码语言:javascript AI 代码解释 @ServicepublicclassPurchaseService{privatefinal RedissonClient redissonClient;publicPurchaseService(RedissonClient redissonClient){this.redissonClient=redissonClient;}privateint a=9;@Transactional(rollbackFor=Exception.class)publicbooleanpurchase(String productId,int quantity){String lockKey="lock:"+productId;RLock lock=redissonClient.getLock(lockKey);try{lock.lock();// 加锁// 检查库存是否充足 int stock=getStockFromDatabase(productId);if(stock>=quantity){// 执行下单操作// // 扣减库存 decreaseStockInDatabase(productId,quantity);if(true){thrownewRuntimeException("1");}returntrue;// 抢购成功}else{returnfalse;// 库存不足,抢购失败}}catch(Exception e){thrownewRuntimeException(e

FAQ

Redis 分布式锁为什么建议使用 SET 命令代替 SETNX+EXPIRE?

Redis锁有哪些挑战?怎么解决常见陷阱?

因为 SETNX 和 EXPIRE 是分开的两步操作,非原子性。如果加锁成功但设置过期时间失败,会导致锁永不失效,引发死锁。SET 命令支持 NX 和 PX 参数,可以原子性地完成加锁和设置过期时间。

业务执行时间超过锁过期时间怎么办?

可以使用看门狗(WatchDog)机制,如 Redisson 客户端提供的自动续期功能。在持有锁期间定期刷新锁的过期时间,确保业务完成前锁不会失效,避免并发安全问题。

Redis锁有哪些挑战?怎么解决常见陷阱?

如何防止误删其他线程的锁?

在加锁时设置一个唯一的标识(如 UUID 或请求 ID)作为 value。解锁时先判断当前锁的 value 是否与自己的标识一致,一致才删除,防止因锁过期后被其他线程获取,而原线程又去删除锁的情况。