一文搞懂redis分布式锁

介绍

Redis分布式锁是一种在分布式系统中,利用Redis的原子操作特性实现的锁机制,用于保护共享资源的并发访问。

原理

原子性与互斥性

Redis分布式锁的核心原理在于利用Redis的某些原子操作(如SETNXGETSETSET带特定选项等)来确保锁的获取与释放操作是原子性的,从而保证了锁的互斥性,即同一时刻只有一个客户端能持有锁。

  • SETNX(Set if Not Exists):当给定的key不存在时,设置key的值为给定的value,并返回1(设置成功);如果key已经存在,则不做任何操作并返回0(设置失败)。利用SETNX,客户端可以尝试创建一个唯一标识的锁key,只有第一个成功创建的客户端才能获得锁。

自动过期与锁续期

为了避免死锁,通常会在锁key上设置一个过期时间(TTL),当持有锁的客户端崩溃或者未能及时释放锁时,锁会自动过期并释放,允许其他客户端获取锁。

  • EXPIREPEXPIRE:在获取锁后立即为锁key设置一个过期时间,确保即使客户端异常,锁也会在一定时间后自动释放。

实现方式

方法一:使用SETNX扩展过期时间命令

使用SET key value NX EX 10原子性设置锁和过期时间,避免死锁。以下代码使用org.springframework.boot:spring-boot-starter-data-redis包进行演示。

RedisClient 类提供获取和释放锁的方法

package com.example.bloom.lock;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

@Component
@Slf4j
public class RedisClient {

    @Autowired
    private StringRedisTemplate redisTemplate;

    private final String lockKey = "production_lock";

    private final String lockValue = "production_id";

    private final Integer tryTimes = 5;

    private final Integer expiredTime = 10;

    public boolean getLock() throws InterruptedException {
        for (int i=0; i<tryTimes; i++) {
            // 对应redis的set key value nx ex seconds命令
            Boolean lock = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, expiredTime, TimeUnit.SECONDS);
            if (lock != null && lock) {
                log.info("thread : {}, lock acquired", Thread.currentThread().getName());
                return true;
            }
            TimeUnit.MILLISECONDS.sleep(500);
            log.info("thread : {}, sleep 500 millseconds end, try continue get lock", Thread.currentThread().getName());
        }
        return false;
    }
    
    public boolean releaseLock() {
        String value = redisTemplate.opsForValue().get(lockKey);
        if (value != null && value.equals(lockValue)) {
            log.info("Thread : {}, Releasing lock", Thread.currentThread().getName());
            return redisTemplate.delete(lockKey);
        }
        return false;
    }
}

ProductionService 类模拟商品服务减少库存,同一时间只有一个线程可以减少库存。

package com.example.bloom.service;

import com.example.bloom.lock.RedisClient;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class ProductionService {

    @Autowired
    private RedisClient redisClient;

    private Integer counts = 10;

    public void setCounts(Integer counts) {
        this.counts = counts;
    }

    public Integer getCounts() {
        return counts;
    }

    public Integer releaseCounts() throws InterruptedException {
        boolean lock = redisClient.getLock();
        if (lock) {
            counts--;
            redisClient.releaseLock();
        }
        return counts;
    }
}

测试用例,开启5个线程同时执行减库存的操作。

package com.example.bloom;

import com.example.bloom.service.ProductionService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@SpringBootTest
class BloomApplicationTests {
    @Autowired
    private ProductionService productionService;

    @Test
    void testRedisLock() throws InterruptedException {
        productionService.setCounts(10);
        int taskCounts = 5;
        CountDownLatch latch = new CountDownLatch(taskCounts);
        ExecutorService executor = Executors.newFixedThreadPool(taskCounts);
        for (int i= 0; i < taskCounts; i++) {
            executor.submit(() -> {
                try {
                    productionService.releaseCounts();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } finally {
                    latch.countDown();
                }
            });

        }
        latch.await();
        assert productionService.getCounts() == 5;
    }
}

打印日志

