004 秒杀下单

news2024/11/26 20:24:04

文章目录

  • 超卖问题
    • 方案一
    • 方案二
    • 方案三
    • aop锁(单机锁)
      • aop锁(单机锁)
        • pom.xml
        • LockAspect.java
        • ServiceLock.java
    • 分布式锁
      • Mysql分布式锁
      • Redis分布式锁
        • ServiceRedisLock.java
        • LockRedisAspect.java
    • 下单性能优化
    • 数据一致性
    • 解决一致性问题
      • 异步同步库存

秒杀下单业务步骤:
1.数据校验(身份信息,token,手机号,是否开始,库存是否充足,是否开启秒杀,是否上架)
2.检查库存,锁定库存
3.扣减库存
4.更新库存
5.实现下单

面临的问题:
1.业务问题:如何在高并发模式下,保证库存不会出现超卖
2.性能问题:如何在高并发模式下,保证下单操作性能
3.数据一致性问题:如何在高并发模式下,保证数据一致性

超卖问题

原因:
在高并发模式下,多线程出现了数据脏读,抢占cpu资源情况下,出现了数据脏读,从而操作了多下订单,因此出现超卖

超卖:比如10个商品,商品数量为0的时候,下单了100个订单,多下单90个
超卖

如何解决超卖问题:
1.上锁(意味着性能下降,一旦上锁,意味着程序的串行化的执行)
2.原子性操作
3.队列(Queue,Redis,队列)

方案一

给业务进行上锁,让库存扣减变成一个原子的操作,让下单的操作是串行化执行,只有当第一个线程执行结束后,后一个线程才能开始执行,从而控制库存超卖。
注意:在分布式环境下,需要使用分布式锁来控制库存

方案二

利用redis的单线程模式:实现原子性操作,让库存得到控制。Redis服务具备天然的原子性的操作特性,Redis的每一个操作都是一个原子性的操作,因此可以利用Redis的这个特性,实现库存控制,且Redis是高性能的内存数据库,利用redis实现性能与业务的完美结合
原子性操作
以上数据存储特点:把库存数据进行单独的存储,扣减库存直接使用库存进行扣减,而不是使用商品中数据进行扣减(因为使用商品数据扣减,必然会经过2步操作,这2步不是原子性,除非使用lua)
此时扣减库存的方式
1.扣减库存:hincrement(“seckill_goods_stock_1”,-1) #此操作是一个原子操作,下一个线程看见的是上一个线程执行的结果,线程之间具有先后顺序
2.判断库存是否存在
优点:既兼顾了性能问题,又解决了业务库存超卖问题

方案三

队列:Redis队列(其他队列)都具有原子性的操作:Redis-list队列实现库存超卖解决方案
特点:
1.队列的长度等于库存数量
2.队列中存储的数据是此商品的id
3.每一个商品都对应一个队列
此时扣减库存,只需要pop一个队列的元素即可,因为队列的长度等于库存数量,因此pop元素相当于扣减库存;此操作也是原子操作

aop锁(单机锁)

超卖:比如模拟1000个用户,产生1000个订单,实际上被卖出75个商品,因此超卖925个订单!!!
超卖
使用Lock锁进行库存控制:Lock lock = new ReentrantLock(true);

//开始加锁
lock.lock();

finally{
//释放锁
lock.unlock;
}

以上加锁方式不能控制库存
锁事务冲突
锁事务冲突

aop锁(单机锁)

问题:针对以上的锁,事务冲突的问题
解决方案:锁上移(在事务开始之前加锁,事务结束后释放锁)
实现方式:表现层加锁,aop增强的方式进行加锁
aop锁

pom.xml

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


LockAspect.java

package com.example.aop;


import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.context.annotation.Scope;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

@Component
@Scope
@Aspect
@Order(1)
public class LockAspect {

    private Lock lock = new ReentrantLock(true);

    @Pointcut("@annotation(com.example.aop.ServiceLock)")
    public void lockAspect(){

    }

