Redis 分布式锁一步步优化过程

Redis 官方文档

点击查看 Redis 中文官方文档

列表模式 http://redis.cn/topics/

举个案例

高并发秒杀场景中,看一下一段代码,会发生哪些问题?

@Slf4j
@RestController
public class GoodController {
    private static final String KEY = "goods:001";

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @RequestMapping("/goods")
    public String buyGoods() {

        String result = stringRedisTemplate.opsForValue().get(KEY);

        int num = Objects.isNull(result) ? 0 : Integer.parseInt(result);

        if (num > 0) {
            int realNum = num - 1;
            stringRedisTemplate.opsForValue().set(KEY,Integer.toString(realNum));
            log.info(">>>>>>>>>>你已经成功秒杀了商品,还剩余{}件",realNum);
            return "你已经成功秒杀了商品,还剩余"+realNum+"件";
        } 
        
        log.info(">>>>>>>>>>活动已经售罄,欢迎下次光临.");
        return "活动已经售罄,欢迎下次光临";
    }
}

肯定会发生超卖(重复卖同一件商品)问题,所以这里可以加锁保证数据安全性,可以使用 Synchornized、ReentantLock 锁,但是推荐使用 ReentantLock 锁,它有一个 tryLock() 方法可以尝试加锁,不会死等。从而可以做一些自己的业务操作,改进之后如下:

	@RequestMapping("/goods2")
    public String buyGoods() {

        if (lock.tryLock()) {
            try {
                String result = stringRedisTemplate.opsForValue().get(KEY);
                int num = Objects.isNull(result) ? 0 : Integer.parseInt(result);
                if (num > 0) {
                    int realNum = num - 1;
                    stringRedisTemplate.opsForValue().set(KEY, Integer.toString(realNum));
                    log.info(">>>>>>>>>>你已经成功秒杀了商品,还剩余{}件", realNum);
                    return "你已经成功秒杀了商品,还剩余" + realNum + "件";
                }
                log.info(">>>>>>>>>>活动已经售罄,欢迎下次光临.");
                return "活动已经售罄,欢迎下次光临";
            } finally {
                lock.unlock();
            }
        } else {
            // do_something....
        }
        return "程序结束";
    }

测试效果如下:

2022-09-14 11:10:26.028  INFO 92342 --- [o-9292-exec-120] com.gwm.cloud.redislock.GoodController2  : >>>>>>>>>>你已经成功秒杀了商品,还剩余92022-09-14 11:10:26.125  INFO 92342 --- [o-9292-exec-185] com.gwm.cloud.redislock.GoodController2  : >>>>>>>>>>你已经成功秒杀了商品,还剩余82022-09-14 11:10:26.148  INFO 92342 --- [o-9292-exec-109] com.gwm.cloud.redislock.GoodController2  : >>>>>>>>>>你已经成功秒杀了商品,还剩余72022-09-14 11:10:26.157  INFO 92342 --- [o-9292-exec-179] com.gwm.cloud.redislock.GoodController2  : >>>>>>>>>>你已经成功秒杀了商品,还剩余62022-09-14 11:10:26.183  INFO 92342 --- [o-9292-exec-120] com.gwm.cloud.redislock.GoodController2  : >>>>>>>>>>你已经成功秒杀了商品,还剩余52022-09-14 11:10:26.189  INFO 92342 --- [io-9292-exec-93] com.gwm.cloud.redislock.GoodController2  : >>>>>>>>>>你已经成功秒杀了商品,还剩余42022-09-14 11:10:26.192  INFO 92342 --- [io-9292-exec-74] com.gwm.cloud.redislock.GoodController2  : >>>>>>>>>>你已经成功秒杀了商品,还剩余32022-09-14 11:10:26.198  INFO 92342 --- [io-9292-exec-67] com.gwm.cloud.redislock.GoodController2  : >>>>>>>>>>你已经成功秒杀了商品,还剩余22022-09-14 11:10:26.203  INFO 92342 --- [o-9292-exec-192] com.gwm.cloud.redislock.GoodController2  : >>>>>>>>>>你已经成功秒杀了商品,还剩余12022-09-14 11:10:26.207  INFO 92342 --- [io-9292-exec-43] com.gwm.cloud.redislock.GoodController2  : >>>>>>>>>>你已经成功秒杀了商品,还剩余02022-09-14 11:10:26.210  INFO 92342 --- [o-9292-exec-110] com.gwm.cloud.redislock.GoodController2  : >>>>>>>>>>活动已经售罄,欢迎下次光临.
2022-09-14 11:10:26.212  INFO 92342 --- [io-9292-exec-89] com.gwm.cloud.redislock.GoodController2  : >>>>>>>>>>活动已经售罄,欢迎下次光临.