2025-05-03 17:43:40.331  INFO 26764 --- [pool-1-thread-2] com.example.bloom.lock.RedisClient       : thread : pool-1-thread-2, lock acquired
2025-05-03 17:43:40.331  INFO 26764 --- [pool-1-thread-2] com.example.bloom.lock.RedisClient       : Thread : pool-1-thread-2, Releasing lock
2025-05-03 17:43:40.859  INFO 26764 --- [pool-1-thread-3] com.example.bloom.lock.RedisClient       : thread : pool-1-thread-3, sleep 500 millseconds end, try continue get lock
2025-05-03 17:43:40.859  INFO 26764 --- [pool-1-thread-4] com.example.bloom.lock.RedisClient       : thread : pool-1-thread-4, sleep 500 millseconds end, try continue get lock
2025-05-03 17:43:40.859  INFO 26764 --- [pool-1-thread-1] com.example.bloom.lock.RedisClient       : thread : pool-1-thread-1, sleep 500 millseconds end, try continue get lock
2025-05-03 17:43:40.859  INFO 26764 --- [pool-1-thread-5] com.example.bloom.lock.RedisClient       : thread : pool-1-thread-5, sleep 500 millseconds end, try continue get lock
2025-05-03 17:43:40.864  INFO 26764 --- [pool-1-thread-1] com.example.bloom.lock.RedisClient       : thread : pool-1-thread-1, lock acquired
2025-05-03 17:43:40.865  INFO 26764 --- [pool-1-thread-1] com.example.bloom.lock.RedisClient       : Thread : pool-1-thread-1, Releasing lock
2025-05-03 17:43:41.386  INFO 26764 --- [pool-1-thread-3] com.example.bloom.lock.RedisClient       : thread : pool-1-thread-3, sleep 500 millseconds end, try continue get lock
2025-05-03 17:43:41.386  INFO 26764 --- [pool-1-thread-5] com.example.bloom.lock.RedisClient       : thread : pool-1-thread-5, sleep 500 millseconds end, try continue get lock
2025-05-03 17:43:41.386  INFO 26764 --- [pool-1-thread-4] com.example.bloom.lock.RedisClient       : thread : pool-1-thread-4, sleep 500 millseconds end, try continue get lock
2025-05-03 17:43:41.393  INFO 26764 --- [pool-1-thread-5] com.example.bloom.lock.RedisClient       : thread : pool-1-thread-5, lock acquired
2025-05-03 17:43:41.394  INFO 26764 --- [pool-1-thread-5] com.example.bloom.lock.RedisClient       : Thread : pool-1-thread-5, Releasing lock
2025-05-03 17:43:41.899  INFO 26764 --- [pool-1-thread-3] com.example.bloom.lock.RedisClient       : thread : pool-1-thread-3, sleep 500 millseconds end, try continue get lock
2025-05-03 17:43:41.899  INFO 26764 --- [pool-1-thread-4] com.example.bloom.lock.RedisClient       : thread : pool-1-thread-4, sleep 500 millseconds end, try continue get lock
2025-05-03 17:43:41.909  INFO 26764 --- [pool-1-thread-4] com.example.bloom.lock.RedisClient       : thread : pool-1-thread-4, lock acquired
2025-05-03 17:43:41.909  INFO 26764 --- [pool-1-thread-4] com.example.bloom.lock.RedisClient       : Thread : pool-1-thread-4, Releasing lock
2025-05-03 17:43:42.414  INFO 26764 --- [pool-1-thread-3] com.example.bloom.lock.RedisClient       : thread : pool-1-thread-3, sleep 500 millseconds end, try continue get lock
2025-05-03 17:43:42.425  INFO 26764 --- [pool-1-thread-3] com.example.bloom.lock.RedisClient       : thread : pool-1-thread-3, lock acquired
2025-05-03 17:43:42.425  INFO 26764 --- [pool-1-thread-3] com.example.bloom.lock.RedisClient       : Thread : pool-1-thread-3, Releasing lock

可以看到结果符合预期,但是这里有一个小问题。可能出现下面这种情况,有个线程a刚好在释放获取的锁,而恰恰此时锁因为TTL到了刚好过期了,线程b立马获取了刚刚过期的锁,线程a不知道锁过期,在redis服务器执行了del key命令,恰恰把线程b刚获取的锁删掉了。这就导致线程a释放了线程b的锁,其他线程又可以在线程b还在执行逻辑时抢锁,从而引发线程安全问题。整个流程如下。

时间线程a线程bredis服务器
1调用relaseLock方法
2执行get操作获取锁键的值
3检查锁键的值确认是库存锁
4锁过期被删除
5执行getLock方法获取锁
6执行del命令删除锁(在不知情的情况下失去了锁)

