【RocketMQ】秒杀设计与实现

🎯 导读:本文档详细探讨了高并发场景下的秒杀系统设计与优化策略,特别是如何在短时间内处理大量请求。文档分析了系统性能指标如QPS(每秒查询率)和TPS(每秒事务数),并通过实例讲解了如何使用JMeter进行性能测试。此外,文档提供了技术选型指南,包括SpringBoot、Redis、RocketMQ等技术的应用,并给出了具体的用户量评估和服务器配置建议。最后,通过分析不同的库存扣减与订单创建实现方式,提出了使用Redis分布式锁等技术提高并发性能的方法。

文章目录

  • 秒杀
    • 介绍
    • 性能指标
      • QPS
      • TPS
      • 怎么优化接口性能
    • 技术选型
    • 用户量评估
    • 技术要点
    • 架构图
    • 数据库
    • 创建项目选择依赖seckill-web(接受用户秒杀请求)
      • pom.xml
      • 修改配置文件
      • 创建SeckillController
    • 创建项目选择依赖seckill-service(处理秒杀)
      • 修改yml文件
      • 逆向生成实体类
      • 修改启动类
      • 修改GoodsMapper
      • 修改GoodsMapper.xml
      • 同步MySQL数据到redis
        • 方法1
        • 方法2
      • 秒杀业务监听器
      • 修改GoodsService
      • 修改GoodsServiceImpl

秒杀

介绍

秒杀:很短的时间内,要处理大量的请求

【高并发介绍】

并发:多个任务在同一时间段内执行,cpu不停切换来执行不同任务

并行:多核CPU上,多个任务在同一时刻执行

要想高并发,硬件很重要,但是成本很高,企业希望在有限的硬件上,最优化软件的性能

性能指标

QPS

  • QPS:每秒钟处理请求的数量,业务处理时间越低,QPS越高

Tomcat 的 QPS:SpringBoot的Tomcat默认是最大是200个线程,如果请求处理消耗50ms,理论QPS就是1000*200/50=4000,实际大概率会更低

可以在配置文件中设置tomcat的线程数量

在这里插入图片描述

【使用Jmeter测试】

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

如果异常很大,超过0.5%,数据就没有太大的价值

Tomcat最大连接数改成400

在这里插入图片描述

如果并发量非常大,一个Tomcat顶不住,可以做服务集群。

  • 一个nginx可以顶住5w的QPS,再负载均衡到多个tomcat服务中

在这里插入图片描述

  • 并发量达到30w,nginx顶不住了,使用好机器来提供虚拟IP,然后再将请求分发到多个Nginx中
    在这里插入图片描述

  • 个人开发,100wQPS就很强了。如果有很大的流量,可以根据用户IP拆分到不同地区的机房。一个域名下面对应很多个服务器IP,按照用户IP区域将其分发大较近的机房IP即可

在这里插入图片描述

TPS

每秒钟能够处理的事务或交易的数量。

怎么优化接口性能

  • 减少IO(批量查询、批量插入、批量删除)
  • 尽早return(例如先去Redis判断的库存够不够,再去执行扣减库存)
  • 能异步就异步(减库存放到MQ)
  • 锁粒度尽量小
  • 事务范围尽可能小
  • 前端分流(如拼图滑块、计算,有人快、有人慢,同时可以验证是否为机器人)
  • 做限制(一个人针对一个商品只能抢一次优惠券,Redis setnx,抢过就不让进来了)

在这里插入图片描述

  • seckill-web:接受秒杀请求,然后把业务交给seckill-service执行
  • seckill-service:处理秒杀真实业务

技术选型

  • Springboot 接收请求并操作 redis 和 MySQL
  • Redis 用于缓存+分布式锁
  • RocketMQ 用于解耦、削峰、异步
  • MySQL 用于存放真实的商品信息
  • Mybatis 用于操作数据库的orm框架

用户量评估

总用户量:50w

日活量:1-2w(用户不会天天用,除非经常做活动)

qps:2w+(怎么统计,日志,统计次数)

几台服务器(什么配置):8C16G 4-6台

  • seckill-web:4台
  • seckill-service:2台

