自学记录

模拟库存扣减的场景

Redis使用的数据结构是string

key为stock,初始的value为298

redis key 自动 加1 redis 自减_redis

初始代码:

@GetMapping("/deduct_stock1")
    public String deductStack1() {

        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
        if (stock > 0) {
            int realStock = stock - 1;
            stringRedisTemplate.opsForValue().set("stock", realStock + "");
            System.out.println("扣减成功,剩余库存:" + realStock);
        } else {
            System.out.println("扣减失败,库存不足");
        }
        return "end";
    }

问题:可能出现超卖现象。

例如有三个用户并发执行,三个用户都获取了stock的值后进入if判断语句,现在三者获得的stock都是初始值298,那么该业务执行后stock的值是297,但是实际上还多卖给了两个用户,出现超卖现象。

解决方式:使用synchronized 关键字

synchronized 关键字的作用是创建一个同步块,用于对共享资源进行同步控制。它的作用是确保同一时刻只有一个线程可以进入被同步的代码块,从而防止多个线程同时访问或修改共享资源,保证了线程安全性。

@GetMapping("/deduct_stock1")
    public String deductStack1() {
        synchronized (this) {
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            if (stock > 0) {
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", realStock + "");
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
        }
        return "end";
    }

这样改进后看似解决了刚刚提到的超卖问题,但是实际上治标不治本,问题仍然存在。

问题:在实际的业务场景中,会有多个服务器,但是synchronized只对所处的服务中生效,无法影响其他的服务中的业务,所以,当上述问题不是发生在单体项目中,而是发生在多服务项目中,使用synchronized仍然会产生超卖现象

解决方法:使用分布式锁

使用商品id来作为分布式锁的key,使用stringRedisTemplate内置setIfAbsent方法来实现分布式锁,原理等于setnx(k, v),如果redis中有k,则返回false,没有k,则创建分布式锁,返回true。在执行完扣减库存的业务后需要删除分布式锁,给别的线程放行。

@GetMapping("/deduct_stock1")
    public String deductStack1() {
        //此处是模拟,实际情况中商品id应该是通过方法中的参数传递过来
        String lockKey = "productId";
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "block");
        if (!result) {
            return "error";
        }
        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
        if (stock > 0) {
            int realStock = stock - 1;
            stringRedisTemplate.opsForValue().set("stock", realStock + "");
            System.out.println("扣减成功,剩余库存:" + realStock);
        } else {
            System.out.println("扣减失败,库存不足");
        }
        stringRedisTemplate.delete(lockKey);
        return "end";
    }

接下来考虑可能出现异常的情况

问题:当第一个持有锁的用户在没有删除锁之前出现异常了,那么就无法释放分布式锁,则别的线程就无法获取分布式锁,从而导致很多用户无法扣减库存。

解决方法:使用try-catch-finally,将删除锁的逻辑放在finally中,则一定会执行删除锁的逻辑

@GetMapping("/deduct_stock1")
    public String deductStack1() {
        //此处是模拟,实际情况中商品id应该是通过方法中的参数传递过来
        String lockKey = "productId";
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "block");
        if (!result) {
            return "error";
        }
        try {
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            if (stock > 0) {
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", realStock + "");
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
        } finally {
            stringRedisTemplate.delete(lockKey);
        }
        return "end";
    }

接下来考虑宕机的情况

问题:当在执行扣减库存的业务的时候,出现了宕机的情况,哪怕有finally也无法执行删除分布式锁的逻辑

解决方法:为分布式锁设定一个超时时间,到了超时时间,则redis自动删除锁。

@GetMapping("/deduct_stock1")
    public String deductStack1() {
        //此处是模拟,实际情况中商品id应该是通过方法中的参数传递过来
        String lockKey = "productId";
        /*
        *   注意:
        *       不能先创建锁,再设置超时时间,一定要创建锁的同时,就设置超时时间
        *       因为,如果分开来设置,在创建锁和设置超时时间中间出现了宕机或异常,仍然无法释放锁
        */
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "block", 10, TimeUnit.SECONDS);
        if (!result) {
            return "error";
        }
        try {
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            if (stock > 0) {
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", realStock + "");
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
        } finally {
            stringRedisTemplate.delete(lockKey);
        }
        return "end";
    }

接下里考虑高并发的场景

问题:出现当前获取锁的用户需要的执行时间大于分布式锁的超时时间,自动释放后,下一个用户获取分布式锁,而当前用户会执行释放锁的操作,就把下一个用户获取的锁释放了,以此类推。如图:

redis key 自动 加1 redis 自减_中间件_02

解决方法:将分布式锁的value设置为UUID,每次删除之前,判断当前锁的value与执行删除锁逻辑的线程的UUID是否相同,不同则等待,相同则删除

@GetMapping("/deduct_stock1")
    public String deductStack1() {
        //此处是模拟,实际情况中商品id应该是通过方法中的参数传递过来
        String lockKey = "productId";
        /*
        *   注意:
        *       不能先创建锁,再设置超时时间,一定要创建锁的同时,就设置超时时间
        *       因为,如果分开来设置,在创建锁和设置超时时间中间出现了宕机或异常,仍然无法释放锁
        */
        String clientId = UUID.randomUUID().toString();
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 10, TimeUnit.SECONDS);
        if (!result) {
            return "error";
        }
        try {
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            if (stock > 0) {
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", realStock + "");
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
        } finally {
            if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
                stringRedisTemplate.delete(lockKey);
            }
        }
        return "end";
    }

这样的情况似乎以及没问题了,但是仔细思考一下,其实仍然存在问题

问题:如果当前逻辑执行到finally中的if语句后,但此时分布式锁超时了,同时下一个用户正好获取了锁,则当前逻辑删除的分布式锁是下一个用户获取的。

解决方法:通过锁续命的操作来解决这一问题,锁续命:如果持有锁的进程或线程在执行任务时需要更多时间,它可以周期性地(通常是在锁即将过期时)发送一个续命请求,以更新锁的过期时间。redisson封装该实现。

@GetMapping("/deduct_stock1")
    public String deductStack1() {
        //此处是模拟,实际情况中商品id应该是通过方法中的参数传递过来
        String lockKey = "productId";
        /*
        *   注意:
        *       不能先创建锁,再设置超时时间,一定要创建锁的同时,就设置超时时间
        *       因为,如果分开来设置,在创建锁和设置超时时间中间出现了宕机或异常,仍然无法释放锁
        */
        String clientId = UUID.randomUUID().toString();
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 10, TimeUnit.SECONDS);
        RLock redissonLock = redisson.getLock(lockKey);
        redissonLock.lock();
        if (!result) {
            return "error";
        }
        try {
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            if (stock > 0) {
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", realStock + "");
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
        } finally {
            redissonLock.unlock();
        }
        return "end";
    }

暂时就优化到这里,随着进一步的学习,或许会有更多需要优化的地方,欢迎批评指正