(四)库存超卖案例实战——优化redis分布式锁

前言

在上一节内容中,我们已经实现了使用redis分布式锁解决商品"超卖"的问题,本节内容是对redis分布式锁的优化。在上一节的redis分布式锁中,我们的锁有俩个可以优化的问题。第一,锁需要实现可重入,同一个线程不用重复去获取锁;第二,锁没有续期功能,导致业务没有执行完成就已经释放了锁,存在一定的并发访问问题。本案例中通过使用redis的hash数据结构实现可重入锁,使用Timer实现锁的续期功能,完成redis分布式锁的优化。最后,我们通过集成第三方redisson工具包,完成分布式锁以上俩点的优化内容。Redisson提供了简单易用的API,使得开发人员可以轻松地在分布式环境中使用Redis。

正文

  • **加锁的lua脚本:**使用exists和hexists指令判断是否存在锁,如果不存在或者存在锁并且该锁下面的field有值,就使用hincrby指令使锁的值加1,实现可重入,否则直接返回0,加锁失败。
Lua 复制代码
if redis.call('exists', KEYS[1]) == 0 or redis.call('hexists', KEYS[1], ARGV[1]) == 1 " +
"then " +
"   redis.call('hincrby', KEYS[1], ARGV[1], 1) " +
"   redis.call('expire', KEYS[1], ARGV[2]) " +
"   return 1 " +
"else " +
"   return 0 " +
"end"
  • **解锁的lua脚本:**使用hexists指令判断是否存在锁,如果为0,代表没有对应field字段的锁,直接返回nil;如果使用hincrby指令使锁field字段锁的值减少1之后值为0,代表锁已经不在占用,可以删除该锁;否则直接返回0,代表是可重入锁,锁还没有释放。
Lua 复制代码
if redis.call('hexists', KEYS[1], ARGV[1]) == 0 " +
"then " +
"   return nil " +
"elseif redis.call('hincrby', KEYS[1], ARGV[1], -1) == 0 " +
"then " +
"   return redis.call('del', KEYS[1]) " +
"else " +
"   return 0 " +
"end"
  • **实现续期的lua脚本:**使用hexists指令判断锁的field值是否存在,如果值为1存在,则将该锁的过期时间更新,否则直接返回0,代表没有找到该锁,续期失败。
Lua 复制代码
if redis.call('hexists', KEYS[1], ARGV[1]) == 1 " +
"then " +
"   return redis.call('expire', KEYS[1], ARGV[2]) " +
"else " +
"   return 0 " +
"end";
  • 创建一个自定义的锁工具类MyRedisDistributeLock,实现加锁、解锁、续期功能
  • MyRedisDistributeLock实现
Lua 复制代码
package com.ht.atp.plat.util;

import org.jetbrains.annotations.NotNull;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.util.Arrays;
import java.util.Timer;
import java.util.TimerTask;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;


public class MyRedisDistributeLock implements Lock {

    public MyRedisDistributeLock(StringRedisTemplate redisTemplate, String lockName, long expire) {
        this.redisTemplate = redisTemplate;
        this.lockName = lockName;
        this.expire = expire;
        this.uuid = getId();
    }

    /**
     * redis工具类
     */
    private StringRedisTemplate redisTemplate;


    /**
     * 锁名称
     */
    private String lockName;

    /**
     * 过期时间
     */
    private Long expire;

    /**
     * 锁的值
     */
    private String uuid;

    @Override
    public void lock() {
        this.tryLock();
    }

    @Override
    public void lockInterruptibly() {

    }

    @Override
    public boolean tryLock() {
        try {
            return this.tryLock(-1L, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return false;
    }

    @Override
    public boolean tryLock(long time, @NotNull TimeUnit unit) throws InterruptedException {
        if (time != -1) {
            this.expire = unit.toSeconds(time);
        }
        String script = "if redis.call('exists', KEYS[1]) == 0 or redis.call('hexists', KEYS[1], ARGV[1]) == 1 " +
                "then " +
                "   redis.call('hincrby', KEYS[1], ARGV[1], 1) " +
                "   redis.call('expire', KEYS[1], ARGV[2]) " +
                "   return 1 " +
                "else " +
                "   return 0 " +
                "end";
        while (!this.redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuid, String.valueOf(expire))) {
            Thread.sleep(50);
        }
//        //加锁成功后,自动续期
        this.renewExpire();
        return true;
    }

    @Override
    public void unlock() {
        String script = "if redis.call('hexists', KEYS[1], ARGV[1]) == 0 " +
                "then " +
                "   return nil " +
                "elseif redis.call('hincrby', KEYS[1], ARGV[1], -1) == 0 " +
                "then " +
                "   return redis.call('del', KEYS[1]) " +
                "else " +
                "   return 0 " +
                "end";
        Long flag = this.redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuid);
        if (flag == null) {
            throw new IllegalMonitorStateException("this lock doesn't belong to you!");
        }
    }

    @NotNull
    @Override
    public Condition newCondition() {
        return null;
    }