以上虽然能够解决单机在 Redis 上的安全行,但是如果是分布式微服务就不能保证了,所以最终还是要使用 Redis 来做分布式锁。

那么这里就是用 Nginx 做负载均衡转发。

安装和配置 nginx 请看:nginx 安装 nginx配置

重新修改后代码如下:

    @RequestMapping("/goods4")
    public String buyGoods() {

        // 加上分布式锁
        String randStr = UUID.randomUUID().toString();
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_KEY, randStr);
        if (!flag) {
            return "抢锁失败,请进行重试...";
        }

        String result = stringRedisTemplate.opsForValue().get(KEY);
        int num = Objects.isNull(result) ? 0 : Integer.parseInt(result);
        if (num > 0) {
            int realNum = num - 1;
            stringRedisTemplate.opsForValue().set(KEY, Integer.toString(realNum));
            log.info(">>>>>>>>>>你已经成功秒杀了商品,还剩余{}件,serverPort={}", realNum,serverPort);

            // 正常执行业务之后直接释放锁
            stringRedisTemplate.delete(REDIS_KEY);
            return "你已经成功秒杀了商品,还剩余" + realNum + "件"+serverPort;
        }
        log.info(">>>>>>>>>>活动已经售罄,欢迎下次光临."+serverPort);
        return "活动已经售罄,欢迎下次光临"+serverPort;
    }

改成用分布式锁来保证数据安全性,但是这里还有一个问题,就是如果业务执行不正常或者不没有释放锁,就会导致所有请求执行不了,所以必须要把释放锁这个操作放到 finally 操作快里面执行,无论如何都要释放掉锁。

那么重新改进后的代码如下:

    @RequestMapping("/goods5")
    public String buyGoods() {

        try {
            // 加上分布式锁
            String randStr = UUID.randomUUID().toString();
            Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_KEY, randStr);
            if (!flag) {
                return "抢锁失败,请进行重试...";
            }

            String result = stringRedisTemplate.opsForValue().get(KEY);
            int num = Objects.isNull(result) ? 0 : Integer.parseInt(result);
            if (num > 0) {
                int realNum = num - 1;
                stringRedisTemplate.opsForValue().set(KEY, Integer.toString(realNum));
                log.info(">>>>>>>>>>你已经成功秒杀了商品,还剩余{}件,serverPort={}", realNum,serverPort);
                return "你已经成功秒杀了商品,还剩余" + realNum + "件"+serverPort;
            }
            log.info(">>>>>>>>>>活动已经售罄,欢迎下次光临."+serverPort);
        } finally {
            // 正常执行业务之后直接释放锁
            stringRedisTemplate.delete(REDIS_KEY);
        }

        return "活动已经售罄,欢迎下次光临"+serverPort;
    }

但是发现这都是正常操作所以可以保证正常释放掉锁,此时如果程序刚好执行到 try 代码块的时候,还没来得及执行 finally 语句块,然后就停电了关机了,那么此时也就释放不了这把锁。

所以这里还需要对锁加入失效时间,保证宕机之后能够正常释放掉锁。修改过后的代码如:

    @RequestMapping("/goods5")
    public String buyGoods() {

        try {
            // 加上分布式锁
            String randStr = UUID.randomUUID().toString();
            Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_KEY, randStr);

            // 加上锁过期时间
            stringRedisTemplate.expire(REDIS_KEY,30L, TimeUnit.SECONDS);
            if (!flag) {
                return "抢锁失败,请进行重试...";
            }

            String result = stringRedisTemplate.opsForValue().get(KEY);
            int num = Objects.isNull(result) ? 0 : Integer.parseInt(result);
            if (num > 0) {
                int realNum = num - 1;
                stringRedisTemplate.opsForValue().set(KEY, Integer.toString(realNum));
                log.info(">>>>>>>>>>你已经成功秒杀了商品,还剩余{}件,serverPort={}", realNum,serverPort);
                return "你已经成功秒杀了商品,还剩余" + realNum + "件"+serverPort;
            }
            log.info(">>>>>>>>>>活动已经售罄,欢迎下次光临."+serverPort);
        } finally {
            // 正常执行业务之后直接释放锁
            stringRedisTemplate.delete(REDIS_KEY);
        }

        return "活动已经售罄,欢迎下次光临"+serverPort;
    }

