【redis】使用redis实现简单的分布式锁,秒杀并发场景可用

在很多秒杀并发的场景下很容易造成库存超卖,我们需要保证库存不被超卖,我们该怎么做呢?

在单机应用中,防止超卖可以使用jdk自带的synchronized关键字来处理。但是在分布式系统应用下使用synchronized关键字就不生效了。那我们可以怎么做呢?下面我用两种解决方案实现,下面实现代码。

一、我们先使用jdk自带的synchronized来试一下,看看是否有效?

@Slf4j
@RestController
@RequestMapping("miaosha")
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class MiaoShaController {
    
    

    private final RedisTemplate<String, String> redisTemplate;

    private final Object obj1 = new Object();
    private final Object obj2 = new Object();

    @PostMapping("distributedLock1")
    public void distributedLock1() {
    
    
        synchronized (obj1) {
    
    
            int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
            if (stock > 0) {
    
    
                int remainingStock = stock - 1;
                redisTemplate.opsForValue().set("stock", String.valueOf(remainingStock));
                log.info("1扣减成功!剩余库存:" + remainingStock);
            } else {
    
    
                log.error("1库存不足!");
            }
        }
    }
    
    @PostMapping("distributedLock2")
    public void distributedLock2() {
    
    
        synchronized (obj2) {
    
    
            int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
            if (stock > 0) {
    
    
                int remainingStock = stock - 1;
                redisTemplate.opsForValue().set("stock", String.valueOf(remainingStock));
                log.info("2扣减成功!剩余库存:" + remainingStock);
            } else {
    
    
                log.error("2库存不足!");
            }
        }
    }
}

注:这里我使用了两个接口来模仿分布式环境,使用压力测试工具运行。这里我使用的是jmeter,本篇文章就不说怎么安装使用了。

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

200个并发,发起两次,两个接口,一共800次请求。
点击运行,查看结果。

在这里插入图片描述
可以看到出现了超卖的情况,由于在分布式环境下synchronized只作用于同一个jvm下。

二、解决方案一:可以使用redis中的setIfAbsent(相当于jedis中的setnx

setIfAbsent、setnx介绍

将 key 的值设为 value,当且仅当 key 不存在。
若给定的 key 已经存在,则 SETNX 不做任何动作。
SETNX 是SET if Not eXists的简写。

改造后的代码一

@Slf4j
@RestController
@RequestMapping("miaosha")
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class MiaoShaController {
    
    

    private final RedisTemplate<String, String> redisTemplate;

    @PostMapping("distributedLock1")
    public void distributedLock1() {
    
    
        // 改造成redis的setnx
        String lockKey = "lockKey";
        // 添加uuid的value,防止程序运行10秒以上之后锁被自动清除,执行finally的时候会把后面进来的请求加好的锁删除,会造成超卖现象
        String clientId = String.valueOf(UUID.randomUUID());
        Boolean bool = false;
        try {
    
    
            // 设置redis锁,10秒后自动清除
            bool = redisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 10, TimeUnit.SECONDS);
            if (!bool) {
    
    
                log.warn("1业务繁忙,请稍后再试!");
                return;
            }
            int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
            if (stock > 0) {
    
    
                int remainingStock = stock - 1;
                redisTemplate.opsForValue().set("stock", String.valueOf(remainingStock));
                log.info("1扣减成功!剩余库存:" + remainingStock);
            } else {
    
    
                log.error("1库存不足!");
            }
        } catch (Exception e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            // 为了保险起见,这边再把锁删除
            if (bool && clientId.equals(redisTemplate.opsForValue().get(lockKey))) {
    
    
                redisTemplate.delete(lockKey);
            }
        }
    }

    @PostMapping("distributedLock2")
    public void distributedLock2() {
    
    
        // 改造成redis的setnx
        String lockKey = "lockKey";
        // 添加uuid的value,防止程序运行10秒以上之后锁被自动清除,执行finally的时候会把后面进来的请求加好的锁删除,会造成超卖现象
        String clientId = String.valueOf(UUID.randomUUID());
        Boolean bool = false;
        try {
    
    
            // 设置redis锁,10秒后自动清除
            bool = redisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 10, TimeUnit.SECONDS);
            if (!bool) {
    
    
                log.warn("2业务繁忙,请稍后再试!");
                return;
            }
            int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
            if (stock > 0) {
    
    
                int remainingStock = stock - 1;
                redisTemplate.opsForValue().set("stock", String.valueOf(remainingStock));
                log.info("2扣减成功!剩余库存:" + remainingStock);
            } else {
    
    
                log.error("2库存不足!");
            }
        } catch (Exception e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            // 为了保险起见,这边再把锁删除
            if (bool && clientId.equals(redisTemplate.opsForValue().get(lockKey))) {
    
    
                redisTemplate.delete(lockKey);
            }
        }
    }
}