带宽:100M

技术要点

  1. 通过 redis 的 setnx 对用户和商品做去重判断, 防止用户刷接口
  2. 每天晚上 8 点通过定时任务把 MySQL 中参与秒杀的库存商品, 同步到 redis 中去, 做库存的预扣减, 提升接口性能
  3. 通过 RocketMQ 消息中间件的异步消息, 来将秒杀的业务异步化, 进一步提升性能
  4. seckill-service 使用并发消费模式, 并且设置合理的线程数量, 快速处理队列中堆积的消息
  5. 使用 redis 的分布式锁+自旋锁, 对商品的库存进行并发控制, 把并发压力转移到程序中和 redis 中去, 减少 db 压力
  6. 使用声明式事务注解 Transactional, 并且设置异常回滚类型, 控制数据库的原子性操作
  7. 使用 jmeter 压测工具, 对秒杀接口进行压力测试, 在 8C16G 的服务器上, qps2k+, 达到压测预期

架构图

在这里插入图片描述

数据库

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;--   ----------------------------
-- Table structure for goods
--   ----------------------------
DROP TABLE IF EXISTS   `goods`;
CREATE TABLE `goods`  (`id` int(11) NOT NULL AUTO_INCREMENT,`goods_name` varchar(255) CHARACTER SET   utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,`price` decimal(10, 2) NULL DEFAULT NULL,`stocks` int(255) NULL DEFAULT NULL,`status` int(255) NULL DEFAULT NULL,`pic` varchar(255) CHARACTER SET utf8mb4   COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,`create_time` datetime(0) NULL DEFAULT   NULL,`update_time` datetime(0) NULL DEFAULT   NULL,PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB   AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci   ROW_FORMAT = Dynamic;--   ----------------------------
-- Records of goods
--   ----------------------------
INSERT INTO `goods` VALUES   (1, '小米12s', 4999.00,   1000, 2, 'xxxxxx', '2023-02-23 11:35:56', '2023-02-23 16:53:34');
INSERT INTO `goods` VALUES   (2, '华为mate50', 6999.00,   10, 2, 'xxxx', '2023-02-23 11:35:56', '2023-02-23 11:35:56');
INSERT INTO `goods` VALUES   (3, '锤子pro2', 1999.00,   100, 1, NULL, '2023-02-23 11:35:56', '2023-02-23 11:35:56');--   ----------------------------
-- Table structure for   order_records
--   ----------------------------
DROP TABLE IF EXISTS   `order_records`;
CREATE TABLE   `order_records`  (`id` int(11) NOT NULL AUTO_INCREMENT,`user_id` int(11) NULL DEFAULT NULL,`order_sn` varchar(255) CHARACTER SET   utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,`goods_id` int(11) NULL DEFAULT NULL,`create_time` datetime(0) NULL DEFAULT   NULL,PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB   AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci   ROW_FORMAT = Dynamic;SET FOREIGN_KEY_CHECKS = 1;

创建项目选择依赖seckill-web(接受用户秒杀请求)

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.6.13</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.powernode</groupId><artifactId>seckill-web</artifactId><version>0.0.1-SNAPSHOT</version><name>seckill-web</name><description>Demo project for Spring Boot</description><properties><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId><optional>true</optional></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><!-- RocketMQ的依赖 --><dependency><groupId>org.apache.RocketMQ</groupId><artifactId>RocketMQ-spring-boot-starter</artifactId><version>2.2.1</version></dependency><dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2</artifactId><version>2.0.14</version></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><excludes><exclude><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></exclude></excludes></configuration></plugin></plugins></build></project>

修改配置文件

server:port: 7001tomcat:threads:max: 400
spring:application:name: seckill-webredis:host: 127.0.0.1port: 6379database: 0lettuce:pool:enabled: truemax-active: 100max-idle: 20min-idle: 5
RocketMQ:name-server: 192.168.188.129:9876     # RocketMQ的nameServer地址producer:access-key: dsad secret-key: dsadasfasgroup: powernode-group        # 生产者组别,不配置会报错send-message-timeout: 3000  # 消息发送的超时时间retry-times-when-send-async-failed: 2  # 异步消息发送失败重试次数max-message-size: 4194304       # 消息的最大长度

创建SeckillController

package com.powernode.controller;import com.alibaba.fastjson.JSON;
import org.apache.RocketMQ.client.producer.SendCallback;
import org.apache.RocketMQ.client.producer.SendResult;
import org.apache.RocketMQ.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.HashMap;
import java.util.concurrent.atomic.AtomicInteger;@RestController
public class SeckillController {@Autowiredprivate StringRedisTemplate redisTemplate;@Autowiredprivate RocketMQTemplate RocketMQTemplate;/*** 压测时自动是生成用户id*/AtomicInteger ai = new AtomicInteger(0);/*** 1.用户去重,一个用户针对一种商品只能抢购一次* 2.做库存的预扣减  拦截掉大量无效请求* 3.放入mq 异步化处理订单* userId通过登录状态拿取* @return*/@GetMapping("doSeckill")public String doSeckill(Integer goodsId /*, Integer userId*/) {int userId = ai.incrementAndGet();// unique key 唯一标记 去重String uk = userId + "-" + goodsId;// set nx  set if not exist。如果要每天刷新,key加上年月日即可,key再设置过期时间Boolean flag = redisTemplate.opsForValue().setIfAbsent("seckillUk:" + uk, "");if (!flag) {return "您已经参与过该商品的抢购,请参与其他商品抢购!";}// 假设库存已经同步了  key:goods_stock:1  val:10// 直接扣减数量,线程安全。如果先查出来,再减少,线程不安全Long count = redisTemplate.opsForValue().decrement("goods_stock:" + goodsId);// getkey  java  setkey    先查再写 再更新 有并发安全问题if (count < 0) {return "该商品已经被抢完,请下次早点来";}// 放入mqHashMap<String, Integer> map = new HashMap<>(4);map.put("goodsId", goodsId);map.put("userId", userId);RocketMQTemplate.asyncSend("seckillTopic3", JSON.toJSONString(map), new SendCallback() {@Overridepublic void onSuccess(SendResult sendResult) {System.out.println("发送成功" + sendResult.getSendStatus());}@Overridepublic void onException(Throwable throwable) {System.err.println("发送失败" + throwable.getMessage());}});// 不能直接返回抢购成功,因为MQ可能是有问题的return "拼命抢购中,请稍后去订单中心查看";}
}

创建项目选择依赖seckill-service(处理秒杀)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.6.13</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.powernode</groupId><artifactId>seckill-service</artifactId><version>0.0.1-SNAPSHOT</version><name>seckill-service</name><description>Demo project for Spring Boot</description><properties><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.3.0</version></dependency><dependency><groupId>com.MySQL</groupId><artifactId>MySQL-connector-j</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId><optional>true</optional></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.2.6</version></dependency><!-- RocketMQ的依赖 --><dependency><groupId>org.apache.RocketMQ</groupId><artifactId>RocketMQ-spring-boot-starter</artifactId><version>2.2.1</version></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId></dependency><dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2</artifactId><version>2.0.14</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><excludes><exclude><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></exclude></excludes></configuration></plugin></plugins></build></project>

修改yml文件

server:port: 7002
spring:application:name: seckill-servicedatasource:driver-class-name: com.MySQL.cj.jdbc.Driverurl: jdbc:MySQL://127.0.0.1:3306/seckill?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTCusername: rootpassword: 123456redis:host: 127.0.0.1port: 6379database: 0lettuce:pool:enabled: truemax-active: 100max-idle: 20min-idle: 5
mybatis:configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImplmapper-locations: classpath*:mapper/*.xml
RocketMQ:name-server: 192.168.188.129:9876

逆向生成实体类

修改启动类

@SpringBootApplication
@MapperScan(basePackages = {"com.powernode.mapper"}) // mapper上面有@Mapper注解,这里就不用加扫描了
@EnableScheduling // 开启定时任务
public class seckillServiceApplication {public static void main(String[] args) {SpringApplication.run(seckillServiceApplication.class, args);}
}

修改GoodsMapper

List<Goods> selectSeckillGoods();

修改GoodsMapper.xml

<!--  查询数据库中需要参于秒杀的商品数据 status = 2 -->
<select id="selectSeckillGoods" resultMap="BaseResultMap">select `id`,`stocks` from goods where `status` = 2
</select>

同步MySQL数据到redis

方法1
package com.powernode.config;import com.powernode.domain.Goods;
import com.powernode.mapper.GoodsMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;import javax.annotation.PostConstruct;
import java.util.List;/*** 将MySQL的参与抢购的商品的数据* 同步到redis里面去* 在上游服务需要使用redis来做库存的预扣减*/
@Component
public class DataSyncConfig {@Autowiredprivate GoodsMapper goodsMapper;@Autowiredprivate StringRedisTemplate redisTemplate;// 业务场景是搞一个定时任务 每天10点开启// 为了 测试方便 项目已启动就执行一次/*** spring bean的生命周期* 在当前对象 实例化完以后* 属性注入以后* 执行 PostConstruct 注解的方法*/@PostConstruct// java的注解,不是Spring的注解,项目启动的时候,就执行这个方法@Scheduled(cron = "0 10 0 0 0 ?")public void initData() {List<Goods> goodsList = goodsMapper.selectSeckillGoods();if (CollectionUtils.isEmpty(goodsList)) {return;}goodsList.forEach(goods -> redisTemplate.opsForValue().set("goods_stock:" + goods.getId(), goods.getStocks().toString()));}}

不用上面的方法的话,可以在启动类中写,但是不推荐

在这里插入图片描述

Bean生命周期

  • 实例化对象 new

  • 属性赋值

  • 初始化

    • spring

在这里插入图片描述

  • boot (前:PostConstruct,或下面写法;中;后)

  • @Component
    public class DataSync implements InitializingBean, BeanPostProcessor{@Override public void afterropertiesSet() throws Exception {} @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException{}@Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException{}
    }
    
  • 使用

  • 销毁

方法2
package com.powernode.data;import com.powernode.domain.Goods;
import com.powernode.mapper.GoodsMapper;
import org.springframework.boot.CommandLineRunner;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;import javax.annotation.Resource;
import java.util.List;@Component
public class MySQLToRedis2 implements CommandLineRunner {@Resourceprivate GoodsMapper goodsMapper;@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic void run(String... args) throws Exception {initData();}private void initData() {//1 查询数据库中需要参于秒杀的商品数据List<Goods> goodsList = goodsMapper.queryseckillGoods();ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();//2 把数据同步到Redisfor (Goods goods : goodsList) {operations.set("goods:" + goods.getGoodsId(), goods.getTotalStocks().toString());}}}

秒杀业务监听器

package com.powernode.listener;import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.powernode.service.GoodsService;
import org.apache.RocketMQ.common.message.MessageExt;
import org.apache.RocketMQ.spring.annotation.RocketMQMessageListener;
import org.apache.RocketMQ.spring.core.RocketMQListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;import java.util.concurrent.TimeUnit;/*** 默认负载均衡模式* 默认多线程消费*/
@Component
@RocketMQMessageListener(topic = "seckillTopic3", consumerGroup = "seckill-consumer-group")
public class SeckillMsgListener implements RocketMQListener<MessageExt> {@Autowiredprivate GoodsService goodsService;@Autowiredprivate StringRedisTemplate redisTemplate;// 20sint time = 20000;/** 扣减库存* 写订单表*/@Overridepublic void onMessage(MessageExt message) {String s = new String(message.getBody());JSONObject jsonObject = JSON.parseObject(s);Integer goodsId = jsonObject.getInteger("goodsId");Integer userId = jsonObject.getInteger("userId");// 减库存,写订单表,使用同步代码块
//        synchronized (this) {
//            goodsService.realDoSeckill1(goodsId, userId);
//        }// 减库存,写订单表,使用MySQL行锁// goodsService.realDoSeckill1(goodsId, userId);// 减库存,写订单表,使用Redis自旋加锁  int current = 0;// 如果有业务因为自旋时间限制,在有限时间内没有抢得到锁,可以增加限制时间上限,或者把循环改成truewhile (current <= time) {// 一般在做分布式锁的情况下,会给锁一个过期时间,防止出现死锁Boolean flag = redisTemplate.opsForValue().setIfAbsent("goods_lock:" + goodsId, "", 10, TimeUnit.SECONDS);if (flag) {// 加锁成功try {goodsService.realDoSeckill(goodsId, userId);return;} finally {// 解锁redisTemplate.delete("goods_lock:" + goodsId);}} else {// 获取锁失败,自旋加锁current += 200;try {Thread.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}}}}}

修改GoodsService

void realDoSeckill(Integer goodsId, Integer userId);

修改GoodsServiceImpl

【基础方案:有问题】

@Resource
private GoodsMapper goodsMapper;@Autowired
private OrderRecordsMapper orderRecordsMapper;/*** 扣减库存* 写订单表* @param goodsId* @param userId*/
@Override
@Transactional(rollbackFor = RuntimeException.class)
public void realDoSeckill(Integer goodsId, Integer userId) {// 扣减库存  插入订单表Goods goods = goodsMapper.selectByPrimaryKey(goodsId);int finalStock = goods.getStocks() - 1;if (finalStock < 0) {// 只是记录日志 让代码停下来   这里的异常用户无法感知throw new RuntimeException("库存不足:" + goodsId);}goods.setStocks(finalStock);goods.setUpdateTime(new Date());// insert 要么成功 要么报错  update 会出现i<=0的情况// update goods set stocks =  1 where id = 1  没有行锁int i = goodsMapper.updateByPrimaryKey(goods);if (i > 0) {// 写订单表OrderRecords orderRecords = new OrderRecords();orderRecords.setGoodsId(goodsId);orderRecords.setUserId(userId);orderRecords.setCreateTime(new Date());// 时间戳生成订单号orderRecords.setOrderSn(String.valueOf(System.currentTimeMillis()));orderRecordsMapper.insert(orderRecords);}
}

上面的实现不是线程安全的,先查了库存,然后再去修改。并发中,可能一开始库存是够的,后面被其他用户抢走了,库存不够了,但是这里的程序还会继续往下执行

【加锁方案:效率低】

在这里插入图片描述

加锁:库存扣减不对,性能差

原因:加事务》加锁》提交事务,MySQL默认事务隔离级别是可重复读。原本有1000件,两个人消费,按理说是998件。但实际上,A进入了方法,修改完库存,释放了锁,但是还没有提交事务,@Transactional是包住整个方法的。B线程进来获得了锁,查询数据库,还是1000件,导致两个线程业务执行完成之后,还剩下999

解决:要先提交事务,才释放锁,这样才是正确的。将代码改成锁包住事务,数据正确性保证了。但是效率还是低

在这里插入图片描述

分布式系统要改成分布式锁

【使用MySQL行锁(innodb才有),并发性能不足】

  • update goods set stocks = stocks - 1会触发行锁
  • update goods set stocks = 具体值不会触发行锁
  • stocks > 1加一个控制
/*** MySQL行锁  innodb  行锁* 分布式锁* todo 答案1** @param goodsId* @param userId*/
@Override
@Transactional(rollbackFor = RuntimeException.class)
public void realDoSeckill1(Integer goodsId, Integer userId) {// update goods set stocks = stocks - 1 ,update_time = now() where id = #{value} and stocks > 1 int i = goodsMapper.updateStocks(goodsId);if (i > 0) {// 写订单表OrderRecords orderRecords = new OrderRecords();orderRecords.setGoodsId(goodsId);orderRecords.setUserId(userId);orderRecords.setCreateTime(new Date());// 时间戳生成订单号orderRecords.setOrderSn(String.valueOf(System.currentTimeMillis()));orderRecordsMapper.insert(orderRecords);}
}

缺点:通过MySQL来控制锁,数据库压力大,如果并发数在1000以下还好,高一点还是建议其他方案

【在监听器中使用Redis自旋加锁】

详情看前面的秒杀业务监听器实现

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

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

相关文章

鸿蒙开发(NEXT/API 12)【申请接入Wear Engine服务】 穿戴服务

申请Wear Engine服务前&#xff08;开发者需实名认证为个人开发者或者企业开发者&#xff0c;认证前&#xff0c;请先了解二者的[权益区别] &#xff09;&#xff0c;确认开发环境并完成创建项目、创建HarmonyOS应用等基本准备工作&#xff0c;再继续进行以下开发活动。 进入华…

JVM(HotSpot):字符串常量池(StringTable)

文章目录 一、内存结构图二、案例讲解三、总结 一、内存结构图 JDK1.6 JDK1.8 我们发现&#xff0c;StringTable移入了Heap里面。所以&#xff0c;应该想到&#xff0c;StringTable将受到GC管理。 其实&#xff0c;1.6中&#xff0c;在方法区中的时候&#xff0c;也是受GC管…

Android Studio 新版本 Logcat 的使用详解

点击进入官方Logcat介绍 一个好的Android程序员要会使用AndroidStudio自带的Logcat查看日志&#xff0c;会Log定位也是查找程序bug的第一关键。同时Logcat是一个查看和处理日志消息的工具&#xff0c;它可以更快的帮助开发者调试应用程序。 步入正题&#xff0c;看图说话。 点…

Linux 之 IO模型

IO的本质是基于操作系统接口来控制底层的硬件之间数据传输&#xff0c;并且在操作系统中实现了多种不同的IO方式&#xff08;模型&#xff09;&#xff0c;比较常见的有下列三种 阻塞型IO模型 非阻塞型IO模型 多路复用IO模型 一、阻塞与非阻塞IO 一般默认的 IO 操作都是阻塞…

在Linux中进行OpenSSH升级(编译安装在openssh目录)

由于OpenSSH有严重漏洞&#xff0c;因此需要升级OpenSSH到最新版本。 注意&#xff1a;在OpenSSH升级过程中千万不要断开服务器连接&#xff0c;不然的话&#xff0c;会出现断开后连接不了服务器的情况。 第一步&#xff0c;查看当前的OpenSSH服务版本。 命令&#xff1a;ss…

DataEase v2 开源代码 Windows 从0到1环境搭建

一、环境准备 功能名称 描述 其它 操作系统 Windows 数据库 Mysql8.0 开发环境 JDK17以上 本项基于的21版本开发 Maven 3.9版本 开发工具 idea2024.2版本 前端 VSCode TIPS&#xff1a;如果你本地有jdk8版本&#xff0c;需要切换21版本&#xff0c;请看…

C语言 | Leetcode C语言题解之第448题找到所有数组中消失的数字

题目&#xff1a; 题解&#xff1a; int* findDisappearedNumbers(int* nums, int numsSize, int* returnSize) {for (int i 0; i < numsSize; i) {int x (nums[i] - 1) % numsSize;nums[x] numsSize;}int* ret malloc(sizeof(int) * numsSize);*returnSize 0;for (in…

遥感图像文本检索

遥感图像文本检索是一种通过自然语言描述&#xff0c;从大量遥感图像中搜索与之相关的图像的技术。它用于遥感解释任务中&#xff0c;帮助用户根据文字描述快速找到符合条件的遥感图像&#xff0c;这在城市规划、环境监测、灾害管理等领域具有重要应用意义。 实现这一技术的核…

线路交换与分组交换的深度解析

1. 线路交换 原理 线路交换是一种在通信双方之间建立固定通信路径的方式。当用户发起通信时&#xff0c;网络为其分配一条专用的物理通道&#xff0c;这条通道在整个通话过程中保持不变。这意味着在通话期间&#xff0c;其他用户无法使用这条线路。 优点 稳定性&#xff1a…

记录一次出现循环依赖问题

具体的结构设计&#xff1a; 在上面的图片中&#xff1a; UnboundBlackVerifyChain类中继承了UnboundChain类。但是UnboundChain类中注入了下面三个类。 Scope(“prototype”) UnboundLinkFlowCheck类 Scope(“prototype”) UnboundUserNameCheck类 Scope(“prototype”) Un…

【刷题6】一维前缀和、二维前缀和

目录 一、一维前缀和二、二维前缀和 一、一维前缀和 题目&#xff1a; 思路&#xff1a; 一、前缀和&#xff0c;时间复杂度O&#xff08;1&#xff09;&#xff0c;快速得到区间的值 二、预处理&#xff0c;公式——dp[i] dp[i-1] arr[i] 三、使用前缀和&#xff0c;根据…

VUE a-table 动态拖动修改列宽+固定列

实现效果 实现思路 自定义表头&#xff0c;在标题后面加两个标签&#xff0c;分别用来显示拖拽图标&#xff08;cursor: col-resize&#xff09;&#xff0c;和蓝色标记线&#xff08;有的时候鼠标移动过程中不一定会在表内&#xff0c;这个时候不显示图标&#xff0c;只显示蓝…

综合练习 学习案例

//验证码 前四位是字母 最后一位是数字 public class test1 {public static void main(String[] args){char [] charsnew char[52];for (int i 0; i <chars.length ; i) {if(i<25){chars[i](char)(i97);}else{chars[i](char)(i65-26);}}Random rnew Random();String cod…

828华为云征文|华为云Flexus云服务器X实例部署 即时通讯IM聊天交友软件——高性能服务器实现120W并发连接

营运版的即时通讯IM聊天交友系统&#xff1a;特点可发红包&#xff0c;可添加多条链接到用户网站和应用&#xff0c;安卓苹果APPPC端H5四合一 后端开发语言&#xff1a;PHP&#xff0c; 前端开发语言&#xff1a;uniapp混合开发。 集安卓苹果APPPC端H5四合一APP源码&#xff0…

语音转文字免费利器:助力高效办公与学习

语音转文字免费的软件如同一股清流&#xff0c;让我们能够更轻松地将语音信息转化为可编辑的文字内容。今天我们一起来分析它们的功能、特点以及如何为我们的生活和工作带来便利。 1.365在线转文字 链接直达&#xff1a;https://www.pdf365.cn/ 这是一个功能强大的在线工具…

量化必备!股票常用数据批量下载、定时更新,代码打包好了!

上一节课我详细演示了从tushare获取股票列表和基本信息并且配置定时更新任务的详细流程&#xff0c;旨在教会想要学习通过Python获取股票数据并且定期更新的朋友。 不过有很多朋友完全没有Python基础&#xff0c;如果一开始把大量时间花费在搞数据上&#xff0c;本末倒置不说&…

如果您忘记了 Apple ID 和密码,按照指南可重新进入您的设备

即使您的 iPhone 或 iPad 由于各种原因被锁定或禁用&#xff0c;也可以使用 iTunes、“查找我的”、Apple 支持和 iCloud 解锁您的设备。但是&#xff0c;此过程需要您的 Apple ID 和密码来验证所有权并移除激活锁。如果您忘记了 Apple ID 和密码&#xff0c;请按照我们的指南重…

【PyTorch】图像分割

图像分割是什么 Image Segmentation 将图像每一个像素分类 图像分割分类 超像素分割&#xff1a;少量超像素代替大量像素&#xff0c;常用于图像预处理语义分割&#xff1a;逐像素分类&#xff0c;无法区分个体实例分割&#xff1a;对个体目标进行分割全景分割&#xff1a;…

动态库的加载全过程

区分一组概念&#xff1a;逻辑地址&#xff0c;虚拟地址&#xff0c;物理地址。 逻辑地址&#xff1a;是我们的代码在编译过程&#xff0c;编译器帮对每一条代码所生成的指令所编写的地址。 物理地址&#xff1a;当程序被放入到内存中时&#xff0c;内存与每一条指令所对应的…

031集——文本文件按空格分行——C#学习笔记

如下图&#xff0c;读取每行文本&#xff0c;每行文本再按空格分开读取一个字符串&#xff0c;输出到另一个文本&#xff1a; CAD环境下&#xff0c;代码如下&#xff1a; using Autodesk.AutoCAD.DatabaseServices; using Autodesk.AutoCAD.Runtime; using System; using Sys…