那么有没有方法解决这个问题呢?下面介绍第二种方法来解决这个问题。

方法二:使用流水线:watch key + 事务

前置知识点

什么是流水线

在一般情况下,用户每执行一个Redis命令,Redis客户端和Redis服务器就需要执行以下步骤:

  1. 客户端向服务器发送命令请求。
  2. 服务器接收命令请求,并执行用户指定的命令调用,然后产生相应的命令执行结果。
  3. 服务器向客户端返回命令的执行结果。
  4. 客户端接收命令的执行结果,并向用户进行展示。

与大多数网络程序一样,执行Redis命令所消耗的大部分时间都用在了发送命令请求和接收命令结果上面:Redis服务器处理一个命令请求通常只需要很短的时间,但客户端将命令请求发送给服务器以及服务器向客户端返回命令结果的过程却需要花费不少时间。通常情况下,程序需要执行的Redis命令越多,它需要进行的网络通信操作也会越多,程序的执行速度也会因此而变慢。

为了解决这个问题,我们可以使用Redis提供的流水线特性:这个特性允许客户端把任意多条Redis命令请求打包在一起,然后一次性地将它们全部发送给服务器,而服务器则会在流水线包含的所有命令请求都处理完毕之后,一次性地将它们的执行结果全部返回给客户端。

通过使用流水线特性,我们可以将执行多个命令所需的网络通信次数从原来的N次降低为1次,这可以大幅度地减少程序在网络通信方面耗费的时间,使得程序的执行效率得到显著的提升。

下图是在不使用流水线的情况下执行3个Redis命令产生的网络通信操作。
在不使用流水线的情况下执行3个Redis命令产生的网络通信操作
下图是在使用流水线的情况下执行3个Redis命令产生的网络通信操作。
在使用流水线的情况下执行3个Redis命令产生的网络通信操作

什么是事务

流水线只能保证多条命令会一起被发送至服务器,但它并不保证这些命令都会被服务器执行。

下面介绍下事务特性:

  • 事务可以将多个命令打包成一个命令来执行,当事务成功执行时,事务中包含的所有命令都会被执行。
  • 相反,如果事务没有成功执行,那么它包含的所有命令都不会被执行。

通过使用事务,用户可以保证自己想要执行的多个命令要么全部被执行,要么一个都不执行。

WATCH:对键进行监视

客户端可以通过执行WATCH命令,要求服务器对一个或多个数据库键进行监视,如果在客户端尝试执行事务之前,这些键的值发生了变化,那么服务器将拒绝执行客户端发送的事务,并向它返回一个空值。

与此相反,如果所有被监视的键都没有发生任何变化,那么服务器将会如常地执行客户端发送的事务。

通过同时使用WATCH命令和Redis事务,我们可以构建出一种针对被监视键的乐观锁机制,确保事务只会在被监视键没有发生任何变化的情况下执行,从而保证事务对被监视键的所有修改都是安全、正确和有效的。

重点实现

释放锁时,我们使用流水线一次发送一批redis命令:一开始我们先watch key监听锁key,然后执行redis事务del key删除锁。若锁key的值在删除时发生变化(例如锁key因为ttl过期后由其他线程SET key value NX EX 10重新设置值),则del key不成功,事务执行失败;否则,事务执行成功,锁被释放。

RedisClient类释放锁时使用新的方式。

public class RedisClient {
	public void releaseWatchLock() {
        redisTemplate.execute(new SessionCallback() {
            // 使用流水线一次发送一系列redis命令
            public List<Object> execute(RedisOperations operations) {
            	// 监听锁key
                operations.watch(lockKey);
                String value = String.valueOf(operations.opsForValue().get(lockKey));
                if (value.equals(lockValue)) {
                    // 使用redis事务
                    operations.multi();
                    operations.delete(lockKey);
                    // 执行redis事务。返回null说明其他线程已获取锁,锁被修改(redis key过期删除后重新设置旧值也是修改),事务执行失败。
                    return operations.exec();
                }
                // 最后取消监听
                operations.unwatch();
                return null;
            }
        });
    }
}

ProductionService类减少库存时调用新的releaseWatchLock方法。

public class ProductionService {

    public Integer releaseWatchCounts() throws InterruptedException {
        boolean lock = redisClient.getLock();
        if (lock) {
            counts--;
            redisClient.releaseWatchLock();
        }
        return counts;
    }
}

