Redis分布式锁-最全版

简介

现在很多互联网公司的网站、应用至少都会部署2台以上的机器,形成一个分布式服务部署架构,用以解决单机服务部署架构下的很多问题,比如提升QPS、TPS。但是同时也带来了其他问题,比如事务处理、超卖,当然这些问题都是有解决方案的,本篇文章探讨下用分布式锁来解决我们常说的超卖问题。分布式锁的实现方案也有很多,借助zookeeper、dubbo、redis等都可以实现。本篇用redis实现分布式锁,并逐步分析实现过程中存在的bug,并针对发现的bug逐步完善代码,写一个大型互联网公司,比如类似某东、某宝这些会产生非常高并发的网站常用的实现方案。

初始方案

用redis的setnx原子操作设值命令。

@Autowired
    private StringRedisTemplate stringRedisTemplate;
    @RequestMapping("/test")
    public String test() throws InterruptedException{
        synchronized (this) {
        	//假如开始我们的库存stock在redis里面初始值是30
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); //相当于redis.get("stock")
            if (stock > 0){
                //每访问一次库存就减去1
                int realStock = stock - 1;
                //把剩余库存重新设置到redis
                stringRedisTemplate.opsForValue().set("stock", realStock + ""); //相当于iedis.set(key,value)
                System.out.println("扣减成功,剩余库存:" + realStock);
            }else {
                System.out.println("扣减失败, 库存不足");
            }
        }
        return "处理结束";
    }

以上这种写法是常规的库存业务处理,单机部署访问,这种写法是没问题的,但多台机器部署,并且产生并发访问,就会出现库存超买,因为synchronized是单机锁,只能在同一个JVM进程下生效。

部署多台机器的方式,启动多个服务端口,springboot应用就很方便,改下端口号直接启动就好,用nginx配置多台机器负责均衡,用JMeter模拟同时发送几百个请求

怎么判断出现超买,假如部署了两台机器,如果两台机器都打印了日志 “扣减成功,剩余库存:15”,也就是出现多台机器打印相同的剩余库存,就是超买了。

代码优化 1

@Autowired
    private StringRedisTemplate stringRedisTemplate;
    @RequestMapping("/test")
    public String test() throws InterruptedException{
        String lockKey = "商品ID";
        try {
            Boolean setStatus = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"lock", 10L, TimeUnit.SECONDS);
            if (!setStatus) {
                return "系统繁忙,请稍等!";
            }
            //假如开始我们的库存stock在redis里面初始值是30
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); //相当于redis.get("stock")
            if (stock > 0){
                //每访问一次库存就减去1
                int realStock = stock - 1;
                //把剩余库存重新设置到redis
                stringRedisTemplate.opsForValue().set("stock", realStock + ""); //相当于iedis.set(key,value)
                System.out.println("扣减成功,剩余库存:" + realStock);
            }else {
                System.out.println("扣减失败, 库存不足");
            }
        } finally {
            stringRedisTemplate.delete(lockKey);
        }
        return "处理结束";
    }

这种代码的思路分析:try-finally捕获异常防止代码处理过程中出现的各种bug导致redis锁没有释放,设置redis过期时间防止redis服务停了或者其他意外错误导致没有释放锁,要注意redis的过期时间一定要和值同时设置,不能分开两个命令单独设置值和过期时间,防止设置了值之后没来得及设置过期时间redis服务停了,保证原子操作。(setIfAbsent底层是redis的sexnx和expire命令,因为redis是天生的单例模式即单线程模式,所以无论多少个请求到来,始终都会进行先后顺序排队,一个个执行,所以redis能对同一个资源加锁)。这种写法可以解决一般的问题。

但这种写法还会有bug,而且很难排查。看下面的图就会明白这个bug出现的场景。
假如线程1处理完要15秒,锁过期是10秒,线程1未处理完,锁就过期了,同时线程2来了并加了一把锁,到线程1处理完,同时也会释放锁,而此时释放的也是线程2的锁,这时线程3可以直接进来加锁等处理,依次类推到第4个线程第5个线程 …,这种情况下就导致了锁永久失效。

请添加图片描述

代码优化 2

这种写法避免了锁永久失效

@Autowired
    private StringRedisTemplate stringRedisTemplate;
    @RequestMapping("/test")
    public String test() throws InterruptedException{
        String lockKey = "商品ID";
        String markId = UUID.randomUUID().toString();
        try {
            Boolean setStatus = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,markId, 10L, TimeUnit.SECONDS);
            if (!setStatus) {
                return "系统繁忙,请稍等!";
            }
            //假如开始我们的库存stock在redis里面初始值是30
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); //相当于redis.get("stock")
            if (stock > 0){
                //每访问一次库存就减去1
                int realStock = stock - 1;
                //把剩余库存重新设置到redis
                stringRedisTemplate.opsForValue().set("stock", realStock + ""); //相当于iedis.set(key,value)
                System.out.println("扣减成功,剩余库存:" + realStock);
            }else {
                System.out.println("扣减失败, 库存不足");
            }
        } finally {
            //加一个标记值,如果是当前线程的值则可以处理释放线程,防止释放掉不属于当前线程的锁,导致锁永久失效
            if (markId.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
                stringRedisTemplate.delete(lockKey);
            }
        }
        return "处理结束";
    }

还可以对这种写法继续进行优化,代码如下:
这种写法是用 Redisson做分布式锁,解决了锁永久失效,它的底层原理:为当前线程单独开一个子线程,在子线程里面做一个定时即是循环,不断重复检测当前线程的锁是否还未失效,如果是则重新设置过期时间,不断重新设置,为锁续命,这种实现方式在Redissson中叫WatchDog,这样就避免了线程未处理完锁就失效的问题。

	/**
     * 初始化redisson客户端
     * @return
     */
    public Redisson redisson () {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(0);
        return (Redisson) Redisson.create(config);
    }

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private Redisson redisson;
    @RequestMapping("/test")
    public String test() throws InterruptedException{
        String lockKey = "商品ID";
        RLock redissonLock = redisson.getLock(lockKey);
        try {
            redissonLock.lock(30, TimeUnit.SECONDS);
            //假如开始我们的库存stock在redis里面初始值是30
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); //相当于redis.get("stock")
            if (stock > 0){
                //每访问一次库存就减去1
                int realStock = stock - 1;
                //把剩余库存重新设置到redis
                stringRedisTemplate.opsForValue().set("stock", realStock + ""); //相当于iedis.set(key,value)
                System.out.println("扣减成功,剩余库存:" + realStock);
            }else {
                System.out.println("扣减失败, 库存不足");
            }
        } finally {
            redissonLock.unlock();
        }
        return "处理结束";
    }

====================End ====================

好了,码字留作学习记录,期待各位斧正。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值