分布式锁(三)——基于redis的分布式锁实例

之前介绍分布式锁的时候,给过一个漫画介绍分布式锁的链接,传送门——漫画说明分布式锁这篇总结写的确实不错。但是有些地方我们可以按照其他思路来进行实现。

这里先需要了解一下各种失败机制,failover,failsafe,failfast,failback,forking

整体介绍

如果要实现一个锁的机制,无非就是加锁,释放锁,已经其他异常场景下的几种处理。在redis下如何实现这些操作还是值得探讨的。

加锁

老版本的redis可以通过setnx命令来实现分布式锁(本篇博客也会采用这种方式)setnx命令文档,这个命令在key已经存在的情况下会返回0,在key不存在的情况下才会返回1,至于key所对应的value,这个根据业务随意就好。

解锁

解锁就很简单了,直接del指定的key值就可以了

异常场景的处理

锁超时

如果一个得到锁的线程在执行任务的过程中遇到了异常,来不及显示的释放分布式锁,则这个资源就会被永远锁住,这个是要处理的。可以给redis中指定的key设置一个过期时间,就可以解决这一问题,不过个人觉得设置一个标志位似乎更加合理一点

在高一点的redis版本中可以通过set命令设置过期时间

误删

在设置了过期时间之后,如果一个进程A执行很慢,在规定时间中没有顺利执行完指定的业务逻辑代码,锁就被释放了,之后进程B获得了锁(这个锁key值相同,但是value值不同)。进程B执行一段时间之后,进程A如果没有做value值的判断,则会删除进程B的分布式锁。因此在释放锁的时候,需要判断一下value值

这里只是简单的列举了两个问题,造成这些问题的原因无非就一个——加锁/解锁+业务操作的原子性

简单版本

先上一个简单的版本,我们利用setNx获取锁。

 /*
 * 基于redis的分布式锁
 * @param productLockDto
 * @return
 */
@Transactional(rollbackFor = Exception.class)
public int updateStockRedisLock(ProductLockDto productLockDto){
    int result = 0;
    final String key = String.format("redis_lock_product_id:%s",productLockDto.getId());
    String value = UUID.randomUUID().toString()+System.nanoTime();
    //利用setNx操作建立锁。
    Boolean res = stringRedisTemplate.opsForValue().setIfAbsent(key,value);
    if(res){
        //开始核心的业务逻辑处理
        ProductLock productLockEntity = lockMapper.selectByPrimaryKey(productLockDto.getId());
        if(productLockEntity!=null && productLockEntity.getStock().compareTo(productLockDto.getStock())>=0){
            productLockEntity.setStock(productLockDto.getStock());
            //与普通的更新操作相比,仅仅是增加了版本号的统计
            result = lockMapper.updateStockForNegative(productLockEntity);

            if(result>0){
                log.info("通过redis的分布式锁更新成功,剩余库存stock={}",productLockDto.getStock());
            }
        }
    }
    return result;
}

但是比较恶心的是,这里没有释放锁的操作,因此只能获取一次锁,然后操作一次,之后并没有释放锁,导致系统只能更新一次数据。日志如下所示:
在这里插入图片描述
数据库中也只是显示更新一次
在这里插入图片描述

正确释放锁

上述的代码中没有释放锁的操作,导致是有一个进程能对数据进行操作,但是在整体介绍部分说过,在释放锁之前需要对比一下redis锁的key值,避免出现进程A删除进程B对应的分布式锁的key值。

/* 
* 基于redis的分布式锁
 * @param productLockDto
 * @return
 */