测试用例,开启5个线程同时执行减库存的操作。

@SpringBootTest
class BloomApplicationTests {
    @Autowired
    private ProductionService productionService;

    @Test
    void testRedisWatchLock() throws InterruptedException {
        productionService.setCounts(10);
        int taskCounts = 5;
        CountDownLatch latch = new CountDownLatch(taskCounts);
        ExecutorService executor = Executors.newFixedThreadPool(taskCounts);
        for (int i= 0; i < taskCounts; i++) {
            executor.submit(() -> {
                try {
                    productionService.releaseWatchCounts();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } finally {
                    latch.countDown();
                }
            });

        }
        latch.await();
        assert productionService.getCounts() == 5;
    }
}

使用这种方式后,再捋一下线程a释放锁时,锁刚好过期,此时线程b获得锁,线程a想要释放时锁时,发现监听的锁key被修改过了,导致删除失败。线程a不需要再释放锁,线程b继续正常执行。

比起第一种方式,这种方式虽然避免了出错。但是同样让redis服务器耗费更多的时间和资源用来监听key和处理事务,那么有没有更好的解决方案呢?下面请看方法三。

方法三:使用lua脚本

前置知识

lua脚本特点

Redis服务器以原子方式执行Lua脚本,在执行完整个Lua脚本及其包含的Redis命令之前,Redis服务器不会执行其他客户端发送的命令或脚本,因此被执行的Lua脚本天生就具有原子性。

释放分布式锁的lua脚本
-- KEYS[1]:锁键名,ARGV[1]:请求ID
if redis.call("GET", KEYS[1]) == ARGV[1] then
    return redis.call("DEL", KEYS[1])
else
    return 0
end

重点实现

RedisClient类使用lua脚本释放锁。

public class RedisClient {
	public boolean releaseLockUseLuaScript() {
        String scriptText = "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end";
        DefaultRedisScript<Long> script = new DefaultRedisScript<>(scriptText, Long.class);
        List<String> keys = Collections.singletonList(lockKey);
        Long result = redisTemplate.execute(script, keys, lockValue);
        return result != null && result == 1;
    }
}

ProductionService类减少库存时调用新的releaseLuaCounts方法。

public class ProductionService {

    public Integer releaseLuaCounts() throws InterruptedException {
        boolean lock = redisClient.getLock();
        if (lock) {
            counts--;
            redisClient.releaseLockUseLuaScript();
        }
        return counts;
    }
}

测试用例,开启5个线程同时执行减库存的操作。

@SpringBootTest
class BloomApplicationTests {
    @Autowired
    private ProductionService productionService;

    @Test
    void testRedisLuaLock() throws InterruptedException {
        productionService.setCounts(10);
        int taskCounts = 5;
        CountDownLatch latch = new CountDownLatch(taskCounts);
        ExecutorService executor = Executors.newFixedThreadPool(taskCounts);
        for (int i= 0; i < taskCounts; i++) {
            executor.submit(() -> {
                try {
                    productionService.releaseLuaCounts();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } finally {
                    latch.countDown();
                }
            });

        }
        latch.await();
        assert productionService.getCounts() == 5;
    }
}

另一种实现:使用redission

其实org.redisson:redisson-spring-boot-starter包有自带的分布式锁实现,它也是使用的lua脚本,我们只需要简单地调用几个redisson方法。所以有成熟的方法我们就去借鉴,不要自己造轮子。

ProductionService类直接使用redission实现分布式锁。

@Service
@Slf4j
public class ProductionService {

	@Autowired
    private RedissonClient redissonClient;

    public Integer releaseCountsUseRedission() {
        RLock lock = redissonClient.getLock("production_lock");
        try {
            // 非阻塞式加锁(等待3秒,锁有效期10秒)
            boolean locked = lock.tryLock(3, 10, TimeUnit.SECONDS);
            if (locked) {
                log.info("thread : {}, lock acquired", Thread.currentThread().getName());
                // 执行业务逻辑
                counts--;
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            // 检查持有锁的是不是当前线程(检查redis key的值是不是当前线程)
            if (lock.isHeldByCurrentThread()) {
                // 释放锁
                lock.unlock();
                log.info("Thread : {}, has Released lock", Thread.currentThread().getName());
            }
        }
        return counts;
    }
}

测试用例,开启10个线程同时执行减库存的操作。

@SpringBootTest
class BloomApplicationTests {
    @Autowired
    private ProductionService productionService;

