JAVA实现分布式锁

场景:若某商品库存量为50,存入redis,有一个功能:从redis将库存量读取出来,如果库存大于0,便对库存进行减一操作代表被买走一个,当库存小于0时提示库存不足

目录

搭建场景demo

Redis普通方法实现分布式锁

Redisson实现 分布式锁

阻塞加锁方法


搭建场景demo

 查看Redis中的库存量

image.png

 引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

配置文件加入配置

redis 配置不配 database 默认值是0

spring:
  redis:
    enabled: true
    database: 5
    host: 127.0.0.1
    port: 6379
    password: 123456
    timeout: 36000
    connectionPoolSize: 1
    connectionMinimumIdleSize: 1

接口功能如下

    @GetMapping(value = "/test11")
    public Boolean test11() {
        String key = "stock";
        //此处为自己写的工具类 直接使用stringRedisTemplate就可以
        Integer stock = redisUtil.getCacheObject(key);
        if (stock > 0){
            stock = stock -1;
            redisUtil.setCacheObject(key,stock,2L,TimeUnit.HOURS);
            System.out.println("库存成功减1, 剩余库存" + stock);
        }else {
            System.out.println("库存减少失败,库存不足");
        }
        return true;
    }

发送请求测试该功能成功运行,查看redis库存,库存量减一

image.png

image.png

但是其实这样会出现一个问题:如果有两个或者多个请求同时请求该接口,A请求和B请求都拿到了自行车商品的库存量50减一存入库存数量就会是49,而实际情况是应该剩48辆,这样就导致了并发问题,可以进行并发测试去验证结果

解决办法

加锁,在一个请求过来的时候加锁去解决问题

Redis普通方法实现分布式锁

@GetMapping(value = "/test11")
public Boolean test11() {
    String key = "stock";
    String lockKey = "product101";
    try{
        //如果key存在就为false,且不会在存,不存在就会进行存入
        Boolean product = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,                     
        "product");
        if (!product){
            return false;
        }

        Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get(key));
        if (stock > 0){
            stock = stock -1;
            redisUtil.setCacheObject(key,stock,2L,TimeUnit.HOURS);
            System.out.println("库存成功减1, 剩余库存" + stock);
        }else {
            System.out.println("库存减少失败,库存不足");
        }
        }finally {
            //最后要释放锁
            stringRedisTemplate.delete(lockKey);
        }
        return true;
    }

这样还是会导致问题

锁超时,如果有请求A进来,操作到一半突然挂掉,会导致该lockKey一直存在Redis中,这块资源就会一直被锁住,导致其他线程不能操作。解决办法:给key设置过期时间

设置过期时间之后还会导致问题,如果请求A进来之后操作时间大于设置的key的过期时间就会导致锁"不生效",A请求还没操作完毕,B请求就拿到了锁,到最后释放锁的时候其实A请求释放的是B请求的锁。解决办法:可以加一唯一标识设置到value,最后删除锁的时候去进行判断一下是否是A请求的锁,如果是才进行删除释放

	@GetMapping(value = "/test11")
    public Boolean test11() {
        String key = "stock";
        String lockKey = "product101";

        UUID uuid = UUID.randomUUID();
        //如果key存在就为false,且不会在存,不存在就会进行存入
        Boolean product = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, uuid.toString());
        try{
            if (!product){
                return false;
            }

            Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get(key));
            if (stock > 0){
                stock = stock -1;
                redisUtil.setCacheObject(key,stock,2L,TimeUnit.HOURS);
                System.out.println("库存成功减1, 剩余库存" + stock);
            }else {
                System.out.println("库存减少失败,库存不足");
            }
        }finally {
            //最后要释放锁
            if (Objects.equals(uuid,product)){
                stringRedisTemplate.delete(lockKey);
            }
        }
        return true;
    }

image.png

虽然解决了以上的误删情况,但是还是没有解决同一时间有两个线程在操作该方法块,依然不完美。

解决办法: 利用Redission实现分布式锁

Redisson实现 分布式锁

学习参考链接 redisson 实现分布式锁

添加依赖

 <!--Redis分布式锁-->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.5.0</version>
</dependency>

配置RedissonClient

创建RedisProperties类,装配redis配置

@Data
@Component
@ConfigurationProperties(prefix = "spring.redis")
public class RedisProperties {

    private String host;
    private int port;
    private String password;
    private int database;

}

创建RedissionConfig类,配置RedissonClient

@Configuration
@Log4j2
public class RedissionConfig {

    @Resource
    RedisProperties redisProperties;

    @Bean
    public RedissonClient redissonClient() {
        RedissonClient redissonClient;
        Config config = new Config();

        System.out.println(redisProperties.getHost());

        String url = "redis://" + redisProperties.getHost() + ":" + redisProperties.getPort();
        config.useSingleServer().setAddress(url)
                .setPassword(redisProperties.getPassword())
                .setDatabase(redisProperties.getDatabase());
        try {
            redissonClient = Redisson.create(config);
            return redissonClient;
        }catch (Exception e){
            log.error("RedissonClient init redis url" + url + " Exception " + e);
            return null;
        }
    }
}

加锁解锁方法

从阻塞和非阻塞的特性来区分,加锁的方式又分为阻塞加锁和非阻塞加锁两类