    /**
     * 给线程拼接唯一标识
     *
     * @return
     */
    private String getId() {
        return UUID.randomUUID() + "-" + Thread.currentThread().getId();
    }


    private void renewExpire() {
        String script = "if redis.call('hexists', KEYS[1], ARGV[1]) == 1 " +
                "then " +
                "   return redis.call('expire', KEYS[1], ARGV[2]) " +
                "else " +
                "   return 0 " +
                "end";
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("-------------------");
                Boolean flag = redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuid, String.valueOf(expire));
                if (flag) {
                    renewExpire();
                }
            }
        }, this.expire * 1000 / 3);
    }
}
  • 实现加锁功能
  • 实现解锁功能
  • 使用Timer实现锁的续期功能
  • 使用MyRedisDistributeLock实现库存的加锁业务
  • 使用自定义MyRedisDistributeLock工具类实现加锁业务
Lua 复制代码
public void checkAndReduceStock() {
        //1.获取锁
        MyRedisDistributeLock myRedisDistributeLock = new MyRedisDistributeLock(stringRedisTemplate, "stock", 10);
        myRedisDistributeLock.lock();

        try {
            // 2. 查询库存数量
            String stockQuantity = stringRedisTemplate.opsForValue().get("P0001");
            // 3. 判断库存是否充足
            if (stockQuantity != null && stockQuantity.length() != 0) {
                Integer quantity = Integer.valueOf(stockQuantity);
                if (quantity > 0) {
                    // 4.扣减库存
                    stringRedisTemplate.opsForValue().set("P0001", String.valueOf(--quantity));
                }
            } else {
                System.out.println("该库存不存在!");
            }
        } finally {
            myRedisDistributeLock.unlock();
        }
    }
  • 启动服务7000、7001、7002,压测优化后的自定义分布式锁:平均访问时间362ms,吞吐量每秒246,库存扣减为0,表明优化后的分布式锁是可用的。
  • 集成redisson工具包,使用第三方工具包实现分布式锁,完成并发访问"超卖"问题案例演示
Lua 复制代码
<dependency>
	<groupId>org.redisson</groupId>
	<artifactId>redisson-spring-boot-starter</artifactId>
	<version>3.11.6</version>
</dependency>
  • 创建一个redisson配置类,引入redisson客户端工具
Lua 复制代码
package com.ht.atp.plat.config;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


@Configuration
public class MyRedissonConfig {

    @Bean
    RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer()
                .setAddress("redis://192.168.110.88:6379");
        //配置看门狗的默认超时时间为30s,供续期使用
        config.setLockWatchdogTimeout(30000);
        return Redisson.create(config);
    }
}
  • 使用Redisson锁 实现"超卖"业务方法
Lua 复制代码
//可重入锁
    @Override
    public void checkAndReduceStock() {
        // 1.加锁,获取锁失败重试
        RLock lock = this.redissonClient.getLock("lock");
        lock.lock();

        try {
            // 2. 查询库存数量
            String stockQuantity = stringRedisTemplate.opsForValue().get("P0001");
            // 3. 判断库存是否充足
            if (stockQuantity != null && stockQuantity.length() != 0) {
                Integer quantity = Integer.valueOf(stockQuantity);
                if (quantity > 0) {
                    // 4.扣减库存
                    stringRedisTemplate.opsForValue().set("P0001", String.valueOf(--quantity));
                }
            } else {
                System.out.println("该库存不存在!");
            }
        } finally {
            // 4.释放锁
            lock.unlock();
        }
    }
  • 开启7000、7001、7002服务,压测扣减库存接口
  • 压测结果:平均访问时间222ms,吞吐量为384每秒
  • 库存扣减结果为0

结语

综上所述,无论是自定义分布式锁还是使用redisson工具类,都能实现分布式锁解决并发访问的"超卖问题",redisson工具使用集成更加方便简洁,推荐使用redisson工具包。本节内容到这里就结束了,我们下期见。。。。。。

相关推荐
懒虫虫~12 分钟前
基于SpringBoot解决RabbitMQ消息丢失问题
spring boot·rabbitmq
java干货1 小时前
深度解析:Spring Boot 配置加载顺序、优先级与 bootstrap 上下文
前端·spring boot·bootstrap
sclibingqing2 小时前
SpringBoot项目接口集中测试方法及实现
java·spring boot·后端
KK溜了溜了4 小时前
JAVA-springboot log日志
java·spring boot·logback
我命由我123455 小时前
Spring Boot 项目集成 Redis 问题:RedisTemplate 多余空格问题
java·开发语言·spring boot·redis·后端·java-ee·intellij-idea
面朝大海,春不暖,花不开5 小时前
Spring Boot消息系统开发指南
java·spring boot·后端
hshpy5 小时前
setting up Activiti BPMN Workflow Engine with Spring Boot
数据库·spring boot·后端
jay神5 小时前
基于Springboot的宠物领养系统
java·spring boot·后端·宠物·软件设计与开发
不知几秋6 小时前
Spring Boot
java·前端·spring boot
howard20057 小时前
5.4.2 Spring Boot整合Redis
spring boot·整合redis