加上了过期时间,但是还是有个问题,那就是加锁和设置锁失效时间不是一个原子操作,如果加锁成功,然后又停电宕机了,没有给锁设置过期时间,那么锁又会释放不成功。

将加锁和设置锁失效时间两个步骤换成一个操作,如下所示:


    @RequestMapping("/goods6")
    public String buyGoods() {

        try {
            String randStr = UUID.randomUUID().toString();
            // 加上分布式锁、设置锁过期时间
            Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_KEY, randStr,30L, TimeUnit.SECONDS);

            if (!flag) {
                return "抢锁失败,请进行重试...";
            }

            String result = stringRedisTemplate.opsForValue().get(KEY);
            int num = Objects.isNull(result) ? 0 : Integer.parseInt(result);
            if (num > 0) {
                int realNum = num - 1;
                stringRedisTemplate.opsForValue().set(KEY, Integer.toString(realNum));
                log.info(">>>>>>>>>>你已经成功秒杀了商品,还剩余{}件,serverPort={}", realNum,serverPort);
                return "你已经成功秒杀了商品,还剩余" + realNum + "件"+serverPort;
            }
            log.info(">>>>>>>>>>活动已经售罄,欢迎下次光临."+serverPort);
        } finally {
            // 正常执行业务之后直接释放锁
            stringRedisTemplate.delete(REDIS_KEY);
        }

        return "活动已经售罄,欢迎下次光临"+serverPort;
    }

现在已经保证了加锁的正确性了,但是还存在一个巨大的问题,就是误删问题,因为你给锁设置的超时时间是 30s,假设业务处理时间需要消耗 40s,那么此时第一个还在处理业务,超过了 30s,锁直接失效释放了,此时第二个线程就会加锁成功,等到第一个线程执行完之后,执行 finally 块释放锁,就把第二个线程的锁给释放了,这就是误删问题,非常严重。那么怎么解决呢?

是不是可以在释放前先获取到锁,然后判断这把锁是不是自己的呢?是自己的就可以释放,不是自己的就不能释放,修改之后如下:

    @RequestMapping("/goods6")
    public String buyGoods() {

        String randStr = UUID.randomUUID().toString();
        try {
            // 加上分布式锁、设置锁过期时间
            Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_KEY, randStr,30L, TimeUnit.SECONDS);

            if (!flag) {
                return "抢锁失败,请进行重试...";
            }

            String result = stringRedisTemplate.opsForValue().get(KEY);
            int num = Objects.isNull(result) ? 0 : Integer.parseInt(result);
            if (num > 0) {
                int realNum = num - 1;
                stringRedisTemplate.opsForValue().set(KEY, Integer.toString(realNum));
                log.info(">>>>>>>>>>你已经成功秒杀了商品,还剩余{}件,serverPort={}", realNum,serverPort);
                return "你已经成功秒杀了商品,还剩余" + realNum + "件"+serverPort;
            }
            log.info(">>>>>>>>>>活动已经售罄,欢迎下次光临."+serverPort);
        } finally {
            // 加上锁判断、是否允许释放条件
            if (stringRedisTemplate.opsForValue().get(REDIS_KEY).equals(randStr)) {

                // 正常执行业务之后直接释放锁
                stringRedisTemplate.delete(REDIS_KEY);
            }
        }

        return "活动已经售罄,欢迎下次光临"+serverPort;
    }

