Redis 实现库存扣减

news2024/12/23 0:00:55

在日常开发中有很多地方都有类似扣减库存的操作,比如电商系统中的商品库存,抽奖系统中的奖品库存等。这次分享的是库存的操作怎么防止商品被超卖。

解决方案

  • 基于数据库单库存
  • 基于数据库多库存
  • 基于redis

解决思路

  1. 使用mysql数据库,使用一个字段来存储库存,每次扣减库存去更新这个字段。
  2. 还是使用数据库,但是将库存分层多份存到多条记录里面,扣减库存的时候路由一下,这样子增大了并发量,但是还是避免不了大量的去访问数据库来更新库存。
  3. 将库存放到redis使用redis的incrby特性来扣减库存。

基于数据库单库存

第一种方式在所有请求都会在这里等待锁,获取锁有去扣减库存。在并发量不高的情况下可以使用,但是一旦并发量大了就会有大量请求阻塞在这里,导致请求超时,进而整个系统雪崩;而且会频繁的去访问数据库,大量占用数据库资源,所以在并发高的情况下这种方式不适用。

基于数据库多库存

第二种方式其实是第一种方式的优化版本,在一定程度上提高了并发量,但是在还是会大量的对数据库做更新操作大量占用数据库资源。

基于数据库来实现扣减库存还存在的一些问题:

  • 用数据库扣减库存的方式,扣减库存的操作必须在一条语句中执行,不能先selec在update,这样在并发下会出现超扣的情况。如:
update number set x=x-1 where x > 0
  • MySQL自身对于高并发的处理性能就会出现问题,一般来说,MySQL的处理性能会随着并发thread上升而上升,但是到了一定的并发度之后会出现明显的拐点,之后一路下降,最终甚至会比单thread的性能还要差。
  • 当减库存和高并发碰到一起的时候,由于操作的库存数目在同一行,就会出现争抢InnoDB行锁的问题,导致出现互相等待甚至死锁,从而大大降低MySQL的处理性能,最终导致前端页面出现超时异常。

基于redis

针对上述问题的问题我们就有了第三种方案,将库存放到缓存,利用redis的incrby特性来扣减库存,解决了超扣和性能问题。但是一旦缓存丢失需要考虑恢复方案。比如抽奖系统扣奖品库存的时候,初始库存=总的库存数-已经发放的奖励数,但是如果是异步发奖,需要等到MQ消息消费完了才能重启redis初始化库存,否则也存在库存不一致的问题。
 

基于redis实现扣减库存的具体实现

  1. 使用redis的lua脚本来实现扣减库存
  2. 如果是分布式环境下所以还需要一个分布式锁来控制同时只有一个服务去初始化库存
  3. 需要提供一个回调函数,在初始化库存的时候去调用这个函数获取初始化库存
/**
 * 获取库存回调
 * @author 
 */
public interface IStockCallback {

 /**
  * 获取库存
  * @return
  */
 int getStock();
}
扣减库存服务(StockService)
/**
 * 扣库存
 *
 * @author bjzhangzhenzhen
 */
@Service
public class StockService {
    Logger logger = LoggerFactory.getLogger(StockService.class);

    /**
     * 不限库存
     */
    public static final long UNINITIALIZED_STOCK = -3L;

    /**
     * Redis 客户端
     */
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 执行扣库存的脚本
     */
    public static final String STOCK_LUA;

    static {
        /**
         *
         * @desc 扣减库存Lua脚本
         * 库存(stock)-1:表示不限库存
         * 库存(stock)0:表示没有库存
         * 库存(stock)大于0:表示剩余库存
         *
         * @params 库存key
         * @return
         *   -3:库存未初始化
         *   -2:库存不足
         *   -1:不限库存
         *   大于等于0:剩余库存(扣减之后剩余的库存)
         *      redis缓存的库存(value)是-1表示不限库存,直接返回1
         */
        StringBuilder sb = new StringBuilder();
        sb.append("if (redis.call('exists', KEYS[1]) == 1) then");
        sb.append("    local stock = tonumber(redis.call('get', KEYS[1]));");
        sb.append("    local num = tonumber(ARGV[1]);");
        sb.append("    if (stock == -1) then");
        sb.append("        return -1;");
        sb.append("    end;");
        sb.append("    if (stock >= num) then");
        sb.append("        return redis.call('incrby', KEYS[1], 0 - num);");
        sb.append("    end;");
        sb.append("    return -2;");
        sb.append("end;");
        sb.append("return -3;");
        STOCK_LUA = sb.toString();
    }

    /**
     * @param key           库存key
     * @param expire        库存有效时间,单位秒
     * @param num           扣减数量
     * @param stockCallback 初始化库存回调函数
     * @return -2:库存不足; -1:不限库存; 大于等于0:扣减库存之后的剩余库存
     */
    public long stock(String key, long expire, int num, IStockCallback stockCallback) {
        long stock = stock(key, num);
        // 初始化库存
        if (stock == UNINITIALIZED_STOCK) {
            RedisLock redisLock = new RedisLock(redisTemplate, key);
            try {
                // 获取锁
                if (redisLock.tryLock()) {
                    // 双重验证,避免并发时重复回源到数据库
                    stock = stock(key, num);
                    if (stock == UNINITIALIZED_STOCK) {
                        // 获取初始化库存
                        final int initStock = stockCallback.getStock();
                        // 将库存设置到redis
                        redisTemplate.opsForValue().set(key, initStock, expire, TimeUnit.SECONDS);
                        // 调一次扣库存的操作
                        stock = stock(key, num);
                    }
                }
            } catch (Exception e) {
                logger.error(e.getMessage(), e);
            } finally {
                redisLock.unlock();
            }

        }
        return stock;
    }

    /**
     * 加库存(还原库存)
     *
     * @param key    库存key
     * @param num    库存数量
     * @return
     */
    public long addStock(String key, int num) {

        return addStock(key, null, num);
    }

    /**
     * 加库存
     *
     * @param key    库存key
     * @param expire 过期时间(秒)
     * @param num    库存数量
     * @return
     */
    public long addStock(String key, Long expire, int num) {
        boolean hasKey = redisTemplate.hasKey(key);
        // 判断key是否存在,存在就直接更新
        if (hasKey) {
            return redisTemplate.opsForValue().increment(key, num);
        }

        Assert.notNull(expire,"初始化库存失败,库存过期时间不能为null");
        RedisLock redisLock = new RedisLock(redisTemplate, key);
        try {
            if (redisLock.tryLock()) {
                // 获取到锁后再次判断一下是否有key
                hasKey = redisTemplate.hasKey(key);
                if (!hasKey) {
                    // 初始化库存
                    redisTemplate.opsForValue().set(key, num, expire, TimeUnit.SECONDS);
                }
            }
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        } finally {
            redisLock.unlock();
        }

        return num;
    }

    /**
     * 获取库存
     *
     * @param key 库存key
     * @return -1:不限库存; 大于等于0:剩余库存
     */
    public int getStock(String key) {
        Integer stock = (Integer) redisTemplate.opsForValue().get(key);
        return stock == null ? -1 : stock;
    }

    /**
     * 扣库存
     *
     * @param key 库存key
     * @param num 扣减库存数量
     * @return 扣减之后剩余的库存【-3:库存未初始化; -2:库存不足; -1:不限库存; 大于等于0:扣减库存之后的剩余库存】
     */
    private Long stock(String key, int num) {
        // 脚本里的KEYS参数
        List<String> keys = new ArrayList<>();
        keys.add(key);
        // 脚本里的ARGV参数
        List<String> args = new ArrayList<>();
        args.add(Integer.toString(num));

        long result = redisTemplate.execute(new RedisCallback<Long>() {
            @Override
            public Long doInRedis(RedisConnection connection) throws DataAccessException {
                Object nativeConnection = connection.getNativeConnection();
                // 集群模式和单机模式虽然执行脚本的方法一样,但是没有共同的接口,所以只能分开执行
                // 集群模式
                if (nativeConnection instanceof JedisCluster) {
                    return (Long) ((JedisCluster) nativeConnection).eval(STOCK_LUA, keys, args);
                }

                // 单机模式
                else if (nativeConnection instanceof Jedis) {
                    return (Long) ((Jedis) nativeConnection).eval(STOCK_LUA, keys, args);
                }
                return UNINITIALIZED_STOCK;
            }
        });
        return result;
    }

}
调用
/**
 * @author bjzhangzhenzhen
 */
@RestController
public class StockController {

    @Autowired
    private StockService stockService;