阻塞加锁方法

  1. void lock()​ 如果当前锁可用,则加锁成功,并立即返回如果当前锁不可用就一直阻塞等到锁可用,然后返回。
  2. ​void lock(long leaseTime, TimeUnit unit)​ 加锁的机制是和​​void lock()​一致的,在此基础上增加了锁可用的时间,加锁成功后,如果没有调用显示​​unlock()​方法去解锁,到这个时间之后会自动解锁,如果​​leaseTime​传入-1则一直会持有直到​​unlock

​非阻塞加锁方法

  1. ​boolea tryLock()​调用该方法之后会立刻返回。返回值为true时则认为可用,加锁成功。返回值为false表示该锁不能使用,加锁失败
  2. ​boolean tryLock(long time, TimeUnit unit)​ 如果锁可用立刻返回true,否则最多等待​​time​时间,如果​time<=0​则不会等待,在time时间内锁可用则立刻返回true,time时间之后返回false。如果在等待期间线程被其他线程中断,则会抛出InterruptedException异常
  3. ​boolean tryLock(long waitTime, long leaseTime, TimeUnit unit)​与​boolean tryLock(long time, TimeUnit unit) 相同​​,增加了过期时间

释放锁

​void unlock()​ 释放锁。如果当前线程是锁的持有者(即在该锁实例上加锁成功的线程),则会释放成功,否则会抛出异常。

写一个解锁加锁的工具类

@Component
@Log4j2
public class RedissonLockUtil {

    @Autowired
    private RedissonClient redissonClient;

    /**
     * 加锁 (可重入锁) 阻塞加锁
     *
     * @param lockName 锁名
     * @param expireTime 过期时间
     * @param unit 时间粒度
     * @return
     */
    public Boolean lock(String lockName, long expireTime, TimeUnit unit){
        try{
            if (redissonClient == null){
                log.error("redissonClient为空");
                return false;
            }
            //获取锁实例
            RLock lock = redissonClient.getLock(lockName);
            //锁多少时间后自动释放,防止死锁
            lock.lock(expireTime, unit);
            log.info("线程" + Thread.currentThread().getName() + "加锁" + lockName + "成功");
            return true;
        }catch (Exception e){
            log.error("加锁异常" + lockName + "Exception" + e);
            return false;
        }
    }

    /**
     * 加锁 (可重入锁) 非阻塞加锁
     *
     * @param lockName 锁名
     * @param expireTime 过期时间
     * @param unit 时间粒度
     * @param waitTime 等待时长
     * @return
     */
    public Boolean tryLock(String lockName, long expireTime, TimeUnit unit, long waitTime){
        try {
            if (redissonClient == null){
                log.error("redissonClient为空");
                return false;
            }
            RLock lock = redissonClient.getLock(lockName);
            boolean b = lock.tryLock(waitTime, expireTime, unit);
            if (b){
                log.info("线程" + Thread.currentThread().getName() + "加锁" + lockName + "成功");
                return true;
            }
            return false;
        }catch (InterruptedException e){
            log.error("加锁异常" + lockName + "Exception" + e);
            return false;
        }
    }

    /**
     * 解锁
     *
     * @param lockName 锁名
     * @return
     */
    public Boolean unlock(String lockName) {
        try{
            if (redissonClient == null){
                log.error("redissonClient为空");
                return false;
            }
            RLock lock = redissonClient.getLock(lockName);
            if (lock.isLocked()){
                //判断当前线程释放的锁是否属于该线程
                if (lock.isHeldByCurrentThread()) {
                    //主动释放锁
                    lock.unlock();
                    log.info("线程" + Thread.currentThread().getName() + "解锁" + lockName + "成功");
                    return true;
                }
            }
            return true;
        }catch (Exception e){
            log.error(Thread.currentThread().getName() + "解锁异常:" + lockName);
            return false;
        }
    }

}

测试(在高并发的情况下测试效果更加明显)

阻塞锁

    @Resource
    RedissonLockUtil redissonLockUtil;

    @GetMapping(value = "/test15")
    public Boolean test15() {
        String key = "stock";
        String lockKey = "lock";
        Boolean lock = redissonLockUtil.lock(lockKey,30,TimeUnit.SECONDS);
        if (lock){
            Integer stock = redisUtil.getCacheObject(key);
            if (stock == null){
                return false;
            }
            if (stock > 0){
                stock = stock -1;
                redisUtil.setCacheObject(key,stock,2L,TimeUnit.HOURS);
                System.out.println("库存成功减1, 剩余库存" + stock);
                redissonLockUtil.unlock(lockKey);
            }else {
                System.out.println("库存减少失败,库存不足");
            }
        }
        return true;
    }

运行结果

image.png

非阻塞锁

    @Resource
    RedissonLockUtil redissonLockUtil;

    @GetMapping(value = "/test17")
    public Boolean test17() {
        String key = "stock";
        String lockKey = "lock";
        Boolean lock = redissonLockUtil.tryLock(lockKey,30,TimeUnit.SECONDS,50);
        if (lock){
            Integer stock = redisUtil.getCacheObject(key);
            if (stock == null){
                return false;
            }
            if (stock > 0){
                stock = stock -1;
                redisUtil.setCacheObject(key,stock,2L,TimeUnit.HOURS);
                System.out.println("库存成功减1, 剩余库存" + stock);
                redissonLockUtil.unlock(lockKey);
            }else {
                System.out.println("库存减少失败,库存不足");
            }
        }
        return true;
    }

运行结果

image.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值