Redis优惠券秒杀 | 黑马点评
创始人
2024-05-14 16:28:00
0

目录

一、全局唯一ID

1、全局ID生成器

二、实现秒杀下单

1、基本的下单功能

2、超卖问题

3、乐观锁解决并发问题

三、实现一人一单

1、思路分析

2、代码初步实现 

3、关于锁的范围 

4、关于事务失效

5、集群下线程并发问题


一、全局唯一ID

订单如果用自增长会存在的问题:

ID的规律性太明显了

受单表数量限制,因为如果商城很大订单表数量可能很多,要分库分表,到时候id自增从1开始的话肯定会出现重复的。订单表为了后边方便查询肯定不能重复

1、全局ID生成器

全局id生成器,是一种分布式系统下用来生成全局唯一ID的工具,满足下列特征:

  • 唯一性
  • 高可用
  • 高性能(生成足够快)
  • 递增性(整体递增,方便创建索引)
  • 安全性(规律性不能太明显)

Redis肯定唯一的,性能也高,Redis也是采用递增方案的

生成器代码(Redis自增ID策略):

在最后做拼接的时候,我们不能直接拼接,因为是long类型来接收所以我们得用位运算,前面的左移动32位然后或运算后面的

key的设置是每天一个key,方便订单统计也防止可能会重复

import org.springframework.beans.factory.annotation.Autowired;
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 long BEGIN_TIMESTAMP = 1640995200L;private static int COUNT_BITS = 32;@Autowiredprivate StringRedisTemplate stringRedisTemplate;public Long nextId(String keyPrefix){LocalDateTime now = LocalDateTime.now();long nowSecond = now.toEpochSecond(ZoneOffset.UTC);long time = nowSecond - BEGIN_TIMESTAMP;String format = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));// Redis Incrby 命令将 key 中储存的数字加上指定的增量值。// 如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCRBY 命令。Long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + format);return time << COUNT_BITS | count;}public static void main(String[] args) {LocalDateTime of = LocalDateTime.of(2022, 1, 1, 0, 0, 0);long l = of.toEpochSecond(ZoneOffset.UTC);// LocalTime类的toEpochSecond()方法用于// 将此LocalTime转换为自1970-01-01T00:00:00Z以来的秒数System.out.println(l);}
}

二、实现秒杀下单

1、基本的下单功能

下单时需要满足两点:

  • 秒杀是否开始或结束,如果没开始或已结束则无法下单
  • 库存是否充足,不足则无法下单

实现代码

@Service
public class VoucherOrderServiceImpl extends ServiceImpl implements IVoucherOrderService {@Autowiredprivate ISeckillVoucherService seckillVoucherService;@Autowiredprivate RedisIdWorker redisIdWorker;@Override@Transactionalpublic Result seckillVoucher(Long voucherId) {SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);// 判断秒杀是否还未开始if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {Result.fail("秒杀尚未开始!");}// 判断秒杀是否已经结束if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {Result.fail("秒杀已经结束!");}// 判断库存是否充足if (seckillVoucher.getStock() < 1) {Result.fail("库存不足!");}// 扣减库存boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).update();// 扣减失败if(!success){return Result.fail("库存不足!");}// 创建订单VoucherOrder voucherOrder = new VoucherOrder();// 生成订单 idLong orderId = redisIdWorker.nextId("order");voucherOrder.setVoucherId(voucherId);// 用户 idLong userId = UserHolder.getUser().getId();voucherOrder.setUserId(userId);voucherOrder.setId(orderId);save(voucherOrder);return Result.ok(orderId);}
}

记得方法加上事务注解,一旦出问题可以回滚。

2、超卖问题

当线程1在查到还有1个库存,然后开始扣除的时候,在还没扣除完毕时,这个时候有其他线程看到还有1个库存,都会进行扣除,这种情况就会存在超卖问题了。 

针对这一问题常见解决方案就是加锁,常见有乐观锁和悲观锁

乐观锁

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

版本号法(数据库多一个version来标记是否已经修改)

CAS法(除了多的字段,版本号信息,以库存信息本身有没有变化为判断依据,当线程修改库存时,当线程修改库存时,判断当前数据库中的库存与之前查询得到的库存数据是否一致,如果一致,则说明线程安全,可以执行扣减操作,如果不一致,则说明线程不安全,扣减失败。)

3、乐观锁解决并发问题

我们只需要在修改库存表前判断一下,跟之前查到的值是否相等就行

