微服务 Spring Boot 整合Redis 实现优惠卷秒杀 一人一单_springboot 同步多商店优惠券

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新大数据全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip204888 (备注大数据)
img

正文

*/
private static final long BEGIN_TIMESTAMP = 1640995200L;
/**
* 序列号的位数
*/
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.获取当前日期,精确到天
    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**
> 
> 
> 



@Autowired
private RedisIdWorker redisIdWorker;

private ExecutorService es = Executors.newFixedThreadPool(500);

@Test
public void testWorkerId() 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("times = " + (end- begin));

}


**这里用到了 CountDownlatch,简单的介绍一下:**


**CountDownLatch**名为信号枪:主要的作用是**同步协调在多线程的等待于唤醒问题**


我们如果没有CountDownLatch ,那么由于程序是异步的,当异步程序没有执行完时,主线程就已经执行完了,然后我们期望的是分线程全部走完之后,主线程再走,所以我们此时需要使用到CountDownLatch


CountDownLatch 中有两个最重要的方法


* countDown
* await


**await 是阻塞方法,**我们担心线程没有执行完时,main线程就执行,所以可以**使用await就阻塞主线程**, 那么什么时候main线程不在阻塞呢? **当 CountDownLatch 内部维护的变量为0时,就不再阻塞,直接放行**。


什么时候 **CountDownLatch 维护的变量变为0 呢,我们只需要调用一次countDown ,内部变量就减少1**,我们让分线程和变量绑定, 执行完一个分线程就减少一个变量,当分线程全部走完,CountDownLatch 维护的变量就是0,此时await就不再阻塞,统计出来的时间也就是所有分线程执行完后的时间。


## 二、环境准备


**需要搭建登录环境,`基础环境代码和sql文件`均已上传 GitCode 链接:[基础环境和SQL]( )**


## 三、实现秒杀下单



> 
> **添加优惠卷**
> 
> 
> 


**VoucherServiceImpl 核心代码**



@Service
public class VoucherServiceImpl extends ServiceImpl<VoucherMapper, Voucher> implements IVoucherService {

// 该类无代码,直接MyBatis-Plus继承实现类 即可,自动完成持久化
@Autowired
private ISeckillVoucherService seckillVoucherService;

@Override
public ResultBean<List<Voucher>> queryVoucherOfShop(Long shopId) {
    // 查询优惠券信息
    List<Voucher> vouchers = getBaseMapper().queryVoucherOfShop(shopId);
    // 返回结果
    return ResultBean.create(0, "success", vouchers);
}

@Override
public void addSeckillVoucher(Voucher voucher) {
    // 保存优惠券
    save(voucher);
    // 保存秒杀信息
    SeckillVoucher seckillVoucher = new SeckillVoucher();
    seckillVoucher.setVoucherId(voucher.getId());
    seckillVoucher.setStock(voucher.getStock());
    seckillVoucher.setBeginTime(voucher.getBeginTime());
    seckillVoucher.setEndTime(voucher.getEndTime());
    seckillVoucherService.save(seckillVoucher);
}

}


**VoucherController 接口层**



@RestController
@CrossOrigin
@RequestMapping(“/voucher”)
public class VoucherController {

@Autowired
private IVoucherService voucherService;

/\*\*

* 新增秒杀券
* @param voucher 优惠券信息,包含秒杀信息
* @return 优惠券id
*/
@PostMapping(“seckill”)
public ResultBean addSeckillVoucher(@RequestBody Voucher voucher) {
voucherService.addSeckillVoucher(voucher);
return Result.ok(voucher.getId());
}
}



> 
> **编写下单业务**
> 
> 
> 


**VoucherOrderServiceImpl 优惠卷订单核心业务类**



@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

@Autowired
private ISeckillVoucherService seckillVoucherService;

@Autowired
private RedisIdWorker redisIdWorker;

