目录

全局唯一ID生成

代码实现

①编写工具类

②编写测试类

添加优惠券-利用postman发送请求达成添加功能

VoucherOrderController

VoucherOrderServiceImpl

在jmeter中设置200个线程进行测试

设置请求头管理

超卖原因

乐观锁

CAS法

代码实现

一人一单

代码实现

执行了10次,解决方法,加悲观锁

解决一人一单的并发安全问题

分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁

基于redis的分布式锁

代码实现

优化-在释放锁前添加标识判断

代码改进

使用lua脚本保证判断锁标识和释放锁的原子性

代码实现


全局唯一ID生成

代码实现

①编写工具类

@Component
public class RedisIdWorker {/***开始时间戳*/private static final long BEGIN_TIMESTAMP = 1665619200L;/*** 序列号的为数*/private static final int COUNT_BITS = 32;private StringRedisTemplate stringRedisTemplate;public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}public long nextId(String keyPrefix){// 1.生成时间戳LocalDateTime now = LocalDateTime.now();long nowSecond = now.toEpochSecond(ZoneOffset.UTC);long timestamp = nowSecond - BEGIN_TIMESTAMP;//2.生成序列号//2.1获取当前日期,精确到天//①避免超过32位的上限;②方便统计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 ;//或运算}public static void main(String[] args) {LocalDateTime time = LocalDateTime.of(2022, 10, 13, 0, 0, 0);long second = time.toEpochSecond(ZoneOffset.UTC);System.out.println(second);}}

②编写测试类

@Resourceprivate CacheClient cacheClient;@Resourceprivate RedisIdWorker redisIdWorker;private ExecutorService es = Executors.newFixedThreadPool(500);@Testvoid 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);}latch.await();long end = System.currentTimeMillis();System.out.println("time"+(end-begin));}

添加优惠券-利用postman发送请求达成添加功能

VoucherOrderController

@RestController
@RequestMapping("/voucher-order")
public class VoucherOrderController {@Resourceprivate IVoucherOrderService voucherOrderService;@PostMapping("seckill/{id}")public Result seckillVoucher(@PathVariable("id") Long voucherId) {return voucherOrderService.seckillVoucher(voucherId);}
}

VoucherOrderServiceImpl

 @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.扣减库存boolean success = seckillVoucherService.update().setSql("stock = stock -1").eq("voucher_id",voucherId).update();//6.创建订单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);}

在jmeter中设置200个线程进行测试

设置请求头管理

authorization要根据项目网页信息进行查询

启动黑马点评项目,然后登录,进入系统后,按F12,选择 Network,选择 Header,就可以看到authorization

在数据库查看发现超卖了9个优惠券

超卖原因

乐观锁

CAS法

代码实现

@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.扣减库存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("库存不足!");}//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);}

一人一单

代码实现

public 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();//5.1查询订单int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();//5.2判断是否存在if (count > 0) {//用户已经购买过return Result.fail("用户已经购买过一次了!");}//6.扣减库存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);}

运行发现

执行了10次,解决方法,加悲观锁

①将一人一单判断单独作为一个方法,并添加事务

@Transactionalpublic  Result createVoucherOrder(Long voucherId) {//5.一人一单Long userId = UserHolder.getUser().getId();//5.1查询订单int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();//5.2判断是否存在if (count > 0) {//用户已经购买过return Result.fail("用户已经购买过一次了!");}//6.扣减库存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);}

②获取IVoucherOrderService的代理对象

IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();

③在pom文件中加入依赖

<dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId>
</dependency>
 @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("库存不足!");}Long userId = UserHolder.getUser().getId();synchronized(userId.toString().intern()) {//获取代理对象(事务)IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);}}@Transactionalpublic  Result createVoucherOrder(Long voucherId) {//5.一人一单Long userId = UserHolder.getUser().getId();//5.1查询订单int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();//5.2判断是否存在if (count > 0) {//用户已经购买过return Result.fail("用户已经购买过一次了!");}//6.扣减库存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);}

④刷新maven,重启项目,打开jmeter进行测试

异常99.5%,完成功能

解决一人一单的并发安全问题

在service中复制一份项目

修改端口信息

在postman中创建两个相同的下单请求,发送后发现再次出现了线程安全问题

原因:在不同的jvm中重新获取了锁

分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁

基于redis的分布式锁

代码实现

①实现ILock接口

②SimpleRedisLock

public class SimpleRedisLock implements ILock {private String name;private StringRedisTemplate stringRedisTemplate;private static final String KEY_PREFIX="lock:";public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name = name;this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean tryLock(long timeoutSec) {String key =KEY_PREFIX +name;//获取线程的标识long threadId = Thread.currentThread().getId();//获取锁, //set key value nx ex timeoutSecBoolean success = stringRedisTemplate.opsForValue().setIfAbsent(key, threadId + "", timeoutSec, TimeUnit.SECONDS);return Boolean.TRUE.equals(success); //避免空指针}@Overridepublic void unlock() {//释放锁stringRedisTemplate.delete(KEY_PREFIX + name);}}

③在VoucherOrderServiceImpl中实现->注入StringRedisTemplate->创建锁对象->获取锁->判断

是否获取锁成功->
获取锁失败,返回错误信息或重试
获取锁成功获取代理对象(事务)->释放锁
 @Resourceprivate StringRedisTemplate stringRedisTemplate;@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("库存不足!");}Long userId = UserHolder.getUser().getId();
//        synchronized(userId.toString().intern()) {//创建锁对象SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);//获取锁boolean isLock = lock.tryLock(1200);//判断是否获取锁成功if (!isLock) {//获取锁失败,返回错误信息或重试return Result.fail("不允许重复下单");}try {//获取代理对象(事务)IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);} finally {//释放锁lock.unlock();}}

在postman中发送两个post请求

发现只有一个获取锁成功 √

优化-在释放锁前添加标识判断

代码改进

SimpleRedisLock
    private static final String ID_PREFIX=UUID.randomUUID().toString(true)+"-";@Overridepublic boolean tryLock(long timeoutSec) {//获取线程的标识String threadId = ID_PREFIX + Thread.currentThread().getId();//获取锁, //set key value nx ex timeoutSecBoolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX +name, threadId, timeoutSec, TimeUnit.SECONDS);return Boolean.TRUE.equals(success); //避免空指针}@Overridepublic void unlock() {//获取线程标识String threadId = ID_PREFIX + Thread.currentThread().getId();//获取锁中的标识String lockId = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);//判断标识是否一致if(threadId.equals(lockId)) {//释放锁stringRedisTemplate.delete(KEY_PREFIX + name);}

保证判断锁标识和释放锁的原子性

代码实现

①idea中安装Emmylua插件

②在resources中创建unlock.lua

③编写lua脚本

④在SimpleRedisLock的unlock方法中调用lua脚本

    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 void unlock() {//调用lua脚本stringRedisTemplate.execute(UNLOCK_SCRIPT,Collections.singletonList(KEY_PREFIX +name),ID_PREFIX + Thread.currentThread().getId());}

使用Redisson优化代码

 插一句(经典白学)/(ㄒoㄒ)/~~++++++++++++++++++++++

Redisson入门

代码实现:

①创建RedissonConfig配置类

@Configuration
public class RedissonConfig {@Beanpublic RedissonClient redissonClient(){//配置Config config = new Config();config.useSingleServer().setAddress("redis://127.0.0.1:6379");//创建RedissonClinet对象return Redisson.create(config);}
}

②VoucherOrderServiceImpl实现类中的seckillVoucher修改创建锁对象方式

Redisson可重入锁原理

获取锁流程

释放锁原理

黑马点评-优惠券秒杀相关推荐

  1. 【Redis】Redis实战:黑马点评之优惠券秒杀

    Redis实战:黑马点评之优惠券秒杀 1 全局唯一ID 1.1全局唯一ID 每个店铺都可以发布优惠券: 当用户抢购时,就会生成订单并保存到tb_voucher_order这张表中,而订单表如果使用数据 ...

  2. 黑马点评--优惠卷秒杀

    黑马点评–优惠卷秒杀 全局ID生成器: 是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足下列特性: 为了增加ID的安全性,我们可以不直接使用Redis自增的数值,而是拼接一些其它信息: Re ...

  3. Redis解决优惠券秒杀

    虽然本文是针对黑马点评的优惠券秒杀业务的实现,但是是适用于各种抢购活动,保证线程安全. 摘要:本文先讲了抢购问题,指出其中会出现的多线程问题,提出解决方案采用悲观锁和乐观锁两种方式进行实现,然后发现在 ...

  4. Redis学习笔记②实战篇_黑马点评项目

    若文章内容或图片失效,请留言反馈.部分素材来自网络,若不小心影响到您的利益,请联系博主删除. 资料链接:https://pan.baidu.com/s/1189u6u4icQYHg_9_7ovWmA( ...

  5. 黑马点评项目全部功能实现及详细笔记--Redis练手项目

    目录 一.项目详情 1.1 项目简介 1.2 数据库表设计 1.3 前端部署 1.4 后端搭建 二.短信登录 2.1 发送验证码 2.2 验证码登录 2.3 登录校验拦截器 2.4 退出登录(补充) ...

  6. 个人项目总结-瑞吉外卖/传智健康/黑马点评

    1. 瑞吉外卖 瑞吉外卖技术栈:SpringBoot.MybatisPlus.springMVC 瑞吉外卖是我做的第一个项目,算是我做过所有的项目中最简单的,很适合新手入门,我当时是学完springb ...

  7. 菜鸟项目练习:黑马点评项目总结

    目录 1. 项目介绍 2.各个功能模块 2.1  登录模块 2.1.1 实现短信登录 2.1.2 编写拦截器 2.2 查询商户模块 2.2.1 主页面查询商户类型 2.2.3 按距离查询商户 2.3 ...

  8. springboot-redis-mysql-nginx项目:黑马点评开发(更新中)

    springboot-redis-mysql-nginx项目:黑马点评开发 开篇导读 亲爱的小伙伴们大家好,希望通过此博客,小伙伴们就能理解各种redis的使用啦. 短信登录 这一块我们会使用redi ...

  9. SpringBoot整合Redis实现优惠券秒杀服务(笔记+优化思路版)

    本文属于看黑马的redis的学习笔记,记录了思路和优化流程,精简版最终版请点击这里查看. 文章目录 一.全局ID生成器 1.1 理论 1.1.1 全局唯一ID生成策略 1.2 代码(Redis自增) ...

最新文章

  1. 有理想的程序员必须知道的15件事
  2. 【AI初识境】被Hinton,DeepMind和斯坦福嫌弃的池化,到底是什么?​​​​​​​
  3. HTML中form和div出现间隙以及页面居中的问题
  4. publiccms按照指定显示的日期格式,格式化日期的写法
  5. leetcode 231. 2 的幂
  6. C++ set insert的返回值
  7. 【C语言】三种方式不使用分号输出HelloWorld
  8. Django获取多个复选框的值,并插入对应表底下
  9. python 文本框位置_「每日一练」Python文本框的显示和插入
  10. 解决tex中参考文献出现[S.l.: s.n.]、[S.l.]、 [s.n.]问题
  11. exit()与_exit()函数的区别(Linux系统中)
  12. 3. tensorflow2实现两总体样本尺度、位置、分布检验问题 ——python实战
  13. 二维树状数组的区间加减及查询 tyvj 1716 上帝造题的七分钟
  14. cuda以及cudnn下载慢的问题解决!
  15. wineskin使用教程_使用Wineskin在Mac上运行Windows Apps /游戏
  16. java 代码练习题_99个java练习题及答案
  17. 学习笔记(32):Google开发专家带你学 AI:入门到实战(Keras/Tensorflow)(附源码)-模型微调(青出于蓝胜于蓝)...
  18. Python开发 CDN查询子域名查询
  19. 万洲金业:投资现货黄金的收益与风险如何平衡?
  20. 华芯通关闭,华为等国产服务器芯片企业再受打击

热门文章

  1. linux用户行为审计
  2. CSS设置背景颜色透明的两种方法
  3. html+页面的背景透明,css设置背景透明 元素不透明
  4. (Endless)Scroll View(画卷滚动视图)
  5. DHCP(漫画图文详解)
  6. php表单输入内容换行,php中表单输入框中换行回车替换
  7. 快速寻找文件:locate命令
  8. 常用linux环境配置大全
  9. 过敏性皮肤、春天皮肤过敏、皮肤过敏--皮肤过敏怎么办?
  10. 斗地主拿到双王的概率问题