百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

SpringBoot+Redis的lua脚本实现高性能秒杀下单需求

mhr18 2024-11-05 10:26 36 浏览 0 评论

秒杀抢购可以说是在分布式环境下?个?常经典的案例。

  1. ?并发时间极短、瞬间?户量?,?瞬间的?QPS把系统或数据库直接打挂,响应失败,导致与这个系统耦合的系统出问题。
  2. 超卖: 你只有?百件商品,由于是?并发的问题,导致超卖的情况。

目前秒杀的实现方案主要有两种:

  1. 用redis 将抢购信息进行存储。然后再慢慢消费。 同时,服务器给与用户快速响应。
  2. 用mq实现,比如RabbitMQ,服务器将请求过来的数据先让RabbitMQ存起来,然后再异步消费。

也可以结合redis与mq的方式,通过redis控制剩余库存,达到快速响应,将满足条件的购买的订单先让RabbitMQ存起来,后续进行异步消费。

整体流程

  1. 服务器接收到了大量用户请求过来(1s 2000个请求)。比如传了用户信息,产品信息,和购买数量信息。此时 服务器采用redis 的lua 脚本 去调用redis 中间件。lua 脚本的逻辑是减库存,校验库存是否足够。然后迅速给与服务器反馈(库存是否够,够返回 1 ,不够返回 0)。
  2. 服务器迅速给与用户的请求反馈。提示抢购成功.或者抢购失败。
  3. 抢购成功将订单信息放入MQ,其余线程接收到MQ的信息后,将订单信息存入DB中 。
  4. 后面客户就可以查询 mysql 的订单信息了。

代码展示

架构采用springboot+redis+mysql+myBatis.

商品表:

CREATE TABLE `tb_product` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `product_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'id',
   `price` decimal(65,18) NOT NULL DEFAULT '0',
   `available_qty` bigint NOT NULL DEFAULT '0' COMMENT '数量',
  `title` varchar(1024) NOT NULL DEFAULT '',
   `end_time` bigint NOT NULL DEFAULT '0',
  `start_time` bigint NOT NULL DEFAULT '0',
  `created` bigint NOT NULL DEFAULT '0',
  `updated` bigint NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uq_product_id` (`product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

pom.xml依赖:

<dependency>
  <groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

lua 脚本:

//减少库存,校验库存是否充足

local counter = redis.call('hget',KEYS[1],KEYS[2]);
if not counter then
	return -1;
end;

local result = counter - ARGV[1];
if(result>=0 ) then
	redis .cal1('hset', KEYS[ 1],KEYS[2],result);
	return 1;
end;
return 0;
// 库存数量回滚

local counter = redis.call( 'hget',KEYS[1],KEYS[2]);
if not counter then
	return -1;
end;
redis .call( 'hincrby', KEYS[ 1], KEYS[2],ARGV[ 1]);
return 1;

核心业务代码展示

  • 加载lua脚本
private final static DefaultRedisScript<Long> deductRedisScript = new DefaultRedisScript();
private final static DefaultRedisScript<Long> increaseRedisScript = new DefaultRedisScript();   
//加载lua脚本
@PostConstruct
void init() {
  //加载削减库存lua脚本
  deductRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("redis/fixedDeductInventory.lua")));
  deductRedisScript.setResultType(Long.class);
  //加载库存回滚lua脚本
  increaseRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("redis/fixedIncreaseInventory.lua")));
  increaseRedisScript.setResultType(Long.class);
}
  • 添加库存到redis

**注意点:**在使用redis集群时,lua脚本中存在多个key时,可以通过hash tag这个方法将不同key的值落在同一个槽位上,hash tag 是通过{}这对括号括起来的字符串,如果下列中{fixed:" + data.getProductId() + "} 作为tag,确保同一个产品的信息都在同一个槽位。

@Resource(name = "fixedCacheRedisTemplate")
private RedisTemplate<String, Long> fixedCacheRedisTemplate;

public void ProductToOngoing(Product data, Long time) {
	//设置数量
  long number = data.getAvailableQty();
  fixedCacheRedisTemplate.opsForHash().putIfAbsent("{fixed:" + data.getProductId() + "}-residue_stock_" + data.getRecordId(),
                                                   "{fixed:" + data.getProductId() + "}-residueStock" , number);
  String statusKey = "fixed_product_sold_status_"+ data.getRecordId();
  long timeout = data.getEndTime() - data.getStartTime();
  //添加产品出售状态
  fixedCacheRedisTemplate.opsForValue().set(statusKey, 1L, data.getEndTime() - data.getStartTime(), TimeUnit.MILLISECONDS);
}
  • 下单&库存校验
//检查库存
public boolean checkFixedOrderQty(Long userId, Long productId, Long quantity, Long overTime) {
  Boolean pendingOrder = false;
  String userKey = "";
  try {
    //校验是否开始
    String statusKey = "fixed_product_sold_status_" + productId;
    Long fixedStartStatus = fixedCacheRedisTemplate.opsForValue().get(statusKey);
    if (fixedStartStatus == null || fixedStartStatus != 1L) {
      //报错返回,商品未开售
      throw new WebException(ResultCode.SALE_HAS_NOT_START);
    }
    //检查库存数量
    Long number = deductInventory(productId, quantity);
    if (number != 1L) {
      log.warn("availbale num is null:{} {}", productId, number);
      throw new WebException(ResultCode.AVAILABLE_AMOUNT_INSUFFICIENT);
    }
    return true;
  } catch (Exception e) {
    log.warn("checkFixedOrderQty error:{}", e.getMessage(), e);
    throw e;
  }
}
//下单
 public void createOrder(Long userId, Long productId, BigDecimal price, Long quantity){
   boolean check = checkFixedOrderQty(userId, productId, quantity);
   try {
     if (check) {
       //添加MQ等待下单,后续收到推送的线程保存靠DB中
       CreateCoinOrderData data = new CreateCoinOrderData();
       data.setUserId(userId);
       data.setProductId(productId);
       data.setPrice(price);
       data.setQuantity(quantity);
       rabbitmqProducer.sendMessage(1, JSONObject.toJSONString(data));
     }
   } catch (Exception e) {
     //发生异常,库存需要回滚
     increaseInventory(recordId, quantity, 1L);
     throw e;
   }
 }
	//库存回填
public Long increaseInventory(Long productId, Long num) {
  try {
    // 构建keys信息,代表hash值中所需要的key信息
    List<String> keys = Arrays.asList("{fixed:" + productId + "}-residue_stock_"+ recordId, "{fixed:" + productId + "}-residueStock");
    // 执行脚本
    Object result = fixedCacheRedisTemplate.execute(increaseRedisScript, keys, num);
    log.info("increaseInventory productId :{} num:{}  result:{}", productId, num, result);
    return (Long) result;
  } catch (Exception e) {
    log.warn("increaseInventory error productId:{}  num:{}", productId, num);
  }
  return 0L;

}

来源:https://juejin.cn/post/7286264172097781812

相关推荐

【推荐】一个开源免费、AI 驱动的智能数据管理系统,支持多数据库

如果您对源码&技术感兴趣,请点赞+收藏+转发+关注,大家的支持是我分享最大的动力!!!.前言在当今数据驱动的时代,高效、智能地管理数据已成为企业和个人不可或缺的能力。为了满足这一需求,我们推出了这款开...

Pure Storage推出统一数据管理云平台及新闪存阵列

PureStorage公司今日推出企业数据云(EnterpriseDataCloud),称其为组织在混合环境中存储、管理和使用数据方式的全面架构升级。该公司表示,EDC使组织能够在本地、云端和混...

对Java学习的10条建议(对java课程的建议)

不少Java的初学者一开始都是信心满满准备迎接挑战,但是经过一段时间的学习之后,多少都会碰到各种挫败,以下北风网就总结一些对于初学者非常有用的建议,希望能够给他们解决现实中的问题。Java编程的准备:...

SQLShift 重大更新:Oracle→PostgreSQL 存储过程转换功能上线!

官网:https://sqlshift.cn/6月,SQLShift迎来重大版本更新!作为国内首个支持Oracle->OceanBase存储过程智能转换的工具,SQLShift在过去一...

JDK21有没有什么稳定、简单又强势的特性?

佳未阿里云开发者2025年03月05日08:30浙江阿里妹导读这篇文章主要介绍了Java虚拟线程的发展及其在AJDK中的实现和优化。阅前声明:本文介绍的内容基于AJDK21.0.5[1]以及以上...

「松勤软件测试」网站总出现404 bug?总结8个原因,不信解决不了

在进行网站测试的时候,有没有碰到过网站崩溃,打不开,出现404错误等各种现象,如果你碰到了,那么恭喜你,你的网站出问题了,是什么原因导致网站出问题呢,根据松勤软件测试的总结如下:01数据库中的表空间不...

Java面试题及答案最全总结(2025版)

大家好,我是Java面试陪考员最近很多小伙伴在忙着找工作,给大家整理了一份非常全面的Java面试题及答案。涉及的内容非常全面,包含:Spring、MySQL、JVM、Redis、Linux、Sprin...

数据库日常运维工作内容(数据库日常运维 工作内容)

#数据库日常运维工作包括哪些内容?#数据库日常运维工作是一个涵盖多个层面的综合性任务,以下是详细的分类和内容说明:一、数据库运维核心工作监控与告警性能监控:实时监控CPU、内存、I/O、连接数、锁等待...

分布式之系统底层原理(上)(底层分布式技术)

作者:allanpan,腾讯IEG高级后台工程师导言分布式事务是分布式系统必不可少的组成部分,基本上只要实现一个分布式系统就逃不开对分布式事务的支持。本文从分布式事务这个概念切入,尝试对分布式事务...

oracle 死锁了怎么办?kill 进程 直接上干货

1、查看死锁是否存在selectusername,lockwait,status,machine,programfromv$sessionwheresidin(selectsession...

SpringBoot 各种分页查询方式详解(全网最全)

一、分页查询基础概念与原理1.1什么是分页查询分页查询是指将大量数据分割成多个小块(页)进行展示的技术,它是现代Web应用中必不可少的功能。想象一下你去图书馆找书,如果所有书都堆在一张桌子上,你很难...

《战场兄弟》全事件攻略 一般事件合同事件红装及隐藏职业攻略

《战场兄弟》全事件攻略,一般事件合同事件红装及隐藏职业攻略。《战场兄弟》事件奖励,事件条件。《战场兄弟》是OverhypeStudios制作发行的一款由xcom和桌游为灵感来源,以中世纪、低魔奇幻为...

LoadRunner(loadrunner录制不到脚本)

一、核心组件与工作流程LoadRunner性能测试工具-并发测试-正版软件下载-使用教程-价格-官方代理商的架构围绕三大核心组件构建,形成完整测试闭环:VirtualUserGenerator(...

Redis数据类型介绍(redis 数据类型)

介绍Redis支持五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)及Zset(sortedset:有序集合)。1、字符串类型概述1.1、数据类型Redis支持...

RMAN备份监控及优化总结(rman备份原理)

今天主要介绍一下如何对RMAN备份监控及优化,这里就不讲rman备份的一些原理了,仅供参考。一、监控RMAN备份1、确定备份源与备份设备的最大速度从磁盘读的速度和磁带写的带度、备份的速度不可能超出这两...

取消回复欢迎 发表评论: