当前位置: 首页 > news >正文

redis秒杀

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.lryc.cn/news/21020.html

相关文章:

  • JS学习第3天——Web APIs之DOM(什么是DOM,相关API【创建、增删改查、属性操作、事件操作API】)
  • 【MySQL】增删改操作(基础篇)
  • STM32—DMA
  • C语言刷题(3)——“C”
  • 搭建Vue工程
  • C语言汉诺塔问题【图文详解】
  • 1、RocketMQ概述
  • 【POJ 3352】Road Construction 题解(Tarjan算法求边双连通分量缩点)
  • Python—单分支结构
  • rabbitmq添加用户,虚拟机步,设置rabbitmq配置文件
  • Codeforces Round#853 div2 A-C
  • 软考之操作系统知识
  • 【线性代数/计算复杂性理论】积和式的指数时间算法:Ryser算法
  • 代码随想录 NO52 | 动态规划_leetcode 647. 回文子串 516.最长回文子序列
  • 【数据挖掘】1、综述:背景、数据的特征、数据挖掘的六大应用方向、有趣的案例
  • 【架构师】零基础到精通——康威定律
  • Could not extract response: no suitable HttpMessageConverter
  • 文献计量三大定律之一---洛特卡定律及普赖斯定律
  • 2023年软考高级网络规划设计师
  • 数据治理驱动因素 -报考题
  • 2023淘宝天猫38节红包满减优惠活动时间是从几月几号什么时候开始?
  • Hive表优化、表设计优化、Hive表数据优化(ORC)、数据压缩、存储优化
  • LearnOpenGL-入门-着色器
  • 【谷粒学院】vue、axios、element-ui、node.js(44~58)
  • 【一些回忆】2022.02.26-2023.02.26 一个普通男孩的365天
  • OSPF的多区域特性 (电子科技大学TCP/IP实验三)
  • (四十四)多个事务更新同一行数据时,是如何加锁避免脏写的?
  • 【数据库】第十二章 数据库管理
  • Redis源码---整体架构
  • 基于springboot+vue的校园招聘系统