@Service
public class VoucherOrderServiceImpl extends ServiceImpl implements IVoucherOrderService {@Autowiredprivate ISeckillVoucherService seckillVoucherService;@Autowiredprivate RedisIdWorker redisIdWorker;@Override@Transactionalpublic Result seckillVoucher(Long voucherId) {SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);// 判断秒杀是否还未开始if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {Result.fail("秒杀尚未开始!");}// 判断秒杀是否已经结束if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {Result.fail("秒杀已经结束!");}// 判断库存是否充足if (seckillVoucher.getStock() < 1) {Result.fail("库存不足!");}// 扣减库存boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).eq("stock", seckillVoucher.getStock()).    // 增加对库存的判断,判断当前库存是否与查询出的结果一致update();// 扣减失败if(!success){return Result.fail("库存不足!");}// 创建订单VoucherOrder voucherOrder = new VoucherOrder();// 生成订单 idLong orderId = redisIdWorker.nextId("order");voucherOrder.setVoucherId(voucherId);// 用户 idLong userId = UserHolder.getUser().getId();voucherOrder.setUserId(userId);voucherOrder.setId(orderId);save(voucherOrder);return Result.ok(orderId);}
}

最后我们测试居然发现原本预测执行100条订单的,但是实际上只有76条,为什么呢?

因为我们这种设置乐观锁太保守了,只要查到库存与之前不一样就不能扣除库存,但是实际上在库存还有很多的时候,这种是不影响的还是可以扣除的。于是我们优化:

// 扣减库存boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).// 增加对库存的判断,判断当前库存是否与查询出的结果一致// eq("stock", seckillVoucher.getStock()).    // 修改判断逻辑,改为只要库存大于0,就允许线程扣减gt("stock", 0).        update();

只要库存还是大于0的就能够进行修改

三、实现一人一单

需求:修改秒杀业务,要求一个优惠券,一个用户只能下一单

1、思路分析

我们得从查询订到到判断订单到创建订单这三步都要加上悲观锁,我们是同一个用户来了才需要处理这个并发安全问题,不同的用户是不影响的,因此加的锁应该根据用户的id来加锁

所以用synchronize(userId.toString().intern())这个来锁,为什么要加intern(),因为如果不加每次获取的字符串对象可能不是一个都是不一样的,加了可以保证每次都是同一个,他会去常量池里面找一样的返回。

2、代码初步实现 

@Service
public class VoucherOrderServiceImpl extends ServiceImpl implements IVoucherOrderService {@Autowiredprivate ISeckillVoucherService seckillVoucherService;@Autowiredprivate RedisIdWorker redisIdWorker;@Overridepublic Result seckillVoucher(Long voucherId) {SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);// 判断秒杀是否还未开始if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {Result.fail("秒杀尚未开始!");}// 判断秒杀是否已经结束if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {Result.fail("秒杀已经结束!");}// 判断库存是否充足if (seckillVoucher.getStock() < 1) {Result.fail("库存不足!");}return createVoucherOrder(voucherId);}@Transactionalpublic Result createVoucherOrder(Long voucherId) {// 判断当前优惠券用户是否已经下过单// 用户 idLong userId = UserHolder.getUser().getId();synchronized (userId.toString().intern()) {// 查询订单int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();if (count > 0) {return Result.fail("用户已经购买过一次");}// 扣减库存boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).
//                eq("stock", seckillVoucher.getStock()).    // 增加对库存的判断,判断当前库存是否与查询出的结果一致gt("stock", 0).        // 修改判断逻辑,改为只要库存大于0,就允许线程扣减update();// 扣减失败if (!success) {return Result.fail("库存不足!");}// 创建订单VoucherOrder voucherOrder = new VoucherOrder();// 生成订单 idLong orderId = redisIdWorker.nextId("order");voucherOrder.setVoucherId(voucherId);voucherOrder.setUserId(userId);voucherOrder.setId(orderId);save(voucherOrder);return Result.ok(orderId);}}
}

3、关于锁的范围 

这样加也有弊端,如果锁的范围是这里,锁先释放再提交的事务,假如我们刚改完释放锁还没提交事务,别人进来又改一次,然后再提交事务就会出现问题。

我们必须把锁加在外面,调用方法的时候锁住,锁住整个方法,事务先提交再释放锁

synchronize(userId.toString().intern()){return createVoucherOrder(voucherId);
}

4、关于事务失效

这样做会导致事务失效,我们现在给的是方法加的事务注解,seckillVoucher这个方法没有加,现在本质上是用this.createVoucherOrder来调用的,这个this拿到的是当前对象来调用的,而不是代理对象调用。

我们要想让事务生效,是spring对当前类做了动态代理,生成代理类,用代理对象来做的事务处理。现在用的是非代理对象来做的,所有没有事务功能。