但是此时,判断锁和释放锁的步骤不是一个原子操作,还是可能存在时间先后的差异,也会导致误删锁的问题,所以需要保证判断和释放锁必须是一个原子操作,如下所示:

    @RequestMapping("/goods7")
    public String buyGoods() {

        String randStr = UUID.randomUUID().toString();
        try {
            // 加上分布式锁、设置锁过期时间
            Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_KEY, randStr,30L, TimeUnit.SECONDS);

            if (!flag) {
                return "抢锁失败,请进行重试...";
            }

            String result = stringRedisTemplate.opsForValue().get(KEY);
            int num = Objects.isNull(result) ? 0 : Integer.parseInt(result);
            if (num > 0) {
                int realNum = num - 1;
                stringRedisTemplate.opsForValue().set(KEY, Integer.toString(realNum));
                log.info(">>>>>>>>>>你已经成功秒杀了商品,还剩余{}件,serverPort={}", realNum,serverPort);
                return "你已经成功秒杀了商品,还剩余" + realNum + "件"+serverPort;
            }
            log.info(">>>>>>>>>>活动已经售罄,欢迎下次光临."+serverPort);
        } finally {
            // 通过 Lua 脚本删除分布式锁的 key

            Jedis jedis = RedisUtils.getJedis();

            String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
                    "then\n" +
                    "    return redis.call(\"del\",KEYS[1])\n" +
                    "else\n" +
                    "    return 0\n" +
                    "end";
            try {
                Object result = jedis.eval(script, Collections.singletonList(REDIS_KEY), Collections.singletonList(randStr));
                // 这里记得 toString() 否则不会相等
                if ("1".equals(result.toString())) {
                    log.info(">>>>>>>>>del lock_key success!!!");
                } else {
                    log.info(">>>>>>>>del lock_key error... ");
                }
            } finally {
                if (Objects.nonNull(jedis)) {
                    // 关闭资源
                    jedis.close();
                }
            }
        }

        return "活动已经售罄,欢迎下次光临"+serverPort;
    }

RedisUtils 工具类如下:


public class RedisUtils {

    private static JedisPool jedisPool = null;

    /**
     * 创建 Jedis 连接池
     */
    static {
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxTotal(20);
        jedisPoolConfig.setMaxIdle(10);
        jedisPool = new JedisPool(jedisPoolConfig, "localhost", 6379);
    }

    /**
     * 从 JedisPool 中获取到 Jedis 对象
     */
    public static Jedis getJedis() {
        if (Objects.nonNull(jedisPool)) {
            return jedisPool.getResource();
        }
        throw new RuntimeException("连接池创建失败...");
    }
}

使用到了传说中的 lua 脚本。点击查看 lua 脚本 或者 中文文档

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

经过修改之后发现还是会存在一定问题,Redis 集群是 AP 模式,不像 Zookeeper 是 CP 模式,如下:

在这里插入图片描述在这里插入图片描述

而我们的 Redis 有点像 Eureka 集群,如下:

在这里插入图片描述

因为 Redis 之间通过异步方式通信,所以如果 master 宕机了,并且此时 master 还没有把锁状态同步给从节点,异步复制失败,导致锁丢失。此时上面写的代码又不能保证数据安全性了,所以这个时候只能采用 RedLock,天生解决这种问题,RedLock 红锁的具体实现是交给 Redisson 、并且 Redisson 还是 Java 语言编写的。

    @RequestMapping("/goods7")
    public String buyGoods() {

        RLock lock = redisson.getLock(REDIS_KEY);
        // 加锁
        lock.lock();
        try {
            String result = stringRedisTemplate.opsForValue().get(KEY);
            int num = Objects.isNull(result) ? 0 : Integer.parseInt(result);
            if (num > 0) {
                int realNum = num - 1;
                stringRedisTemplate.opsForValue().set(KEY, Integer.toString(realNum));
                log.info(">>>>>>>>>>你已经成功秒杀了商品,还剩余{}件,serverPort={}", realNum,serverPort);
                return "你已经成功秒杀了商品,还剩余" + realNum + "件"+serverPort;
            }
            log.info(">>>>>>>>>>活动已经售罄,欢迎下次光临."+serverPort);
        } finally {
            // 直接调用 unlock() 解锁即可
            lock.unlock();
        }
        return "活动已经售罄,欢迎下次光临"+serverPort;
    }

但是这里还是存在一个问题,就是解锁不能解锁别人的锁,需要判断能不能解锁才可以解锁,修改之后如下:


    @RequestMapping("/goods9")
    public String buyGoods() {

        // 直接上 Redisson
        RLock lock = redisson.getLock(REDIS_KEY);
        // 加锁
        lock.lock();
        try {
            String result = stringRedisTemplate.opsForValue().get(KEY);
            int num = Objects.isNull(result) ? 0 : Integer.parseInt(result);
            if (num > 0) {
                int realNum = num - 1;
                stringRedisTemplate.opsForValue().set(KEY, Integer.toString(realNum));
                log.info(">>>>>>>>>>你已经成功秒杀了商品,还剩余{}件,serverPort={}", realNum,serverPort);
                return "你已经成功秒杀了商品,还剩余" + realNum + "件"+serverPort;
            }
            log.info(">>>>>>>>>>活动已经售罄,欢迎下次光临."+serverPort);
        } finally {
            // 先判断自己是否能去释放这把锁
            if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                // 直接调用 unlock() 解锁即可
                lock.unlock();
            }
        }

        return "活动已经售罄,欢迎下次光临"+serverPort;
    }

