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

#Redis黑马点评#(四)优惠券秒杀

目录

一 生成全局id

二 添加优惠券

三 实现秒杀下单

方案一(会出现超卖问题)

方案二(解决了超卖但是错误率较高)

方案三(解决了错误率较高和超卖但是会出现一人抢多张问题)

方案四(解决一人抢多张问题“非分布式情况”)

方案五(实现一人一单,跨JVM锁的实现“分布式情况”)

最终解决方案:Lua脚本(解决原子性)


一 生成全局id

策略:

  • 每天一个Key值,方便统计订单量
  • ID构造为:时间戳+计数器

代码实现:

package com.hmdp.utils;import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;@Component
public class RedisIdWorker {// 序列号位数private static final int COUNT_BITS = 32;private final StringRedisTemplate stringRedisTemplate;public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}/*** 开始时间戳*/private static final long BEGIN_TIMESTAMP = 1640995200;/*** 序列号的位数*/public long nextId(String keyPrefix) {// 1.生成时间戳LocalDateTime now = LocalDateTime.now();long nowSecond = now.toEpochSecond(ZoneOffset.UTC);long timestamp = nowSecond - BEGIN_TIMESTAMP;// 2.生成序列号// 2.1获取当前日期,精确到天String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));// 2.2自增长Long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);// 3.拼接并返回return timestamp << COUNT_BITS | count;}
}

核心思路:

Redis的角色:分布式计数器->其不存储订单号,仅提供原子递增的序列号。

订单号的本质:由时间戳(高32位)和序列号位(低32位)运算生成的Long型数值。

1 按照一定的规则来设置key键值。

String key = "icr:" + keyPrefix + ":" + date;
  • icr:代表自增(区分键值种类)
  • keyPrefix:业务标识(区分业务种类)
  • date:当前日期(实现按天区分避免单个键值过大)

2 向redis数据库当中添加数据。

Long count = stringRedisTemplate.opsForValue().increment(key);

Redis的INCR是一个原子操作,对指定键的值执行加1。

若键不存在,就先初始化为0,后续再次执行的时候就会加1,可在Redis当中统计数量,

同时,订单号则作为返回的值。

二 添加优惠券

策略:

存在两个表,一个是普通优惠券的表tb_voucher,一个是秒杀优惠券的表tb_voucher_order。

但是秒杀优惠券的表是建立在普通优惠券的基础之上的。有些共有属性存储在普通优惠表(里面同时也存储一个type类型0/1用于区分是否是优惠券),在秒杀优惠券的表当中存储券开启的时间,结束时间,张数这些核心参数。

代码:

controller控制层的业务实现

    /*** 新增普通券* @param voucher 优惠券信息* @return 优惠券id*/@PostMappingpublic Result addVoucher(@RequestBody Voucher voucher) {voucherService.save(voucher);return Result.ok(voucher.getId());}

下面是实体类的形式(一些特有属性不做强制要求)

package com.hmdp.entity;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;import java.io.Serializable;
import java.time.LocalDateTime;/*** <p>* * </p>** @author 虎哥* @since 2021-12-22*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("tb_voucher")
public class Voucher implements Serializable {private static final long serialVersionUID = 1L;/*** 主键*/@TableId(value = "id", type = IdType.AUTO)private Long id;/*** 商铺id*/private Long shopId;/*** 代金券标题*/private String title;/*** 副标题*/private String subTitle;/*** 使用规则*/private String rules;/*** 支付金额*/private Long payValue;/*** 抵扣金额*/private Long actualValue;/*** 优惠券类型*/private Integer type;/*** 优惠券类型*/private Integer status;/*** 库存*/@TableField(exist = false)private Integer stock;/*** 生效时间*/@TableField(exist = false)private LocalDateTime beginTime;/*** 失效时间*/@TableField(exist = false)private LocalDateTime endTime;/*** 创建时间*/private LocalDateTime createTime;/*** 更新时间*/private LocalDateTime updateTime;}

三 实现秒杀下单

下单之前首先需要判断两点:

  • 时间是否开始,如果尚未开始或者已经结束则无法下单。
  • 库存是否充足,不足则无法下单。

方案一(会出现超卖问题)

VoucherOrderController控制层

    @PostMapping("seckill/{id}")public Result seckillVoucher(@PathVariable("id") Long voucherId) {return voucherOrderService.seckillVoucher(voucherId);}

Service业务层(业务层接口)

    /*** 秒杀优惠券* @param voucherId 优惠券id* @return 结果*/Result seckillVoucher(Long voucherId);

Service业务层(业务层实现类)

    /*** 秒杀优惠券** @param voucherId 优惠券id* @return*/@Transactional@Overridepublic Result seckillVoucher(Long voucherId) {// 1查询优惠券SeckillVoucher byId = seckillVoucherService.getById(voucherId);// 2判断时间范围if (byId.getBeginTime().isAfter(LocalDateTime.now())) {return Result.fail("秒杀尚未开始");}if (byId.getEndTime().isBefore(LocalDateTime.now())) {return Result.fail("秒杀已经结束");}// 3判断库存if (byId.getStock() < 1) {return Result.fail("库存不足");}// 4扣减库存boolean update = seckillVoucherService.update().set("stock", byId.getStock() - 1).eq("voucher_id", voucherId).update();if (!update) {return Result.fail("库存不足");}// 5生成订单VoucherOrder voucherOrder = new VoucherOrder();// 6订单idLong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);// 7用户idvoucherOrder.setUserId(1L);// 8优惠券idvoucherOrder.setVoucherId(voucherId);//  9保存订单save(voucherOrder);// 10返回订单idreturn Result.ok(orderId);}

超卖问题的解决

乐观锁的关键是判断之前查询得到的数据是否有被修改过,常见的方式有两种:

1 版本号法

2 CAS

代码实现:(CAS)

方案二(解决了超卖但是错误率较高)

乐观锁的 WHERE stock=原库存-1 条件在高并发下导致大量冲突。

核心修改:在修改之前判断库存与刚开始查询到的数据是否相同,但是会出现错误率较高,导致很多人提前抢到但是别人修改了,再次校验stock时,出现错误,就无法抢购成功。(出现有前一百个人抢但是他们并没有得到这些优惠券)

        boolean update = seckillVoucherService.update().set("stock", byId.getStock() - 1)// set stock=stock-1(更新操作的条件).eq("voucher_id", voucherId)// where voucher_id=? and stock=?(判断秒杀券的id).eq("stock", byId.getStock())// where stock=?(判断票数是否与刚开始查询到的相同).update();

方案三(解决了错误率较高和超卖但是会出现一人抢多张问题)

核心修改:在修改时判断库存是否还是>0即可(出现类似黄牛使用脚本同时发送请求将优惠券抢完,破坏了一人一单的规则)

        boolean update = seckillVoucherService.update().setSql("stock=stock-1")// set stock=stock-1(更新操作的条件).eq("voucher_id", voucherId)// where voucher_id=? and stock=?(判断秒杀券的id).gt("stock", 0)//  where stock>0(判断修改时券是否>0).update();

方案四(解决一人抢多张问题“非分布式情况”)

bug版(会出现一个用户开多个线程并发的查询操作,出现查询的都是0,导致都去抢购订单并抢购成功,导致一个用户购买多次)

        //实现一人一单Long userId = UserHolder.getUser().getId();if (query().eq("user_id", userId).eq("voucher_id", voucherId).count() > 0) {return Result.fail("用户已经购买过");}

改进版

实现代理,将购买订单加上锁。(分布式情况就会出现错误)

  • synchronized是基于JVM的内存锁,确保同意用户ID的请求在单机内串行执行。
  • userId.toString().intern()保证相同用户ID的字符串对象唯一避免锁失效。
  • AopContext.currentProxy()确保@Transactional事务注解生效避免事务失效问题。
        Long userId = UserHolder.getUser().getId();synchronized (userId.toString().intern()){IVoucherOrderService proxy = (IVoucherOrderService)AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);}

方案五(实现一人一单,跨JVM锁的实现“分布式情况”)

实现原理

满足分布式系统或集群模式下多线程可见并且互斥/

  • 多进程可见
  • 互斥
  • 高可用
  • 高性能
  • 安全性

分布式锁的实现

分布式锁的核心是实现多进程之间互斥,而满足这一点的方式有很多,常见的有三种

我们需要实现的就是一个用户名id只能抢购一个优惠券的目的。

我们先定义一个锁工具接口

package com.hmdp.utils;import org.springframework.stereotype.Component;public interface ILock {/*** 尝试获取锁** @param timeoutSec 锁持有的超时时间,过期自动释放* @return true代表获取锁成功,false代表获取锁失败*/boolean tryLock(Long timeoutSec);/*** 释放锁*/void unLock();
}

再在实现类当中完善相关方法

我们是根据lock前缀以及用户名来写入锁的名称,以到达区分效果,不同的JVM当中的线程读取时达到互斥效果。