@Transactional(rollbackFor = Exception.class)
public int updateStockRedisLock(ProductLockDto productLockDto){
    int result = 0;
    final String key = String.format("redis_lock_product_id:%s",productLockDto.getId());
    String value = UUID.randomUUID().toString()+System.nanoTime();
    //利用setNx操作建立锁。
    Boolean res = stringRedisTemplate.opsForValue().setIfAbsent(key,value);
    try{
        if(res){
            //真正的更新操作
            ProductLock productLockEntity = lockMapper.selectByPrimaryKey(productLockDto.getId());
            if(productLockEntity!=null && productLockEntity.getStock().compareTo(productLockDto.getStock())>=0){
                productLockEntity.setStock(productLockDto.getStock());
                //与普通的更新操作相比,仅仅是增加了版本号的统计
                result = lockMapper.updateStockForNegative(productLockEntity);

                if(result>0){
                    log.info("通过redis的分布式锁更新成功,剩余库存stock={}",productLockEntity.getStock());
                }
            }
        }
        //失败了就失败了,这里什么都没做。
    }catch (Exception e){
        log.error("出现异常,异常信息为:{}",e.fillInStackTrace());
    }finally {//无论如何,这里都需要释放锁。
        String redisValue = stringRedisTemplate.opsForValue().get(key);
        if(value.equals(redisValue)){//之前分析过,为了避免出现进程A删掉进程B的Key,这里需要做一个判断
            stringRedisTemplate.delete(key);
        }
    }
    return result;
}

引入了try-finally语句块,这样就能保证在任何时候都能释放锁,在释放锁的时候进行了一个必要的判断,这样能正确处理数据。同样用jmeter模拟2000个线程之后,数据如下。
在这里插入图片描述
锁正确释放之后,多个进程能进行数据操作。
在这里插入图片描述

2000个进程,最终只是500多个进程抢到了锁,因为我们采取了failover的机制。有些业务场景中,我们需要让这些获取锁失败的进程进行一个轮询,然后再次加入锁的竞争中。

轮询获取锁

先直接上实例吧

/*
 * 基于redis的分布式锁
 *
 * @param productLockDto
 * @return
 */
@Transactional(rollbackFor = Exception.class)
public int updateStockRedisLock(ProductLockDto productLockDto) {
    int result = 0;
    final String key = String.format("redis_lock_product_id:%s", productLockDto.getId());
    Boolean res = true;//利用一个标志位。根据标志位不断轮询判断
    while (res) {
        String value = UUID.randomUUID().toString() + System.nanoTime();
        //利用setNx操作建立锁。
        res = stringRedisTemplate.opsForValue().setIfAbsent(key, value);
        if (res) {
            try {
                res = false;//将标志位置为false,为了后续跳出循环
                //真正的更新操作
                ProductLock productLockEntity = lockMapper.selectByPrimaryKey(productLockDto.getId());
                int recordStock = productLockEntity.getStock();
                if (productLockEntity != null && productLockEntity.getStock().compareTo(productLockDto.getStock()) >= 0) {
                    productLockEntity.setStock(productLockDto.getStock());
                    //与普通的更新操作相比,仅仅是增加了版本号的统计
                    result = lockMapper.updateStockForNegative(productLockEntity);

                    if (result > 0) {
                        log.info("通过redis的分布式锁更新成功,剩余库存stock={}", recordStock - 1);
                    }
                }
            } catch (Exception e) {
                log.error("出现异常,异常信息为:{}", e.fillInStackTrace());
            } finally {
                String redisValue = stringRedisTemplate.opsForValue().get(key);
                if (value.equals(redisValue)) {//之前分析过,为了避免出现进程A删掉进程B的Key,这里需要做一个判断
                    stringRedisTemplate.delete(key);
                }
            }
        }else{
            res=true;//让获取分布式锁失败的线程继续获取锁。
        }
    }
    return result;
}

这个版本中提现了轮询的思想,这个在JDK中可重入锁的源码中有很多体现。这里不再赘述,具体数据结果如下(2000个线程,需要适当扩大数据库连接池的配置)
在这里插入图片描述
可以看到每个请求均正常获取到相关数据。模拟的2000个线程均正常更新了数据。

总结

本篇博客简单总结了redis中分布式锁的实践,篇实战,没啥可总结的。

发布了129 篇原创文章 · 获赞 37 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/liman65727/article/details/104097880