    @Test
    void testRedissionLock() throws InterruptedException {
        productionService.setCounts(10);
        int taskCounts = 10;
        CountDownLatch latch = new CountDownLatch(taskCounts);
        ExecutorService executor = Executors.newFixedThreadPool(taskCounts);
        for (int i= 0; i < taskCounts; i++) {
            executor.submit(() -> {
                try {
                    productionService.releaseCountsUseRedission();
                } finally {
                    latch.countDown();
                }
            });

        }
        latch.await();
        assert productionService.getCounts() == 0;
    }
}

执行日志

2025-05-03 20:55:54.896  INFO 21536 --- [pool-1-thread-3] c.e.bloom.service.ProductionService      : thread : pool-1-thread-3, lock acquired
2025-05-03 20:55:54.902  INFO 21536 --- [pool-1-thread-3] c.e.bloom.service.ProductionService      : Thread : pool-1-thread-3, has Released lock
2025-05-03 20:55:54.975  INFO 21536 --- [pool-1-thread-7] c.e.bloom.service.ProductionService      : thread : pool-1-thread-7, lock acquired
2025-05-03 20:55:54.995  INFO 21536 --- [pool-1-thread-7] c.e.bloom.service.ProductionService      : Thread : pool-1-thread-7, has Released lock
2025-05-03 20:55:55.000  INFO 21536 --- [pool-1-thread-6] c.e.bloom.service.ProductionService      : thread : pool-1-thread-6, lock acquired
2025-05-03 20:55:55.018  INFO 21536 --- [pool-1-thread-6] c.e.bloom.service.ProductionService      : Thread : pool-1-thread-6, has Released lock
2025-05-03 20:55:55.019  INFO 21536 --- [pool-1-thread-2] c.e.bloom.service.ProductionService      : thread : pool-1-thread-2, lock acquired
2025-05-03 20:55:55.030  INFO 21536 --- [pool-1-thread-2] c.e.bloom.service.ProductionService      : Thread : pool-1-thread-2, has Released lock
2025-05-03 20:55:55.034  INFO 21536 --- [pool-1-thread-4] c.e.bloom.service.ProductionService      : thread : pool-1-thread-4, lock acquired
2025-05-03 20:55:55.049  INFO 21536 --- [pool-1-thread-4] c.e.bloom.service.ProductionService      : Thread : pool-1-thread-4, has Released lock
2025-05-03 20:55:55.051  INFO 21536 --- [pool-1-thread-1] c.e.bloom.service.ProductionService      : thread : pool-1-thread-1, lock acquired
2025-05-03 20:55:55.056  INFO 21536 --- [pool-1-thread-1] c.e.bloom.service.ProductionService      : Thread : pool-1-thread-1, has Released lock
2025-05-03 20:55:55.062  INFO 21536 --- [pool-1-thread-5] c.e.bloom.service.ProductionService      : thread : pool-1-thread-5, lock acquired
2025-05-03 20:55:55.063  INFO 21536 --- [pool-1-thread-5] c.e.bloom.service.ProductionService      : Thread : pool-1-thread-5, has Released lock
2025-05-03 20:55:55.075  INFO 21536 --- [pool-1-thread-8] c.e.bloom.service.ProductionService      : thread : pool-1-thread-8, lock acquired
2025-05-03 20:55:55.089  INFO 21536 --- [pool-1-thread-8] c.e.bloom.service.ProductionService      : Thread : pool-1-thread-8, has Released lock
2025-05-03 20:55:55.107  INFO 21536 --- [pool-1-thread-9] c.e.bloom.service.ProductionService      : thread : pool-1-thread-9, lock acquired
2025-05-03 20:55:55.130  INFO 21536 --- [pool-1-thread-9] c.e.bloom.service.ProductionService      : Thread : pool-1-thread-9, has Released lock
2025-05-03 20:55:55.134  INFO 21536 --- [ool-1-thread-10] c.e.bloom.service.ProductionService      : thread : pool-1-thread-10, lock acquired
2025-05-03 20:55:55.147  INFO 21536 --- [ool-1-thread-10] c.e.bloom.service.ProductionService      : Thread : pool-1-thread-10, has Released lock

  • 个人小游戏
    个人小游戏
    在这里插入图片描述
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

会飞的大鱼人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值