@Override
@Transactional
public Result seckillVoucher(Long voucherId) {
    //1. 查询优惠卷
    SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
    //2. 判断秒杀是否开始 开始时间大于当前时间表示未开始抢购
    if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
        return Result.fail("秒杀尚未开始!");
    }
    //3. 判断秒杀是否结束
    if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
        return Result.fail("秒杀已经结束!");
    }
    //4. 判断库存是否充足
    if (seckillVoucher.getStock() < 1) {
        return Result.fail("库存不足!");
    }

    Long userId = UserHolder.getUser().getId();
    //5. 查询订单
    //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")
            .eq("voucher\_id", voucherId).update();
    if (!success) {
        return Result.fail("库存不足!");
    }

    //7. 创建订单
    VoucherOrder voucherOrder = new VoucherOrder();
    long orderId = redisIdWorker.nextId("order");
    voucherOrder.setId(orderId);
    voucherOrder.setUserId(userId);
    voucherOrder.setVoucherId(voucherId);
    save(voucherOrder);
    //8. 返回订单id
    return Result.ok(orderId);
}

}


**VoucherOrderController 接口层**



@RestController
@CrossOrigin
@RequestMapping(“/voucher_order”)
public class VoucherOrderController {

@Autowired
private IVoucherOrderService voucherOrderService;

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

}



> 
> **测试抢购秒杀优惠卷**
> 
> 
> 



> 
> **ApiFox 新增以下接口**
> 
> 
> 


**添加秒杀卷**