经过压测后数据正常,如下:

在这里插入图片描述

但是最终发现还是会存在一个非常严重的问题,就是你的锁失效时间具体要怎么填写?你怎么知道要设置多长的过期时间,这个是要和你的处理业务时间联系再一起,通常会经过 Jmeter 工具测试 qps,然后看平均耗时,但是这个还不是一个好的解决方案。

其实我们可以为这个过期时间续命,或者说是刷新过期时间,在你处理业务的时候,后台线程去刷新这个过期时间。

下面先插个小插曲,先来了解下 Redisson 的东西

Redisson 文档: http://redis.cn/topics/

Redis 分布式锁:http://redis.cn/topics/distlock.html

Github 地址: https://github.com/redisson/redisson

传统的基于 setnx 分布式锁有什么缺点,如下图示?

在这里插入图片描述

在这里插入图片描述

上述图片讲述了基于 setnx 传统的分布式锁的缺点,那么怎么解决呢?

Redis 中就提供了 RedLock 算法,用来实现基于多个实例的分布式锁,锁由多个实例维护,这样即使你有实例发生故障,锁变量依旧还存在,客户端还可以继续完成锁操作。RedLock 算法是实现高可靠用分布式锁的一种有效解决方案,可以在实际开发中应用。

在多主机群模式中,需要部署几台 Redis 服务器,由计算容错率公式推到:

N(需部署 Redis 台数) = 2 * X(宕机的 Redis 台数) + 1(奇数+1),

假设我么允许有 1 台机器宕机,那么最少部署 2 * 1+1 = 3 台 Redis 服务器就可以保证高可用集群
假设我么允许有 2 台机器宕机,那么最少部署 2 * 1+1 = 5 台 Redis 服务器就可以保证高可用集群

+1 的操作是用最少的开销做到高可用集群,+2 的话虽然也可以,但是你需要多准别一台服务器,实现的效果和 +1 实现的效果是一样的。

采用 docker 部署三台 Redis 服务器

docker 命令如下:

docker run -p 6381:6379 --name redis-master-1 -d redis:6.0.7
docker run -p 6382:6379 --name redis-master-2 -d redis:6.0.7
docker run -p 6383:6379 --name redis-master-3 -d redis:6.0.7

配置类如下:

@ConfigurationProperties(prefix = "spring.redis", ignoreUnknownFields = false)
@Data
public class RedisPropertiesAutoConfiguration {

    private int database;

    /**
     * 等待节点回复命令的时间。该时间从命令发送成功时开始计时
     */
    private int timeout;
    private String password;
    private String mode;

    /**
     * 池配置
     */
    private RedisPoolProperties pool;

    /**
     * 单机信息配置
     */
    private RedisSingleProperties single;
}

@Data
public class RedisPoolProperties {

    private int maxIdle;
    private int minIdle;
    private int maxActive;
    private int maxWait;
    private int connTimeout;
    private int soTimeout;

    /**
     * 池大小
     */
    private  int size;
}

@Data
public class RedisSingleProperties {
    private  String address1;
    private  String address2;
    private  String address3;
}

然后配置三个 Redisson 客户端,如下:


@Configuration
@EnableConfigurationProperties(RedisPropertiesAutoConfiguration.class)
public class CacheConfiguration {

    @Autowired
    RedisPropertiesAutoConfiguration redisPropertiesAutoConfiguration;

    @Bean
    RedissonClient redissonClient1() {
        Config config = new Config();
        String node = redisPropertiesAutoConfiguration.getSingle().getAddress1();
        node = node.startsWith("redis://") ? node : "redis://" + node;
        SingleServerConfig serverConfig = config.useSingleServer()
                .setAddress(node)
                .setTimeout(redisPropertiesAutoConfiguration.getPool().getConnTimeout())
                .setConnectionPoolSize(redisPropertiesAutoConfiguration.getPool().getSize())
                .setConnectionMinimumIdleSize(redisPropertiesAutoConfiguration.getPool().getMinIdle());
        if (StringUtils.isNotBlank(redisPropertiesAutoConfiguration.getPassword())) {
            serverConfig.setPassword(redisPropertiesAutoConfiguration.getPassword());
        }
        return Redisson.create(config);
    }

