redis秒杀

news/2024/4/26 16:07:48/文章来源:https://blog.csdn.net/bubbleJessica/article/details/129133918

redis优惠券秒杀

为什么订单表订单ID不采用自增长?

  • id规律性太明显,容易被用户猜测到(比如第一天下订单id10,第二天下订单id100,在昨天的1天内只卖出90商品)
  • 受单表数据量限制(订单数据量大,日积月累单张表就存不下去这么多数据记录)如果单张表存不了,我们使用多张表的话,表独立的,自增长的话id会出现重复,到时用户买到的商品有问题售后就麻烦了

这时我们就要使用全局ID生成器,是一种在分布式系统下生成全局唯一ID的工具,满足以下特点:唯一性、高可用、高性能、递增性、安全性

为了增加id的安全性,我们可以直接不使用redis自增的数值,而是拼接一些其他的信息 

定义一个类RedisIdWorker类并且配置@Component由spring管理的bean

package com.hmdp.utils;import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;@Component
public class RedisIdWorker {@Resourceprivate StringRedisTemplate stringRedisTemplate;private static final long BEGIN_TIMESTAMP=1672531200;private static final int COUNT_BIT=32;public long nextId(String keyPrefix){// 1.生成时间戳LocalDateTime now = LocalDateTime.now();long nowSecond = now.toEpochSecond(ZoneOffset.UTC);long timestamp=nowSecond-BEGIN_TIMESTAMP;//当前时间戳// 2.生成序列号// 获取当前日期String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));// 自增长// 需要加上日期 因为自增上限是2^64,日积月累的销量越来越多,出32位容易,未来就存不下去,不能使用同一个keyLong count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);// 3.拼接并返回// 我们以为时间戳在最低位 要让他处于高位必须移位 移多少位得看给序列化号足够的位置(这里是32位)return timestamp<<COUNT_BIT|count;}/*public static void main(String[] args) {LocalDateTime time = LocalDateTime.of(2023, 1, 1, 0, 0, 0);long second = time.toEpochSecond(ZoneOffset.UTC);System.out.println(second);//1672531200}*/
}

需要加上日期,不然整个业务订单一直采用同一个key自增长,时隔几年,订单越来越多,redis单个key的值自增上限是2的64次方(好处就是每天业务订单量都是不同的key,一天的订单量不会超过2^32)

package com.hmdp;import com.hmdp.entity.Shop;
import com.hmdp.service.impl.ShopServiceImpl;
import com.hmdp.utils.CacheClient;
import com.hmdp.utils.RedisIdWorker;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;import javax.annotation.Resource;import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;import static com.hmdp.utils.RedisConstants.CACHE_SHOP_KEY;@SpringBootTest
class HmDianPingApplicationTests {@Resourceprivate CacheClient cacheClient;@Resourceprivate ShopServiceImpl shopService;@Resourceprivate RedisIdWorker redisIdWorker;private ExecutorService es = Executors.newFixedThreadPool(500);@Testpublic void testIdWorker() throws InterruptedException {CountDownLatch latch = new CountDownLatch(300);Runnable task = () -> {for (int i = 0; i < 100; i++) {long id = redisIdWorker.nextId("order");System.out.println("id=" + id);}latch.countDown();};long begin = System.currentTimeMillis();for (int i = 0; i < 300; i++) {es.submit(task);//线程池异步所以使用CountDownLatch}latch.await();//等待所有countDown结束long end = System.currentTimeMillis();System.out.println("time=" + (end - begin));}@Testvoid testSaveShop() throws InterruptedException {//shopService.saveShop2Redis(1L,10L);Shop shop = shopService.getById(1L);cacheClient.setWithLogicalExpire(CACHE_SHOP_KEY + 1L, shop, 10L, TimeUnit.SECONDS);}
}

 

添加秒杀劵(这里我们没有编写后台管理系统,为了方便起见,使用postman测试)

{"shopId": 1,"title": "100元代金券","subTitle": "周一到周五均可使用","rules": "全场通用\\n无需预约\\n可无限叠加\\不兑现、不找零\\n仅限堂食","payValue": 8000,"actualValue": 10000,"type": 1,"stock": 100,"beginTime":"2023-02-23T19:00:00","endTime":"2023-02-23T20:00:00"
}

注意,我们点击发送请求前需要添加请求头信息 控制台打印其值即token,否则会报401错