    //增强方法
    @Around("lockAspect()")
    public Object around(ProceedingJoinPoint joinPoint){
        Object obj = null;
        //上锁
        lock.lock();

        try {
            //执行业务
            obj = joinPoint.proceed();
        } catch (Throwable e) {
            throw new RuntimeException(e);
        } finally {
            //释放锁
            lock.unlock();
        }

        return obj;

    }
}


ServiceLock.java

package com.example.aop;


import java.lang.annotation.*;

@Target({ElementType.PARAMETER,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ServiceLock {
    String description() default "";
}


功能测试:没有出现超卖,加锁已经实现了库存的控制

分布式锁

为什么要使用分布式锁:
在JUC单机锁的模式下,只能在单个jvm进程中起作用,但是在集群,分布式部署模式下,无法使用单机锁控制多个jvm进程的并发修改问题,无法实现库存超卖控制
在集群服务,分布式服务模式下,存在多个jvm进程对共享资源并发修改的问题,单机锁无法控制在进程级别的共享资源互斥访问的问题,因此在分布式环境下,必须使用分布式锁
第三方锁

分布式应用原理:保证jvm进程对共享资源的互斥访问,防止jvm进程对共享资源并发修改
应用场景:
1.秒杀场景
2.12306抢票
3.退款

Mysql分布式锁

Mysql实现分布式锁几种方式:
1.乐观锁,悲观锁(这种方式在分布式模式下无法控制库存,单机可以控制)
在多进程模式下,多个事务出现了数据脏读,从而无法控制超卖,虽然加上行锁,但是锁失效后,事务还未提交,此时别的进程事务来读取数据,读到了脏数据
2.单独设计一个表,实现记录锁的操作(加锁:插入一条数据,释放锁:删除一条数据)

Redis分布式锁

ServiceRedisLock.java

package com.example.redis;


import java.lang.annotation.*;

@Target({ElementType.PARAMETER,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ServiceRedisLock {
    String description() default "";
}


LockRedisAspect.java

package com.example.redis;



import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

@Component
@Scope
@Aspect
@Order(1)
public class LockRedisAspect {

    @Autowired
    private HttpServletRequest request;



    @Pointcut("@annotation(com.example.redis.ServiceRedisLock)")
    public void lockAspect(){

    }

    //增强方法
    @Around("lockAspect()")
    public Object around(ProceedingJoinPoint joinPoint){

        Object obj = null;

        //获取秒杀id
        String requestURI = request.getRequestURI();
        String killId = requestURI.substring(requestURI.lastIndexOf("/")-1,requestURI.lastIndexOf("/"));

        //上锁

        boolean res = RedissLockUtil.tryLock("seckill_goods_lock_"+killId, TimeUnit.SECONDS,3,10);

        lock.lock();

        try {
            //执行业务
            if (res){
                obj = joinPoint.proceed();
            }

        } catch (Throwable e) {
            throw new RuntimeException(e);
        } finally {
            //释放锁
            if (res){
                RedissLockUtil.unlock("seckill_goods_lock_"+killId);
            }
        }

        return obj;

    }
}


在分布式下测试,可以控制库存

下单性能优化

优化一:从缓存中查询商品数据,不再从数据库查询
优化二:扣减库存,从缓存中开始扣减库存,不考虑数据一致性问题,只需要考虑数据最终一致性即可
优化三:异步化改造,下单的时候,只需要把订单数据传入到队列即可表示下单成功,后面队列的消费者来异步消费消息,实现下订单操作
下单的写的操作,当并发量比较大的时候,写操作会竞争锁资源,造成数据库性能下降。因此对这块代码进行异步化改造
异步处理:消费者在消费端进行监听,如果发现队列中有数据,立马消费队列中数据,然后处理业务



//判断库存是否还存在,如果不存在,那么就直接返回

Integer stockStatus = (Integer) redisTemplate.opsForValue().get(Constants.REDIS_GOODS_END_KEY+killId);

//判断
if(stockStatus!=null && stockStatus.equals(HttpStatus.SEC_GOODS_END)){
	return HttpResult.error("商品已无库存");
}


//优化一:从缓存中查询商品数据,不再从数据库查询

TbSeckillGoods seckillGoods = (TbSeckillGoods) redisTemplate.opsForValue().get("SECKILL_GOODS_STOCK_"+killId);


/**
*库存扣减
*/
private boolean reduceStock(Long killId){
	Long stockNum = redisTemplate.opsForValue.increment("SECKILL_GOODS_STOCK_"+killId,-1);
//扣减成功
if(stockNum > 0){
	return true;
}else if(stockNum == 0){
	//最后一次扣减,stock=1,表示此时库存已经售卖完毕
	//添加标识,表示库存已经扣减完毕
	redisTemplate.opsForValue().set(Constants.REDIS_GOODS_END_KEY+killId,HttpStatus.SEC_GOODS_END);
	return true;
}
//扣减失败
return false;
}





//第二步优化,从缓存中扣减库存,保证这个操作的原子性
boolean res = this.reduceStock(killId);
//判断库存是否扣减成功
if(!res){
	return HttpResult.error("下单失败");
}


//下单
TbSeckillOrder order = new TbSeckillOrder();
order.setSeckillId(killId);
order.setUserId(userId);
//使用队列,把订单数据入队
Boolean succ = SeckillQueue.getMailQueue().produce(order);
if(!succ){
	return HttpResuLt.error("秒杀失败");
}
return HttpResult.ok("秒杀成功");



public void run(ApplicationArguments var){
	new Thread(() -> {
		LOGGER.info("提醒队列启动成功");
		//开启一个线程,一直监听bockingQueue队列
		while(true){
			try {
				//进程内队列
				TbSeckillOrder order = SeckillQueue.getMailQueue().consume();
				if(order!=null){
					//从队列中获取订单,执行下单操作
					seckillService.startAsyncKilled(order);
				}
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}).start();
}

数据一致性

CAP定理
C:一致性(数据一致性:牺牲性能为代价)
A:可用性(性能提升,暂时不追求一致性)
P:分区容错性
redis数据
CAP定理要求在软件架构的设计中,不能同时追求一致性,可用性,要么追求强一致性,要么只实现高性能
问题1:从缓存中扣减库存,存在缓存中缓存库存和数据库库存不一致现象
为了性能考虑,牺牲掉一致性,暂时把数据放在缓存中,放弃了一致性的问题,但是最终需要把数据变成一致性的状态。
如何处理?
(1)最终的一致性:支付完成后,同步库存
(2)异步的方式,同步库存

问题2:下单操作,扣减库存操作不是一个原子操作,一旦下单异常失败,本地事务会回滚,但是redis库存已经发生扣减

解决方案:
(1)异常机制对业务补偿
(2)缓存一致性

解决一致性问题

异步同步库存

发生场景,从缓存中扣减库存,但是数据库的库存没有发生任何的变化,因此可以使用异步的方式同步库存。
异步同步库存
引入新的问题,发送消息的操作和本地事务的操作不是一个原子性
半消息机制
消息一致性:为了保证本地消息,本地事务一致性
事务消息
Rocketmq提供的事务消息,解决本地事务和数据库一致性问题,让发送消息的动作和本地事务是原子性的操作

缓存一致性:先操作数据库,再操作redis
缓存一致性
同时操作数据库,缓存的时候,面临数据库和缓存数据一致性的问题,因为本地事务异常,缓存异常都可能造成数据一致性问题,因此解决这类问题的时候,只需要先操作数据库,后操作缓存即可
1.下单操作数据库出现了异常,本地事务回滚,此时缓存没有进行操作,因此数据是一致性的状态
2.下单成功,缓存操作异常,数据库本地事务会回滚,由于缓存没有操作成功,因此数据还是一致状态


@Transactional
@Override
public HttpResult startAsyncKilled(TbSeckillOrder order) {
	//为了实现缓存,数据库一致性,先操作数据库,后操作缓存
	order.setCreateTime(new Date());
	order.setStatus("0");
	order.setMoney(BigDecimal.ZERO);
	seckillOrderMapper.insertSelective(order);

	//第二步优化,从缓存中扣减库存,保证这个操作的原子性
	boolean res = this.reduceStock(order.getSeckillId());
	//判断库存是否扣减成功
	if(!res){
		return HttpResult.error("下单失败");
	}
	return HttpResult.ok();
}

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

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

相关文章

DS:顺序表、单链表的相关OJ题训练

欢迎各位来到 Harper.Lee 的学习小世界&#xff01; 博主主页传送门&#xff1a;Harper.Lee的博客主页 想要一起进步的uu可以来后台找我交流哦&#xff01; 在DS&#xff1a;单链表的实现 和 DS&#xff1a;顺序表的实现这两篇文章中&#xff0c;我详细介绍了顺序表和单链表的…

Unity SteamVR入门

概述 VR项目现在在当前已经是非常热门的技术&#xff0c;可以给玩家身临其境的感觉&#xff0c;接下来让我们学习这部分的内容吧&#xff01; SteamVR Input SteamVR绑定流程&#xff0c;在Windows窗口的点击SteamVR-input&#xff0c;图1&#xff0c;在这里可以选择你需要绑定…

Photoshop前言

Photoshop前言 分辨率图像格式工具界面组件 分辨率 分辨率是指单位长度内包含的像素点的数量&#xff0c;其单位通常为像素/英寸&#xff08;ppi&#xff09;&#xff0c;300ppi表示每英寸包含300个像素点。对于1英寸1英寸大小的图像&#xff0c;若分辨率为72ppi&#xff0c;则…

Unity MeshRenderer 入门

概述 在项目制作过程中&#xff0c;肯定缺少不了模型的使用&#xff0c;那就一定接触过MeshRenderer&#xff0c;也许还有你不理解的地方&#xff0c;接下来让我们来学习一下这部分的内容吧。 Mesh Filter&#xff08;网格过滤器&#xff09; Mesh:提供一个网格的参考&#xf…

# notepad++ 编辑器英文版,如何打开自动换行

notepad 编辑器英文版&#xff0c;如何打开自动换行 在Notepad中&#xff0c;如果你想要开启自动换行功能&#xff0c;可以按照以下步骤操作&#xff1a; 1、打开 Notepad 编辑器。 1.1. 依次点击菜单栏中的【视图】&#xff0c;英文版对应【View】。1.2. 在【视图】下拉菜单…

Integer中的缓存机制

先看一个示例&#xff1a; public static void main(String[] args) {Integer a127;Integer b127;System.out.println(ab);Integer c128;Integer d128;System.out.println(cd);} 输出&#xff1a; true false 为什么明明都是同一个数字进行比较&#xff0c;当数字等于127的…

串口单线半双工转换电路

用来把单线半双工模式的串口转换成双线&#xff0c;然后才能连接到普通的双线USB 串口模块&#xff0c;比如CH340 之类的。电路设计来自大佬的博客&#xff1a;AVR half-duplex software UART supporting single pin operation。他在Arduino 上用软件模拟串口功能&#xff0c;利…

【火猫DOTA2】电竞世界杯DOTA2项目将在7月份的前三周举办

1、电竞世界杯将于今年7月3日至8月25日在沙特利雅得举办。近日主办方公布了各个项目的举办时间,其中DOTA2项目将在7月份的前三周举办。转载:火猫TV资讯https://www.huomaotv.com/ 目前Falcons、XG、GG和Liquid这五支赢得了足够EPT积分的队伍已经确定直邀沙特。剩下的三个名额还…

nginx的前世今生(二)

书接上回&#xff1a; 上回书说到&#xff0c;nginx的前世今生&#xff0c;这回我们继续说 3.缓冲秘籍&#xff0c;洪流控水 Nginx的缓冲区是其处理数据传输和提高性能的关键设计之一&#xff0c;主要用于暂存和管理进出的数据流&#xff0c;以应对不同组件间速度不匹配的问题…

图片高效批量美化,支持将图片进行旋转180度并生成浴室玻璃图片,让图片瞬间焕然一新!

图片已成为我们生活与工作中不可或缺的一部分。然而&#xff0c;一张张手动美化图片却是一项既耗时又耗力的任务。为了帮助您轻松应对这一挑战&#xff0c;我们推出了一款高效批量美化图片的工具&#xff0c;并支持将图片旋转180度及生成浴室玻璃效果&#xff0c;让您的图片瞬间…

安装英伟达nvidia p4计算卡驱动@FreeBSD14

FreeBSD也能跑cuda AI训练拉&#xff01; 在FreeBSD安装好pytorch和飞桨cpu版本后&#xff0c;尝试安装英伟达nvidia p4计算卡驱动。毕竟全靠cpu速度太慢了&#xff0c;还是GPU快啊&#xff01;在磕磕绊绊几天后&#xff0c;终于成功成功安装好nvidia p4的cuda驱动&#xff0c…

拆单算法交易(Algorithmic Trading)

TWAP TWAP交易时间加权平均价格Time Weighted Average Price 模型&#xff0c;是把一个母单的数量平均地分配到一个交易时段上。该模型将交易时间进行均匀分割&#xff0c;并在每个分割节点上将拆分的订单进行提交。例如&#xff0c;可以将某个交易日的交易时间平均分为N 段&am…

密码学基础练习五道 RSA、elgamal、elgamal数字签名、DSA数字签名、有限域(GF)上的四则运算

1.RSA #include <stdlib.h>#include <stdio.h>#include <string.h>#include <math.h>#include <time.h>#define PRIME_MAX 200 //生成素数范围#define EXPONENT_MAX 200 //生成指数e范围#define Element_Max 127 //加密单元的…

26版SPSS操作教程(高级教程第十八章)

目录 前言 粉丝及官方意见说明 第十八章一些学习笔记 第十八章一些操作方法 经典判别分析 数据假设 具体操作 结果解释 判别结果的图形化展示 结果解释 判别效果验证 结果解释 适用条件的判断 结果解释 贝叶斯判别分析 具体操作 结果解释 逐步判别法 结束语…

Redis---------实现查询缓存业务

目录 数据库与缓存之间的工作业务逻辑&#xff1a; 接下来看查询缓存代码实现&#xff0c;主要是捋清楚业务逻辑&#xff0c;代码实现是死的&#xff1a; Controller: Service: P37作业实现&#xff1a;总体逻辑跟上面的业务逻辑差不多 Controller&#xff1a; Service&#…

【项目构建】04:动态库与静态库制作

OVERVIEW 1.编译动态链接库&#xff08;1&#xff09;编译动态库&#xff08;2&#xff09;链接动态库&#xff08;3&#xff09;运行时使用动态库 2.编译静态链接库&#xff08;1&#xff09;编译静态库&#xff08;2&#xff09;链接静态库&#xff08;3&#xff09;运行时使…

数字身份管理:Facebook如何利用区块链技术?

随着数字化进程的加速&#xff0c;个人身份管理已成为一个关键议题。在这方面&#xff0c;区块链技术正在逐渐展现其巨大潜力。作为全球最大的社交媒体平台&#xff0c;Facebook也在积极探索和应用区块链技术来改进其数字身份管理系统。本文将深入探讨Facebook如何利用区块链技…

【Docker学习】docker start深入研究

docker start也是很简单的命令。但因为有了几个选项&#xff0c;又变得复杂&#xff0c;而且... 命令&#xff1a; docker container start 描述&#xff1a; 启动一个或多个已停止的容器。 用法&#xff1a; docker container start [OPTIONS] CONTAINER [CONTAINER...] 别名&…

【软件工程】需求分析

目录 前言需求分析UML概述用例图用例图的组成用例图中的符号和含义包含的两种使用场景 用例图补充&#xff1a;“系统”用例模型建模确定系统参与者确定系统用例 用例文档用例文档组成部分 活动图组成元素初始节点和终点活动节点转换决策与分支、合并分岔与汇合 类图类的表示类…

【DevOps】怎么找合适的Docker镜像?

目录 一、Docker Hub介绍 主要特点和功能 使用场景 二、怎么找合适的镜像 步骤 1: 访问 Docker Hub 步骤 2: 使用搜索功能 步骤 3: 分析搜索结果 步骤 4: 阅读详细描述 步骤 5: 下载并使用镜像 例子 三、怎么样使用国内镜像加速 常用的国内 Docker 镜像加速器地址 …