还是800次请求,测试运行结果:

在这里插入图片描述
可以看到库存没有出现了超卖,800的请求有24个人抢到了,100个库存,现在看看redis中是否剩余76。

在这里插入图片描述
结果正确。

这样实现基本在一般业务下是没有问题的,但是在超级高并发的情况下,也有可能造成超卖的情况。虽然概率很小,但是我们也要优化到最优。

三、解决方案二:redisson实现分布式锁

官网:https://redisson.org

Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。

以下是Redisson的结构:

在这里插入图片描述
Redisson作为独立节点 可以用于独立执行其他节点发布到分布式执行服务 和 分布式调度任务服务 里的远程任务。

redisson.getLock底层实现原理
会生成一个长达15秒的锁,在业务代码执行过程中,redisson会判断当前业务是否执行完毕,若某种原因造成业务无法15秒内执行完,redisson会自动延长15秒的时间。我们只需要最后释放锁就可以了,操作简单。

改造后的代码二

package xgg.miaosha.demo.controller;

import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author xiegege
 * @date 2020/10/6 14:17
 */
@Slf4j
@RestController
@RequestMapping("miaosha")
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class MiaoShaController {
    
    

    private final RedisTemplate<String, String> redisTemplate;
    private final Redisson redisson;

    @ApiOperation("秒杀、并发场景下分布式锁")
    @PostMapping("distributedLock1")
    public void distributedLock1() {
    
    
        // 改造成redisson
        String lockKey = "lockKey";
        // 获取锁对象
        RLock lock = redisson.getLock(lockKey);
        try {
    
    
            // 加锁
            lock.lock();
            int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
            if (stock > 0) {
    
    
                int remainingStock = stock - 1;
                redisTemplate.opsForValue().set("stock", String.valueOf(remainingStock));
                log.info("1扣减成功!剩余库存:" + remainingStock);
            } else {
    
    
                log.error("1库存不足!");
            }
        } catch (Exception e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            // 释放锁
            lock.unlock();
        }
    }

    @ApiOperation("秒杀、并发场景下分布式锁")
    @PostMapping("distributedLock2")
    public void distributedLock2() {
    
    
        // 改造成redisson
        String lockKey = "lockKey";
        // 获取锁对象
        RLock lock = redisson.getLock(lockKey);
        try {
    
    
            // 加锁
            lock.lock();
            int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
            if (stock > 0) {
    
    
                int remainingStock = stock - 1;
                redisTemplate.opsForValue().set("stock", String.valueOf(remainingStock));
                log.info("2扣减成功!剩余库存:" + remainingStock);
            } else {
    
    
                log.error("2库存不足!");
            }
        } catch (Exception e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            // 释放锁
            lock.unlock();
        }
    }
}

测试运行结果:

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

还是100个库存,最后扣减到了0,可以看到没有超卖的情况。

完美解决了秒杀并发超卖的情况。

总结

在一般情况下可以使用第一种改造方法,如果是并发非常高的业务场景下可以使用第二种。

如果觉得不错,可以点赞+收藏或者关注下博主。感谢阅读!

猜你喜欢

转载自blog.csdn.net/weixin_42825651/article/details/108941107