package com.hmdp.utils;import lombok.Data;
import org.springframework.data.redis.core.StringRedisTemplate;import java.util.concurrent.TimeUnit;@Data
public class SimpleRedisLock implements ILock {private StringRedisTemplate stringRedisTemplate;private String name;private static final String KEY_PREFIX = "lock:";public SimpleRedisLock(String s, StringRedisTemplate stringRedisTemplate) {this.name = s;this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean tryLock(Long timeoutSec) {//获取线程标识和锁String key = KEY_PREFIX + name;long threadId = Thread.currentThread().getId();String value = threadId + "";//获取锁Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(key, value, timeoutSec, TimeUnit.SECONDS);return Boolean.TRUE.equals(success);}@Overridepublic void unLock() {//释放锁stringRedisTemplate.delete(KEY_PREFIX + name);}
}

存在的问题:

防止误删(加一个线程标识进行校验,设置特定的value值用于校验setnx是基于key的)

加一个判断是否是自己的锁(是自己的才删)

代码实现:

根据JVM的id-key值与当前线程的UUID线程标识-value进行区分获取当前线程的身份,解决线程误删操作。

package com.hmdp.utils;import lombok.Data;
import org.springframework.data.redis.core.StringRedisTemplate;import java.util.UUID;
import java.util.concurrent.TimeUnit;@Data
public class SimpleRedisLock implements ILock {private StringRedisTemplate stringRedisTemplate;private String name;private static final String KEY_PREFIX = "lock:";private static final String ID_PREFIX = UUID.randomUUID() + "-";public SimpleRedisLock(String s, StringRedisTemplate stringRedisTemplate) {this.name = s;this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean tryLock(Long timeoutSec) {//获取线程标识和锁String key = KEY_PREFIX + name;//key值String value = ID_PREFIX + Thread.currentThread().getId();//value值//获取锁Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(key, value, timeoutSec, TimeUnit.SECONDS);return Boolean.TRUE.equals(success);}@Overridepublic void unLock() {//获取线程标识String value = ID_PREFIX + Thread.currentThread().getId();//value值//获取锁中的标识String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);if (value.equals(id)) {stringRedisTemplate.delete(KEY_PREFIX + name);}}
}

存在的问题:

防止误删,如果在判断结束后出现了阻塞情况,导致时间达到了TTL时间,其他的线程进入锁依然会被误删,那被误删的线程就会没有锁,导致其他的线程进入抢券,引发线程并发问题)

这里的判断与释放分成了两部分,非原子性操作

get(校验锁归属)和 delete(释放锁)是独立操作,期间锁可能过期并被其他线程获取。

最终解决方案:Lua脚本(解决原子性)

Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。Lua是一种编程语言,他的基本语法可以参考:Lua 教程 | 菜鸟教程

基于Redis的分布式锁

释放锁的业务流程是这样的:

  • 1 获取锁中的线程标识。
  • 2 判断是否与指定的标识一致。
  • 3 如果一致则释放锁。
  • 4 如果不一致则什么都不做。

代码实现:

Lua脚本的编写

--比较线程标识与所种的标识是否一致
if (redis.call('get', KEYS[1]) == ARGV[1]) then--释放锁return redis.call('del', KEYS[1])
end
return 0

调用代码的改进:

package com.hmdp.utils;import lombok.Data;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;@Data
public class SimpleRedisLock implements ILock {private static final String KEY_PREFIX = "lock:";private StringRedisTemplate stringRedisTemplate;private String name;private final String ID_PREFIX;public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name = name;this.stringRedisTemplate = stringRedisTemplate;this.ID_PREFIX = UUID.randomUUID() + "-";}// 释放锁的脚本(static初始化避免多次读取,这样可以优化性能)private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;static {UNLOCK_SCRIPT = new DefaultRedisScript<>();UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));UNLOCK_SCRIPT.setResultType(Long.class);}@Overridepublic boolean tryLock(Long timeoutSec) {//获取线程标识和锁String key = KEY_PREFIX + name;//key值String value = ID_PREFIX + Thread.currentThread().getId();//value值//获取锁Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(key, value, timeoutSec, TimeUnit.SECONDS);return Boolean.TRUE.equals(success);}@Overridepublic void unLock() {//调用Lua脚本stringRedisTemplate.execute(UNLOCK_SCRIPT, Collections.singletonList(KEY_PREFIX + name), ID_PREFIX + Thread.currentThread().getId());}//    @Override
//    public void unLock() {
//        //获取线程标识
//        String value = ID_PREFIX + Thread.currentThread().getId();//value值
//        //获取锁中的标识
//        String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
//        if (value.equals(id)) {
//            stringRedisTemplate.delete(KEY_PREFIX + name);
//        }
//    }
}

Service接口的代码实现展示

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.SimpleRedisLock;
import com.hmdp.utils.UserHolder;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import java.time.LocalDateTime;@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Autowiredprivate ISeckillVoucherService seckillVoucherService;@Autowiredprivate RedisIdWorker redisIdWorker;@Autowiredprivate StringRedisTemplate stringRedisTemplate;/*** 秒杀优惠券** @param voucherId 优惠券id* @return*/@Overridepublic Result seckillVoucher(Long voucherId) {// 1查询优惠券SeckillVoucher byId = seckillVoucherService.getById(voucherId);// 2判断时间范围if (byId.getBeginTime().isAfter(LocalDateTime.now())) {return Result.fail("秒杀尚未开始");}if (byId.getEndTime().isBefore(LocalDateTime.now())) {return Result.fail("秒杀已经结束");}// 3判断库存if (byId.getStock() < 1) {return Result.fail("库存不足");}// 4实现一人一单Long userId = UserHolder.getUser().getId();//创建锁对象SimpleRedisLock simpleRedisLock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);//获取锁boolean isLock = simpleRedisLock.tryLock(1000L);//判断是否成功if (!isLock) {//获取锁失败return Result.fail("请勿重复下单");}try {//获取锁成功,开始创建订单IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);} finally {//释放锁simpleRedisLock.unLock();}}@Transactionalpublic Result createVoucherOrder(Long voucherId) {//实现一人一单Long userId = UserHolder.getUser().getId();if (query().eq("user_id", userId).eq("voucher_id", voucherId).count() > 0) {return Result.fail("用户已经购买过");}// 4扣减库存boolean update = seckillVoucherService.update().setSql("stock=stock-1")// set stock=stock-1(更新操作的条件).eq("voucher_id", voucherId)// where voucher_id=? and stock=?(判断秒杀券的id).gt("stock", 0)//  where stock>0(判断修改时券是否>0).update();if (!update) {return Result.fail("库存不足");}// 5生成订单VoucherOrder voucherOrder = new VoucherOrder();// 6订单idLong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);// 7用户idvoucherOrder.setUserId(userId);// 8优惠券idvoucherOrder.setVoucherId(voucherId);//  9保存订单save(voucherOrder);// 10返回订单idreturn Result.ok(orderId);}
}

http://www.xdnf.cn/news/5300.html

相关文章:

  • Fabric系列 - SoftHSM 软件模拟HSM
  • 前端SSE技术详解:从入门到实战的完整指南
  • C++泛型编程(二):现代C++特性
  • 常见的降维算法
  • 采用SqlSugarClient创建数据库实例引发的异步调用问题
  • 【Qt/C++】深入理解 Lambda 表达式与 `mutable` 关键字的使用
  • MySQL的视图
  • AI 助力,轻松进行双语学术论文翻译!
  • C++GO语言微服务之gorm框架操作MySQL
  • uniapp使用ui.request 请求流式输出
  • LLaVA:开源多模态大语言模型深度解析
  • 物品识别 树莓派4 YOLO v11
  • 青少年编程与数学 02-019 Rust 编程基础 05课题、复合数据类型
  • 解锁 DevOps 新境界 :使用 Flux 进行 GitOps 现场演示 – 自动化您的 Kubernetes 部署
  • 大模型(LLMs)强化学习——RLHF及其变种
  • 基于强化学习 Q-learning 算法求解城市场景下无人机三维路径规划研究,提供完整MATLAB代码
  • linux测试硬盘读写速度
  • uniapp|实现商品分类与列表数据联动,左侧菜单右侧商品列表(瀑布流、高度自动计算、多端兼容)
  • 音频类网站或者资讯总结
  • 电子电器架构 --- 车载以太网拓扑
  • OSPF的四种特殊区域(Stub、Totally Stub、NSSA、Totally NSSA)详解
  • PyTorch 线性回归模型构建与神经网络基础要点解析
  • 数据结构精解:优先队列、哈希表与树结构
  • AI 入门资源:微软 AI-For-Beginners 项目指南
  • Kotlin 协程 vs RxJava vs 线程池:性能与场景对比
  • 【论文阅读】Efficient and secure federated learning against backdoor attacks
  • MySQL 索引(一)
  • 【C++ Qt】容器类(GroupBox、TabWidget)内附思维导图 通俗易懂
  • 发行基础:本地化BUG导致审核失败
  • 动态规划:最长递增子序列