我们要拿到事务代理对象才行。

我们可以用AopContext拿到代理对象,然后用代理对象来调用方法。

这样做我们要添加一个aspectjweaver的依赖,启动类添加@EnableAspectJAutoProxy(exposeProxy=true)注解来暴露代理对象

5、集群下线程并发问题

上面这种情况下只能保证单机部署下安全,在集群环境还是会出现问题

我们模拟集群的环境:

 测试发现集群模式下synchronize锁不住,为什么呢?

在集群模式下,每个都是不同tomcat,不同jvm的存在,每个jvm的每个锁都可以有一个线程来获取,就会出现并行安全问题。

要想解决这种问题,必须得想办法让多个jvm只能用同一个锁。分布式锁

相关内容

热门资讯

迷你退出安卓系统了吗,转型新篇... 最近有没有发现你的手机上那个可爱的迷你退出图标突然不见了?别急,让我来给你揭秘迷你退出安卓系统了吗的...
华为优先使用安卓系统,打造自主... 你知道吗?最近科技圈里有个大动作,那就是华为宣布优先使用安卓系统。这可不是一个简单的决定,它背后可是...
安卓系统隐藏了设置,隐藏设置功... 你知道吗?安卓系统这个大宝藏里,竟然隐藏着一些不为人知的设置!是不是听起来就有点小激动呢?别急,今天...
反渣恋爱系统安卓,收获真爱 你有没有听说过那个神奇的“反渣恋爱系统安卓”呢?最近,这款应用在网络上可是火得一塌糊涂,不少单身狗都...
安卓出厂系统能升级,探索无限可... 你知道吗?现在这个时代,手机更新换代的速度简直就像坐上了火箭!而说到手机,安卓系统可是占据了半壁江山...
老安卓刷机系统,从入门到精通 你有没有想过,你的老安卓手机其实还有大大的潜力呢?没错,就是那个陪伴你多年的老安卓,它可不是只能用来...
安卓粉ios系统app,兼容性... 你有没有发现,身边的朋友圈里,安卓粉和iOS系统粉总是争论不休?今天,咱们就来聊聊这个话题,看看安卓...
安卓系统语言下载,探索安卓系统... 你有没有想过,你的安卓手机是不是该换换口味了?没错,就是语言!想象如果你能轻松切换到自己喜欢的语言,...
安卓共有多少种系统,究竟有多少... 你有没有想过,安卓这个我们每天不离手的操作系统,竟然有那么多不同的版本呢?没错,安卓系统就像一个大家...
安卓系统怎么播放swf,And... 你有没有遇到过这种情况:手里拿着一部安卓手机,想看一个SWF格式的动画,结果发现怎么也打不开?别急,...
pos机安卓系统跟win系统,... 你有没有想过,那些在我们生活中默默无闻的POS机,竟然也有自己的操作系统呢?没错,就是安卓系统和Wi...
俄罗斯封禁安卓系统,本土化替代... 俄罗斯封禁安卓系统的背后:技术、经济与社会的影响在数字化浪潮席卷全球的今天,智能手机已成为我们生活中...
安卓系统总是弹出权限,安卓系统... 手机里的安卓系统是不是总爱和你玩捉迷藏?每次打开一个应用,它就跳出来问你要不要给它开权限,真是让人又...
安卓系统测血氧,便捷健康生活新... 你知道吗?现在科技的发展真是让人惊叹不已!手机,这个我们日常生活中不可或缺的小玩意儿,竟然也能变身成...
蓝光助手安卓系统的,深度解析与... 你有没有发现,现在手机屏幕越来越大,看视频、刷抖音,简直爽到飞起!但是,你知道吗?长时间盯着屏幕,尤...
安卓系统如何隐藏提示,Andr... 你是不是也和我一样,在使用安卓手机的时候,总是被那些弹出来的提示信息打扰到?别急,今天就来教你怎么巧...
安卓6.0系统如何分区,And... 你有没有想过,你的安卓手机里那些神秘的分区到底是怎么来的?别急,今天就来给你揭秘安卓6.0系统如何分...
安卓系统图片怎么涂鸦,指尖上的... 你有没有想过,在安卓系统的手机上,那些单调的图片也能变得生动有趣呢?没错,就是涂鸦!今天,就让我来带...
安卓系统40g,40GB存储空... 你有没有发现,最近你的安卓手机突然变得有点“胖”了呢?没错,就是那个传说中的40G!别急,别慌,今天...
安卓5.0系统怎么重置,轻松实... 手机用久了是不是感觉卡得要命?别急,今天就来教你怎么给安卓5.0系统来个彻底的重置,让它焕发新生!一...