![在这里插入图片描述](https://img-blog.csdnimg.cn/edb6602eafbb4d46802767ffbf381b93.png#pic_center)


**测试返回成功即可。**


**抢购秒杀优惠卷接口**


![在这里插入图片描述](https://img-blog.csdnimg.cn/51f3f7c487314ec2b3f5c3f526987009.png#pic_center)


**测试无误,抢购成功!**


## 四、库存超卖问题


### ⏳问题分析


有关超卖问题分析:在我们原有代码中是这么写的



if (voucher.getStock() < 1) {
// 库存不足
return Result.fail(“库存不足!”);
}
//5,扣减库存
boolean success = seckillVoucherService.update()
.setSql(“stock= stock -1”)
.eq(“voucher_id”, voucherId).update();
if (!success) {
//扣减库存
return Result.fail(“库存不足!”);
}


假设线程1过来查询库存,判断出来库存大于1,正准备去扣减库存,但是还没有来得及去扣减,此时线程2过来,线程2也去查询库存,发现这个数量一定也大于1,那么这两个线程都会去扣减库存,最终多个线程相当于一起去扣减库存,此时就会出现库存的超卖问题。


![在这里插入图片描述](https://img-blog.csdnimg.cn/8365d26021ce4721a7c3719f440b744e.png#pic_center)


**超卖问题是典型的多线程安全问题,** 这种情况下**常见的解决方案就是 加 锁**:而对于加锁,我们**通常有两种解决方案**:


![在这里插入图片描述](https://img-blog.csdnimg.cn/91f627935f114893a6a45efdf162b8c5.png#pic_center)


**悲观锁:**


悲观锁**可以实现对于数据的串行化执行**,比如syn,和lock都是悲观锁的代表,同时,**悲观锁中又可以再细分为公平锁,非公平锁,可重入锁,等等**


**乐观锁:**


**会有一个版本号,每次操作数据会对版本号+1,再提交回数据时,会去校验是否比之前的版本大1 ,如果大1 ,则进行操作成功**,这套机制的核心逻辑在于,\*\*如果在操作过程中,版本号只比原来大1 ,那么就意味着操作过程中没有人对他进行过修改,他的操作就是安全的,\*\*如果不大1,则数据被修改过,当然乐观锁还有一些变种的处理方式比如cas


乐观锁的典型代表:就是**CAS**,利用CAS**进行无锁化机制加锁**,varNum是操作前读取的内存值,while中的var1+var2 是预估值,如果预估值 == 内存值,则代表中间没有被人修改过,此时就将新值去替换 内存值


其中do while 是为了在操作失败时,再次进行自旋操作,即把之前的逻辑再操作一次。



int varNum;
do {
varNum = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

return var5;


我们**采用的方式为:**


在操作时,**对版本号进行+1 操作,然后要求version 如果是1 的情况下,才能操作**,那么第一个线程在操作后,数据库中的version变成了2,但是他自己满足version=1 ,所以没有问题,此时线程2执行,**线程2 最后也需要加上条件version =1 ,但是现在由于线程1已经操作过了,所以线程2,操作时就不满足version=1 的条件了,所以线程2无法执行成功**


![在这里插入图片描述](https://img-blog.csdnimg.cn/4295db7b1b4a4dc5a621507a65f61961.png#pic_center)


### ⌚ 乐观锁解决库存超卖


**加入以下代码解决超卖问题**


之前的方式要修改前后都保持一致,但是这样我们分析过,成功的概率太低,所以我们的乐观锁需要变一下,改成stock大于0 即可



boolean success = seckillVoucherService.update()
.setSql(“stock= stock -1”)
.eq(“voucher_id”, voucherId).update().gt(“stock”,0); //where id = ? and stock > 0


**知识拓展**


针对**CAS中的自旋压力过大,我们可以使用Longaddr这个类去解决**


Java8 **提供的一个对AtomicLong改进后的一个类,LongAdder**


**大量线程并发更新一个原子性的时候,天然的问题就是自旋**,会导致并发性问题,当然这也比我们直接使用syn来的好


所以利用这么一个类,LongAdder来进行优化


如果获取某个值,**则会对cell和base的值进行递增,最后返回一个完整的值**


![在这里插入图片描述](https://img-blog.csdnimg.cn/206944222f9541f491c5538c6d289896.png#pic_center)


**以上的解决方式,依然有些问题,下面使用Jmeter进行测试**


### ✅Jmeter 测试


**添加线程组**


![在这里插入图片描述](https://img-blog.csdnimg.cn/900592dfc72c41089c0bee153bc4d8b0.png#pic_center)


**添加JSON断言,我们认为返回结果为false的就是请求失败**


在线程组右击选择断言 --> JSON 断言


![在这里插入图片描述](https://img-blog.csdnimg.cn/031068a41d4c4567a99290f8c2de24ab.png#pic_center)



**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注大数据)**
![img](https://img-blog.csdnimg.cn/img_convert/c6888b67741d653635a7aae2ebc433e6.png)

**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

c41089c0bee153bc4d8b0.png#pic_center)


**添加JSON断言,我们认为返回结果为false的就是请求失败**


在线程组右击选择断言 --> JSON 断言


![在这里插入图片描述](https://img-blog.csdnimg.cn/031068a41d4c4567a99290f8c2de24ab.png#pic_center)



**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注大数据)**
[外链图片转存中...(img-nStDQU4w-1713402347132)]

**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
微服务架构是一种新兴的软件架构体系,它将一个大型应用程序分解为多个独立的服务,每个服务都提供一定的功能,服务之间通过轻量级的通信机制协同工作,构成一个松散耦合的系统。 而 Spring Boot 是一个快速创建基于 Spring 的应用程序的框架。它采用约定大于配置的方式,简化了 Spring 应用程序的开发,同时提高了应用程序的可维护性。 Redis 是一种高速缓存数据库,具有高并发和低延迟的特点。它能够在存储和读取大规模数据时提供快速的性能和可靠的持久性。 秒杀是指在限定时间内,将大量的请求按照系统预设的规则进行处理,从而实现购买或者抢购等活动。在传统的单机架构中,秒杀的高并发场景经常会导致系统崩溃或者响应缓慢,因此需要采用新的技术来解决这个问题。 将微服务Spring BootRedis 结合起来,可以有效地解决秒杀系统的高并发问题。采用微服务架构,可以将每个服务拆分为独立的功能,提高系统的可扩展性和可维护性。使用 Spring Boot 框架,则可以快速搭建服务,并利用它的依赖注入和 AOP 等特性,增加代码的复用性和可维护性。而 Redis 则可以作为高速缓存数据库,提高系统的响应速度和可靠性。 在秒杀场景中,可以将商品和库存信息缓存在 Redis 中,同时采用消息队列来控制请求的流量,避免系统瞬时崩溃。在秒杀活动开始前,将商品信息和库存信息加载到 Redis 中,由 Redis 进行管理,并在秒杀结束后将结果写入数据库中。通过这种方式,可以提高系统的吞吐量和可靠性,同时保证秒杀活动的公平性和可靠性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值