    @RequestMapping(value = "stock", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    public Object stock() {
        // 商品ID
        long commodityId = 1;
        // 库存ID
        String redisKey = "redis_key:stock:" + commodityId;
        long stock = stockService.stock(redisKey, 60 * 60, 2, () -> initStock(commodityId));
        return stock >= 0;
    }

    /**
     * 获取初始的库存
     *
     * @return
     */
    private int initStock(long commodityId) {
        // TODO 这里做一些初始化库存的操作
        return 1000;
    }

    @RequestMapping(value = "getStock", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    public Object getStock() {
        // 商品ID
        long commodityId = 1;
        // 库存ID
        String redisKey = "redis_key:stock:" + commodityId;

        return stockService.getStock(redisKey);
    }

    @RequestMapping(value = "addStock", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    public Object addStock() {
        // 商品ID
        long commodityId = 2;
        // 库存ID
        String redisKey = "redis_key:stock:" + commodityId;

        return stockService.addStock(redisKey, 2);
    }
}

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/687181.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[架构之路-216]- 架构 - 概念架构 - 模块(Module)、组件(Component)、包(Package)、对象、函数的区别

前言&#xff1a; 在软件架构中&#xff0c;一个重要的任务就是切分系统&#xff0c;而切分系统进程涉及到一个基本的概念&#xff0c;如模块&#xff08;Module&#xff09;、组件&#xff08;Component&#xff09;、包&#xff08;Package&#xff09;、对象&#xff0c;本…

亚马逊云科技以用户为中心,持续丰富安全服务和解决方案

AI加持安全&#xff0c;自动化运营成未来趋势 亚马逊云科技始终在云安全领域不断创新探索、深耕发力&#xff0c;随着全球技术的发展而持续迭代安全能力。 当下&#xff0c;以ChatGPT为代表的AIGC成为最出圈的热点&#xff0c;大量的科技巨头纷纷涌入AI赛道&#xff0c;投入了…

16. WebGPU 数据内存布局

在 WebGPU 中&#xff0c;提供给它的几乎所有数据都需要在内存中设定布局&#xff0c;以匹配在着色器中定义的内容。这与 JavaScript 和 TypeScript 形成鲜明对比&#xff0c;后者很少出现内存布局问题。 在 WGSL 中&#xff0c;当编写着色器时&#xff0c;通常定义 struct 。…

Redis集群详解

目录 一、前言二、Redis 集群方案应该怎么做&#xff1f;都有哪些方案&#xff1f;三、twemproxy四、分而治之-codis1、codis简介2、Codis 分片原理3、不同的 Codis 实例之间槽位关系如何同步&#xff1f;4、假如Redis扩容&#xff0c;如何调整槽位的&#xff1f;5、codis优缺点…

巧用 overflow-scroll 实现丝滑轮播图

前言: 近期我在项目中就接到了一个完成轮播图组件的需求。最开始我也像大家一样&#xff0c;直接选择使用了知名的开源项目 “Swiper”&#xff0c;但是后来发现它在移动端项目中某些测试环境下会白屏一段时间。无论如何调试都不能修复这个问题&#xff0c;于是就自己上手写了个…

Java智慧工厂UWB高精度人员定位管理系统源码

一、系统概述&#xff1a; 智慧工厂高精度定位管理系统源码&#xff0c;采用UWB定位技术&#xff0c;通过部署UWB定位设备实现人、车、物精确定位&#xff0c;打造可寻、可视、可防、可控的一体化管控平台。UWB定位系统具有容量大、稳定性强、精度高、安装便捷、易维护、操作简…

pebblely无需代码连接集简云数据表的方法

1 使用场景 当电商UI接到一个设计产品图的需求时&#xff0c;按照常规的工作安排&#xff0c;从对接需求到最后完成效果图最短时间都要在5天左右&#xff0c;如果遇到高要求的客户&#xff0c;后期还需要在电脑上进一步调整细节&#xff0c;一张成片起码要花上数小时时间去完成…

LabVIEW开发压电陶瓷特性测试系统

LabVIEW开发压电陶瓷特性测试系统 目前&#xff0c;随着科学技术的飞速发展&#xff0c;各个领域都需要纳米级的精密定位系统。而压电定位系统是迄今为止已经得到认可的微纳定位系统。该系统主要由压电驱动系统、位移测量传感器和控制系统组成。其中&#xff0c;整个压电驱动系…

oracle 使用sql语句实现交换一个表中两条记录的某个字段的内容

不要太忙了 好好珍惜眼下 的真爱 &#xff08;马晓静sorry~&#xff09; 表 test --------------- id | i_order --------------- 1 | 22 2 | 23 --------------- 转换后结果为&#xff1a; --------------- id | i_order --------------- 1 | 23 2 | …

【云原生丶Docker】Docker镜像常用命令大全

镜像是Docker中最为核心也是最具创造性的概念&#xff01;在理解Docker的核心概念文章中&#xff0c;我们理解了镜像的含义&#xff0c;下面让我们一起操作一些镜像&#xff0c;包括拉取、推送镜像等操作。 1、镜像支持哪些命令 通过 docker image help 查看 Docker 支持的镜像…

解决nacos频繁输出get changegroupkeys日志

# 1、根据心跳日志&#xff0c;定位日志输出的包名 c.a.n.client.config.impl.ClientWorker : get changedGroupKeys:[]# 2、在 IDEA 中搜索 ClientWorker 所在的包路径 package com.alibaba.nacos.client.config.impl;# 3、在任意格式的配置文件中将该包路径的日志设置为…

Unity 工具 之 Azure OpenAI 功能接入到Unity 中的简单整理

Unity 工具 之 Azure OpenAI 功能接入到Unity 中的简单整理 目录 Unity 工具 之 Azure OpenAI 功能接入到Unity 中的简单整理 一、简单介绍 二、实现原理 三、注意实现 四、简单实现步骤 五、关键代码 六、附加 创建新的 .NET Core &#xff0c;获取 Azure.AI.OpenAI d…

公司个人年终工作总结【10篇】

公司个人年终工作总结1 20__年即将过去&#xff0c;在公司领导的悉心关怀下和同事们的帮助指导下&#xff0c;结合我自身的努力&#xff0c;在工作、学习等各方面都取得了长足的进步&#xff0c;尤其是在保险理赔专业知识和技能培养方面的成熟&#xff0c;使我成为一名合格的车…

Linux_查看硬盘占用情况

一、查看是什么占用了硬盘空间 df -h 这个命令查看的是显示目前在 Linux 系统上的所有文件系统磁盘使用情况&#xff0c;并根据大小适当显示&#xff08;-h 参数代表以可读的方式展示文件的大小&#xff09;。 一下为加 -h 和不加 -h 的结果 不加 -h 加 -h Filesystem&…

探秘安卓广播:揭秘Android广播机制的神奇之处

壹 广播机制 Android中的广播(Broadcast)机制用于进程/线程间通信&#xff0c;该机制使用了观察者模式。观察者模式是一种软件设计模式&#xff0c;该模式是基于消息的发布/订阅事件模型&#xff0c;该模型中的消息发布者是广播机制中的广播发送者&#xff0c;消息订阅者是广播…

ajax原理是什么?如何实现?

一、是什么 AJAX全称(Async Javascript and XML) 即异步的JavaScript 和XML&#xff0c;是一种创建交互式网页应用的网页开发技术&#xff0c;可以在不重新加载整个网页的情况下&#xff0c;与服务器交换数据&#xff0c;并且更新部分网页 Ajax的原理简单来说通过XmlHttpReq…

算法——查找表

查找&#xff0c;根据一个值查找另一个值&#xff0c;value值可以是容器&#xff0c;结构&#xff0c;这样可查找的元素就更多&#xff1b; 哈希冲突&#xff1a; 主关键字&#xff1a;可以唯一的标识一个记录的关键字&#xff0c;如准考证号&#xff1b; 此关键词&#xff…

RFID微图柜的操作流程以及功能介绍

微型图书馆智能书柜是一种基于 RFID 技术的图书自助借阅延伸服务的终端设备&#xff0c;可实现 24 小时无人看守的共享的智能化设备&#xff0c;可自助借还书、自动数据记录分析、自助提醒等一体化管理服务功能。 智能书柜的操作流程 借书&#xff1a;在操作页面上选择“借书…

【好书精读】网络是怎样连接的 —— 信号在网线和集线器中传输

&#xff08; 该图由我使用 AI 绘制 &#xff09; 目录 每个包都是独立传输的 防止网线中的信号衰减很重要 “双绞”是为了抑制噪声 集线器将信号发往所有线路 每个包都是独立传输的 从计算机发送出来的网络包会通过集线器 、 路由器等设备被转发 &#xff0c; 最 终到达…

解决 CentOS/Alma 安装 libpcap-devel 报错:No match for argument: libpcap-devel

环境&#xff1a;Alma 8.5、Centos 7.x 解决方案 Linux 安装软件的时候&#xff0c;需要 libpcap-devel 这个组件&#xff0c;执行命令&#xff1a;yum install libpcap-devel &#xff0c;然后报错如下&#xff1a; Last metadata expiration check: 0:05:24 ago on Mon 12…