(四)库存超卖案例实战——优化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工具包。本节内容到这里就结束了,我们下期见。。。。。。

相关推荐
往事随风去16 分钟前
虚拟线程在Spring Boot中的正确使用方式
spring boot
麦兜*26 分钟前
Spring Boot 应用 Docker 监控:Prometheus + Grafana 全方位监控
spring boot·后端·spring cloud·docker·prometheus
L.EscaRC1 小时前
Redisson在Spring Boot中的高并发应用解析
java·spring boot·后端
Naylor2 小时前
玩转kafka
spring boot·kafka
摇滚侠2 小时前
Spring Boot3零基础教程,StreamAPI 介绍,笔记98
java·spring boot·笔记
摇滚侠2 小时前
Spring Boot3零基础教程,StreamAPI 的基本用法,笔记99
java·spring boot·笔记
codingPower3 小时前
升级mybatis-plus导致项目启动报错: net.sf.jsqlparser.statement.select.SelectBody
java·spring boot·maven·mybatis
刘一说4 小时前
深入理解 Spring Boot Web 开发中的全局异常统一处理机制
前端·spring boot·后端
智_永无止境4 小时前
Spring Boot全局异常处理指南
java·spring boot
屹奕5 小时前
基于EasyExcel实现Excel导出功能
java·开发语言·spring boot·excel