    @Bean
    RedissonClient redissonClient2() {
        Config config = new Config();
        String node = redisPropertiesAutoConfiguration.getSingle().getAddress2();
        node = node.startsWith("redis://") ? node : "redis://" + node;
        SingleServerConfig serverConfig = config.useSingleServer()
                .setAddress(node)
                .setTimeout(redisPropertiesAutoConfiguration.getPool().getConnTimeout())
                .setConnectionPoolSize(redisPropertiesAutoConfiguration.getPool().getSize())
                .setConnectionMinimumIdleSize(redisPropertiesAutoConfiguration.getPool().getMinIdle());
        if (StringUtils.isNotBlank(redisPropertiesAutoConfiguration.getPassword())) {
            serverConfig.setPassword(redisPropertiesAutoConfiguration.getPassword());
        }
        return Redisson.create(config);
    }

    @Bean
    RedissonClient redissonClient3() {
        Config config = new Config();
        String node = redisPropertiesAutoConfiguration.getSingle().getAddress3();
        node = node.startsWith("redis://") ? node : "redis://" + node;
        SingleServerConfig serverConfig = config.useSingleServer()
                .setAddress(node)
                .setTimeout(redisPropertiesAutoConfiguration.getPool().getConnTimeout())
                .setConnectionPoolSize(redisPropertiesAutoConfiguration.getPool().getSize())
                .setConnectionMinimumIdleSize(redisPropertiesAutoConfiguration.getPool().getMinIdle());
        if (StringUtils.isNotBlank(redisPropertiesAutoConfiguration.getPassword())) {
            serverConfig.setPassword(redisPropertiesAutoConfiguration.getPassword());
        }
        return Redisson.create(config);
    }

最终代码如下:


    @RequestMapping("/goods10")
    public String buyGoods() {

        // 直接上 Redisson
        RLock lock1 = redissonClient1.getLock(REDIS_KEY);
        RLock lock2 = redissonClient2.getLock(REDIS_KEY);
        RLock lock3 = redissonClient3.getLock(REDIS_KEY);

        RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
        boolean isLockBoolean;
        try {
            // 开始加锁
            isLockBoolean = redLock.tryLock(3, 300, TimeUnit.SECONDS);
             if (isLockBoolean) {
                 String result = stringRedisTemplate.opsForValue().get(KEY);
                 int num = Objects.isNull(result) ? 0 : Integer.parseInt(result);
                 if (num > 0) {
                     int realNum = num - 1;
                     stringRedisTemplate.opsForValue().set(KEY, Integer.toString(realNum));
                     log.info(">>>>>>>>>>你已经成功秒杀了商品,还剩余{}件,serverPort={}", realNum,serverPort);
                     return "你已经成功秒杀了商品,还剩余" + realNum + "件"+serverPort;
                 }
                 log.info(">>>>>>>>>>活动已经售罄,欢迎下次光临."+serverPort);
             }
        } catch (Exception e) {
            e.printStackTrace();
        }
        finally {
            // 先判断自己是否能去释放这把锁
            if (redLock.isLocked() && redLock.isHeldByCurrentThread()) {
                // 直接调用 unlock() 解锁即可
                redLock.unlock();
            }
        }

        return "活动已经售罄,欢迎下次光临"+serverPort;
    }

这样下面数据安全性保证已经提高到 99% 了。现在就剩下最后一个锁续命问题了。

分布式锁在 Redis 中存的数据格式如下所示:

在这里插入图片描述

在 Redisson 的实现中,它会额外开一个线程定期检查线程是否还持有这把锁,如果有则延迟锁过期时间,定期检查默认是每 1/3 的锁时间检查一次,如果锁还持有,那么就会刷新过期时间。而这个额外的线程就叫做 Watch Dog

摘取官网的一段话如下所示:

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

所以最后一个缓存锁续命问题 Redisson 也帮我们实现了,那么现在我们这个分布式锁应该算是比较完整的了。后续的 Redisson 源码分析请看另一篇文章。

所以实现分布式锁优先推荐 Redisson 去实现,使用传统的 setnx 需要自己解决很多问题。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

魔道不误砍柴功

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

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

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

打赏作者

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

抵扣说明:

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

余额充值