 添加成功

优惠券秒杀下单功能 

package com.hmdp.service.impl;import com.hmdp.dto.Result;
import com.hmdp.entity.SeckillVoucher;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.mapper.VoucherOrderMapper;
import com.hmdp.service.ISeckillVoucherService;
import com.hmdp.service.IVoucherOrderService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisIdWorker;
import com.hmdp.utils.UserHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import javax.annotation.Resource;
import java.time.LocalDateTime;/*** <p>*  服务实现类* </p>** @author 虎哥* @since 2021-12-22*/
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Resourceprivate ISeckillVoucherService seckillVoucherService;@Resourceprivate RedisIdWorker redisIdWorker;@Override@Transactionalpublic Result seckillVoucher(Long voucherId) {// 1.查询优惠券SeckillVoucher voucher = seckillVoucherService.getById(voucherId);// 2.判断秒杀是否开始if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {// 尚未开始return Result.fail("秒杀尚未开始!");}// 3.判断秒杀是否结束if (voucher.getEndTime().isBefore(LocalDateTime.now())) {return Result.fail("秒杀已经结束!");}// 4.判断库存是否充足if (voucher.getStock()<1) {// 库存不足return Result.fail("库存不足!");}// 5.扣减库存// eq代表where条件boolean success=seckillVoucherService.update().setSql("stock=stock-1").eq("voucher_id",voucherId).update();if (!success){// 扣减失败return Result.fail("库存不足!");}// 6.创建订单VoucherOrder voucherOrder = new VoucherOrder();// 6.1. 订单idlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);// 6.2. 用户idLong userId = UserHolder.getUser().getId();voucherOrder.setUserId(userId);// 6.3. 代金券idvoucherOrder.setVoucherId(voucherId);save(voucherOrder);// 7.返回订单Idreturn Result.ok(orderId);}
}

超卖

版本号法+CAS法(compare and set) 

// 5.扣减库存// eq代表where条件boolean success=seckillVoucherService.update().setSql("stock=stock-1")// set stock=stock-1.eq("voucher_id",voucherId).eq("stock",voucher.getStock())// where id=? and stock=?.update();

乐观锁成功率太低(因为只要库存不一致就失败)优化:就是库存大于0即可

乐观锁的方案是在更新数据时候去使用的

// 5.扣减库存// eq代表where条件boolean success=seckillVoucherService.update().setSql("stock=stock-1")// set stock=stock-1.eq("voucher_id",voucherId).gt("stock",0)// where id=? and stock>0.update();

一人一单(知识点:动态代理、事务、aop、悲观锁、锁的范围)

这里是新增数据,无法判断是否有无修改过,因为它不存在,判断是否存在,只能用悲观锁方案解决(从查询订单到判断订单到新增订单这一段逻辑加上悲观锁)

虽然userId值一样,但是每次调用toString方法都是新的对象 

<dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId></dependency>

暴露代理对象 

package com.hmdp;import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;@EnableAspectJAutoProxy(exposeProxy = true)
@MapperScan("com.hmdp.mapper")
@SpringBootApplication
public class HmDianPingApplication {public static void main(String[] args) {SpringApplication.run(HmDianPingApplication.class, args);}}

之前在方法内部添加锁可能会发生情况:开启事务执行,获取锁查询完减库存提交订单释放锁才会提交事务,函数执行完之后由spring去提交事务,锁释放意味着其他线程也可以进来了,而此时事务尚未提交,其他线程趁此时进来,去查询订单的话,刚刚新增的订单很有可能还未写进数据库,因为还未提交,查询依然不存在,产生并发问题, 可知我们刚才锁定的范围缩小了,解决方案:把整个函数锁起来,先事务提交再释放锁,那我们就要把锁放在函数外面,这时就出现事务问题消失 因为此时调用方法的直接是当前目标对象IVoucherOrderService对象,没有事务功能,事务要想生效,其实是我们spring对当前这个类进行动态代理, 拿到当前类的代理对象,用它来作事务处理 

package com.hmdp.service.impl;import com.hmdp.dto.Result;
import com.hmdp.entity.SeckillVoucher;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.mapper.VoucherOrderMapper;
import com.hmdp.service.ISeckillVoucherService;
import com.hmdp.service.IVoucherOrderService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisIdWorker;
import com.hmdp.utils.UserHolder;
import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import javax.annotation.Resource;
import java.time.LocalDateTime;/*** <p>*  服务实现类* </p>** @author 虎哥* @since 2021-12-22*/
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Resourceprivate ISeckillVoucherService seckillVoucherService;@Resourceprivate RedisIdWorker redisIdWorker;@Overridepublic Result seckillVoucher(Long voucherId) {// 1.查询优惠券SeckillVoucher voucher = seckillVoucherService.getById(voucherId);// 2.判断秒杀是否开始if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {// 尚未开始return Result.fail("秒杀尚未开始!");}// 3.判断秒杀是否结束if (voucher.getEndTime().isBefore(LocalDateTime.now())) {return Result.fail("秒杀已经结束!");}// 4.判断库存是否充足if (voucher.getStock()<1) {// 库存不足return Result.fail("库存不足!");}// 5.一人一单Long userId = UserHolder.getUser().getId();synchronized (userId.toString().intern()) {// 获取代理对象(事务)IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);}}// 方法加上同步锁肯定是线程安全的 this指向IVoucherOrderService// 不建议加上方法上 因为代表任何用户来了都要加上锁 串行执行 性能低// 我们只需要同一用户进来才需要加锁@Transactionalpublic Result createVoucherOrder(Long voucherId){// 5.一人一单Long userId = UserHolder.getUser().getId();// 当用户id值一样时,锁就一样// synchronized (userId.toString().intern()) {// 5.1.查询订单int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();// 5.2.判断是否存在if (count > 0) {//用户已经购买过return Result.fail("用户已经购买过一次!");}// 6.扣减库存// eq代表where条件boolean success = seckillVoucherService.update().setSql("stock=stock-1")// set stock=stock-1.eq("voucher_id", voucherId).gt("stock", 0)// where id=? and stock>0.update();if (!success) {// 扣减失败return Result.fail("库存不足!");}// 7.创建订单VoucherOrder voucherOrder = new VoucherOrder();// 7.1. 订单idlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);// 7.2. 用户idvoucherOrder.setUserId(userId);// 7.3. 代金券idvoucherOrder.setVoucherId(voucherId);save(voucherOrder);// 8.返回订单Idreturn Result.ok(orderId);//}}
}

上述处理方案虽然解决了一人一单的并发安全问题(通过加锁可以解决单机情况下的问题),但是在集群模式下就不行了 

通过加锁可以解决在单机情况下的一人一单安全问题,但是在集群模式下就不行了(每个jvm都有自己的锁监视器,集群模式下各个服务器的锁不共享)。
因此,我们的解决方案就是实现一个共享的锁监视器,即:
分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。 

 

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

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

从零开始学习iftop流量监控(找出服务器耗费流量最多的ip和端口)

一、iftop是什么iftop是类似于top的实时流量监控工具。作用&#xff1a;监控网卡的实时流量&#xff08;可以指定网段&#xff09;、反向解析IP、显示端口信息等官网&#xff1a;http://www.ex-parrot.com/~pdw/iftop/二、界面说明>代表发送数据&#xff0c;< 代表接收数…

chatGPT模型原理

文章目录简介BertGPT 初代GPT-2GPT-3chatGPT开源ChatGPT简介 openai 的 GPT 大模型的发展历程。 Bert 2018年&#xff0c;自然语言处理 NLP 领域也步入了 LLM 时代&#xff0c;谷歌出品的 Bert 模型横空出世&#xff0c;碾压了以往的所有模型&#xff0c;直接在各种NLP的建模…

EasyRecovery16MAC苹果版本Photo最新版数据恢复软件

无论是在工作学习中&#xff0c;还是在生活中&#xff0c;Word、Excle等办公软件都是大家很常用的。我们在使用电脑的过程中&#xff0c;有时会因自己的误删或电脑故障&#xff0c;从而导致我们所写的文档丢失了。出现这样的大家不要着急&#xff0c;今天小编就给大家推荐一款可…

FreeRTOS优先级翻转

优先级翻转优先级翻转&#xff1a;高优先级的任务反而慢执行&#xff0c;低优先级的任务反而优先执行优先级翻转在抢占式内核中是非常常见的&#xff0c;但是在实时操作系统中是不允许出现优先级翻转的&#xff0c;因为优先级翻转会破坏任务的预期顺序&#xff0c;可能会导致未…

YOLOv5模型学习记录

新年伊始&#xff0c;YOLOv8横空出世&#xff0c;这个还未开源时便引发界内广泛热议的目标检测算法&#xff0c;一经问世便再次引发热潮&#xff0c;而作为与其师出同源的YOLOv5&#xff0c;自然要拿来与其比较一番。接下来我们便来学习一下吧。 模型结构 首先便是模型结构了…

Rasa 3.x 学习系列-摆脱意图:一种新的对话模式

Rasa 3.x 学习系列-摆脱意图:一种新的对话模式 在2019年的一篇文章中,Alan Nichol写道 :是时候摆脱意图了。一年后,Rasa发布了Rasa中的第一个无意图(或“端到端”)对话模型。现在,我们宣布迈出了一个重要的步伐,将LLM的强大功能带入Rasa的对话管理中。 首先,意图非常…

ACSC 2023 比赛复现

Admin Dashboard 在 index.php 中可以看到需要访问者是 admin 权限&#xff0c;才可以看到 flag。 report.php 中可以让 admin bot 访问我们输入的 url&#xff0c;那么也就是说可以访问 addadmin.php 添加用户。 在 addadmin.php 中可以添加 admin 用户&#xff0c;但是需…

李宏毅2023春季机器学习课程

目录2021&2022课程重磅须知我维护的其他项目更新日志课程地址课程资料直链课程作业直链其他优质课程2021&2022课程 CSDN Github 重磅须知 为方便所有网课资料与优质电子书籍的实时更新维护&#xff0c;创建一个在线实时网盘文件夹&#xff1b;   网盘获取方式&#…

mindspore的MLP模型(多层感知机)

导入模块 import hashlib import os import tarfile import zipfile import requests import numpy as np import pandas as pd import mindspore import mindspore.dataset as ds from mindspore import nn import mindspore.ops as ops import mindspore.numpy as mnp from …

Springdoc Swagger UI集成OAuth2认证

目录引言方式1&#xff1a;Bearer Token方式2&#xff1a;标准OAuth2授权码流程方式3&#xff1a;集成OIDC发现端点扩展&#xff1a;同时支持多种认证方式引言 之前的文章讲过OAuth2体系&#xff0c;以授权码流程为例&#xff08;参见下图&#xff09;&#xff0c; 其中资源服…

[SSD综述 1.3] SSD及固态存储技术半个世纪发展史

在我们今天看来&#xff0c;SSD已不再是个新鲜事物。这多亏了存储行业的前辈们却摸爬滚打了将近半个世纪&#xff0c;才有了SSD的繁荣&#xff0c; 可惜很多前辈都没有机会看到。所有重大的技术革新都是这样&#xff0c;需要长期的技术积累&#xff0c;一代一代的工程师们默默的…

基于springboot+vue的校园招聘系统

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战 主要内容&#xff1a;毕业设计(Javaweb项目|小程序等)、简历模板、学习资料、面试题库、技术咨询 文末联系获取 项目介绍…

vue3+rust个人博客建站日记2-确定需求

反思 有人说过我们正在临近代码的终结点。很快&#xff0c;代码就会自动产生出来&#xff0c;不需要再人工编写。程序员完全没用了&#xff0c;因为商务人士可以从规约直接生成程序。 扯淡&#xff01;我们永远抛不掉代码&#xff0c;因为代码呈现了需求的细节。在某些层面上&a…

【Python从入门到进阶】9、流程控制语句-条件语句(if-else)

接上篇《8、Python的输入输出》 上一篇我们学习了Python的输入和输出相关内容。本篇我们来学习Python的控制流语句。 一、流程控制语句的含义 之前我们分别学习过“变量及数据类型”、“运算符”&#xff0c;其中“变量及数据类型”相当于我们学习自然语言中的“字”&#xf…

一文让你彻底理解Linux内核调度器进程优先级

一、前言 本文主要描述的是进程优先级这个概念。从用户空间来看&#xff0c;进程优先级就是nice value和scheduling priority&#xff0c;对应到内核&#xff0c;有静态优先级、realtime优先级、归一化优先级和动态优先级等概念。我们希望能在第二章将这些相关的概念描述清楚。…

超算中心、并行计算

现在超算中心已经迅速发展 合肥&#xff1a; 合肥先进中心 合肥曙光超算中心平台 合肥安徽大学超算中心 合肥中科大超算中心 合肥中科院超算中心 合肥大一点的公司都会有自己的集群&#xff0c; 超算中心又称为集群&#xff0c;一般集群是小型服务器组成&#xff0c;超…

【软件测试】从功能到自动化测试,测试人的进阶之路细节,这些必不可少......

目录&#xff1a;导读前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09;前言 测试流程&#xff0…

RIP路由协议的更新(电子科技大学TCP/IP第二次实验)

一&#xff0e;实验目的 1、掌握 RIP 协议在路由更新时的发送信息和发送方式 2、掌握 RIP 协议的路由更新算法 二&#xff0e;预备知识 1、静态路由选择和动态路由选择 2、内部网关协议和外部网关协议 3、距离向量路由选择 三&#xff0e;实验原理 RIP 协议&#xff08…

【OC】块初识

Block简介 Blocks是C语言的扩充功能。可以用一句话来表示Blocks的扩充功能&#xff1a;带有自动变量的匿名函数。 匿名函数 所谓匿名函数就是不带有名称的函数。C语言的标准不允许存在这样的函数。例&#xff1a; int func(int count);它声明了名称为func的函数。下面的源代…

C++---线性dp---传纸条(每日一道算法2023.2.26)

注意事项&#xff1a; 本题dp思路与 “线性dp–方格取数” 一致&#xff0c;下方思路仅证明为什么使用方格取数的思路是正确的。 题目&#xff1a; 小渊和小轩是好朋友也是同班同学&#xff0c;他们在一起总有谈不完的话题。 一次素质拓展活动中&#xff0c;班上同学安排坐成…