Redis实战:黑马点评之优惠券秒杀

1 全局唯一ID

1.1全局唯一ID

每个店铺都可以发布优惠券:

当用户抢购时,就会生成订单并保存到tb_voucher_order这张表中,而订单表如果使用数据库自增ID就存在一些问题:

  • id的规律性太明显

    如果我们的id具有太明显的规则,用户或者说商业对手很容易猜测出来我们的一些敏感信息,比如商城在一天时间内,卖出了多少单,这明显不合适。

  • 受单表数据量的限制

    随着我们商城规模越来越大,mysql的单表的容量不宜超过500W,数据量过大之后,我们要进行拆库拆表,但拆分表了之后,他们从逻辑上讲他们是同一张表,所以他们的id是不能一样的, 于是乎我们需要保证id的唯一性。

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

常见的全局唯一ID的生成策略有:

  • UUID:16进制的字符串ID,可以做唯一ID,但不支持自增
  • Redis 自增
  • snowflake 雪花算法:long 类型的64ID,性能更好,但是比较依赖于时钟,如果时间不准确,可能会出现异常问题
  • 数据库自增:单独创建一张表,在表中记录自增的数值

这里我们使用redis来完成全局ID生成器的制作,因为redis可以很容易的满足以上特性:

  • 唯一性:redis是独立于数据库之外的,因此我们不用担心不同数据库中id重复的问题
  • 高可用:可以通过部署redis集群实现高可用
  • 高性能:redis是基于内存存储的,性能比较高
  • 递增性:redis的String类型中有incrby命令,我们可以用其来实现id的递增
  • 安全性:为了增加ID的安全性,我们可以不直接通过Redis自增来生成id,而是在自增的同时拼接一些其它信息:

通过redis生成的id组成结构如下:

符号位:1bit,永远为0

时间戳:31bit,以秒为单位,可以使用69年

序列号:32bit,秒内的计数器,支持每秒产生2^32个不同ID

1.2 Redis实现全局唯一Id

工具类代码如下:

/*** 全局唯一id生成工具类*/
@Component
public class RedisIdWorker {/*** 开始时间戳,单位:秒* 这里使用的时间是2022/9/18 16:52*/private static final long BEGIN_TIMESTAMP = 1663491103L;/*** 序列号的位数*/private static final int COUNT_BITS = 32;@Autowiredprivate StringRedisTemplate stringRedisTemplate;public long nextId(String keyPrefix) {// 1.生成时间戳,现在时间减去起始时间LocalDateTime now = LocalDateTime.now();long nowSecond = now.toEpochSecond(ZoneOffset.UTC);long timestamp = nowSecond - BEGIN_TIMESTAMP;/*** 2.生成序列号,这里key根据日期动态生成,每天都会生成一个新的key,这么做的主要原因有:* (1)由于redis中数字是有最大限制的,如果我们将key写死,即key是唯一的,那么日积月累之后可能有一天value就无法自增*     了,而如果根据日期动态生成key,即每天一个key,那么value是很难达到数字上限的* (2)可以更加方便的统计每天的业务量   */// 2.1.获取当前日期,精确到天String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));// 2.2.自增长,key不存在会自动创建long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);// 3.拼接并返回return timestamp << COUNT_BITS | count;}
}

我们可以根据以下代码来模拟多线程环境下id的生成速度

@Test
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);}latch.await();long end = System.currentTimeMillis();System.out.println("time = " + (end - begin));
}

在这里我们用到了countdownlatch,countdownlatch名为信号枪:主要的作用是同步协调在多线程的等待于唤醒问题

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

CountDownLatch 中有两个最重要的方法:countDown和await

await 方法 是阻塞方法,我们担心分线程没有执行完时,main线程就先执行,所以使用await可以让main线程阻塞,那么什么时候main线程不再阻塞呢?当CountDownLatch 内部维护的 变量变为0时,就不再阻塞,直接放行,那么什么时候CountDownLatch 维护的变量变为0 呢,我们只需要调用一次countDown ,内部变量就减少1,我们让分线程和变量绑定, 执行完一个分线程就减少一个变量,当分线程全部走完,CountDownLatch 维护的变量就是0,此时await就不再阻塞,统计出来的时间也就是所有分线程执行完后的时间。

测试结束后,我们可以看到redis中生成的计数信息,说明我们通过刚刚的测试生成了30000个id


3 添加优惠卷

每个店铺都可以发布优惠券,分为平价券和特价券。平价券可以任意购买,而特价券需要秒杀抢购:

tb_voucher:优惠券的基本信息,优惠金额、使用规则等
tb_seckill_voucher:优惠券的库存、开始抢购时间,结束抢购时间。特价优惠券才需要填写这些信息

关于以上两个表,有几个点需要注意:

  • 平价券只需要在tb_voucher中保存信息即可,而特价券不仅需要在tb_voucher中保存信息,还需要在tb_seckill_voucher保存信息
  • 在tb_voucher中通过type字段来区分一个优惠券是平价券还是特价券。
  • tb_seckill_voucher的主键使用的是特价券在tb_voucher中的id

关于新增优惠券的代码,在基础代码中已经完成了,我们只需简单阅读一下即可

新增普通卷代码:

VoucherController

@PostMapping
public Result addVoucher(@RequestBody Voucher voucher) {voucherService.save(voucher);return Result.ok(voucher.getId());
}

新增秒杀卷代码:

VoucherController

@PostMapping("seckill")
public Result addSeckillVoucher(@RequestBody Voucher voucher) {voucherService.addSeckillVoucher(voucher);return Result.ok(voucher.getId());
}

VoucherServiceImpl

@Override
@Transactional
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);// 保存秒杀库存到Redis中stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString());
}

我们可以通过postman,往表中插入一张秒杀券,以便后续功能的测试,注意这里的beginTime和endTime一定要改在当前时间之后,否则前端页面会无法显示

{"shopId": 1,"title": "100元代金券","subTitle": "周一到周日均可使用","rules": "全场通用\\n无需预约\\n可无限叠加\\不兑现、不找零\\n仅限堂食","payValue": 8000,"actualValue": 10000,"type": 1,"stock": 100,"beginTime":"2022-09-18T17:42:00","endTime":"2022-09-19T23:40:00"
}


4 实现秒杀下单基本代码

下单核心思路:当我们点击抢购时,会触发右侧的请求,我们只需要编写对应的controller即可

秒杀下单时需要判断两点:

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

下单核心逻辑分析:

当用户开始进行下单,我们应当去查询优惠卷信息,查询到优惠卷信息,判断是否满足秒杀条件,比如时间是否充足,如果时间充足,则进一步判断库存是否足够,如果两者都满足,则扣减库存,创建订单,然后返回订单id,如果有一个条件不满足则直接结束。

VoucherOrderController代码编写如下:

@RestController
@RequestMapping("/voucher-order")
public class VoucherOrderController {@Autowiredprivate IVoucherOrderService voucherService;/*** 实现秒杀下单* @param voucherId* @return*/@PostMapping("seckill/{id}")public Result seckillVoucher(@PathVariable("id") Long voucherId) {return voucherService.seckillVoucher(voucherId);}}

VoucherOrderServiceImpl代码编写如下:

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Autowiredprivate ISeckillVoucherService seckillVoucherService;@Autowiredprivate RedisIdWorker redisIdWorker;/*** 实现秒杀下单* @param voucherId* @return*/@Override@Transactionalpublic Result seckillVoucher(Long voucherId) {//获取秒杀券信息SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);//判断秒杀券是否存在if(seckillVoucher == null){return Result.fail("秒杀券不存在");}//判断秒杀是否开始或是否结束LocalDateTime now = LocalDateTime.now();//如果现在时间在开始时间之前或者在结束时间之后说明秒杀活动未开始或者已结束if(now.isBefore(seckillVoucher.getBeginTime())||now.isAfter(seckillVoucher.getEndTime())){return Result.fail("不在活动时间内");}//判断库存是否足够if(seckillVoucher.getStock() < 1){return  Result.fail("库存不足");}//扣减库存boolean success = seckillVoucherService.update().setSql("stock = stock -1").eq("voucher_id", voucherId).update();if(!success){return  Result.fail("库存不足");}//生成订单信息VoucherOrder voucherOrder = new VoucherOrder();//使用自定义的全局id生成器生成订单idLong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);//保存用户idvoucherOrder.setUserId(UserHolder.getUser().getId());//保存优惠券idvoucherOrder.setVoucherId(voucherId);save(voucherOrder);//返回订单idreturn Result.ok(orderId);}
}

5 库存超卖问题

5.1库存超卖问题分析

关于库存数量的判断和库存的扣减,在我们原有代码中是这么写的:

    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执行查询库存,判断库存大于0,于是准备去扣减库存,但是在扣减库存之前,线程2也执行了查询库存的操作,也发现库存大于零,那么这两个线程最终都会去扣减库存,而此时库存中商品的数量只有1,此时就会出现库存的超卖问题,即库存变成负数

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

这里我们使用乐观锁解决库存超卖问题,而乐观锁也有以下两种实现方式:

  • 版本号法 :所谓版本号法就是在数据表中新增一个version字段,这个字段的值就是该行数据的版本号,当我们执行修改操作时,让版本号+1,这样,在多线程并发的时候,我们就可以基于版本号来判断数据有没有被修改过

    例如,此时有线程1和线程2两个线程同时来访问库存,线程1先执行,在查询库存时会将库存和版本号一并查询出来,假设此时库存和版本号都为1,当线程1判断库存大于0并准备去执行扣减操作时,线程2开始执行了,同样的,线程2也会去查询库存和版本号,得到的结果也都为1,当线程2准备去执行扣减操作时,线程1的扣减操作已经开始执行了,线程1在执行扣减操作时会判断此时的版本号是否与之前查询出来的版本号一致,即版本号是否为1,经过判断发现是一致的,线程1就会开始执行扣减操作,库存减一,同时版本号加一。当线程2开始执行扣减操作时,也会去判断此时的版本号是否与之前查询出来的一致,之前线程2查询出来的版本号是1,但此时版本号已经变成2了,线程2的更新操作就会失败,这样也就解决了超卖的问题。

  • CAS法:CAS 法,即比较和替换法,是在版本号法的基础上改进而来

    以我们当前的业务为例,我们发现库存和版本号是同时查而且同时发生变化的,当我们查询库存时会将版本号一并查询出来,而当库存减一时版本号也会加一,这种情况下,我们就可以用库存数量代替版本号来判断当前数据是否发生变化

    例如,此时有线程1和线程2两个线程同时来访问库存,线程1先执行,在查询库存时只将库存查询出来,假设此时库存为1,当线程1判断库存大于0并准备去执行扣减操作时,线程2开始执行了,同样的,线程2也会去查询库存,得到的结果也是1,当线程2准备去执行扣减操作时,线程1的扣减操作已经开始执行了,线程1在执行扣减操作时会判断此时的库存数量是否与之前查询出来的库存数量一致,即是否为1,经过判断发现是一致的,线程1就会开始执行扣减操作,库存减一。当线程2开始执行扣减操作时,也会去判断此时的库存数量是否与之前查询出来的一致,之前线程2查询出来的库存数量是1,但此时版本号已经变成0了,线程2的更新操作就会失败,这样也就解决了超卖的问题。

5.2 乐观锁解决超卖问题

修改代码方案一

这种方案是基于CAS法实现的,我们可以将VoucherOrderServiceImpl 在扣减库存时执行的sql语句改写成:

  boolean success = seckillVoucherService.update().setSql("stock = stock -1").eq("voucher_id", voucherId).eq("stock",seckillVoucher.getStock()).update(); //where id = ? and stock = ?

以上逻辑的核心含义是:只要扣减库存时的库存和之前查询出来的库存是一致的,就意味着没有其他线程修改过库存,那么此时就是线程安全的

以上这种方式虽然保证了库存不会超卖,但是库存充足的情况下会出现许多扣减库存失败的情况,失败的原因在于:如果在同一时间有多个线程拿到了相同数量的库存,那么这些线程中最多只会有一条线程能扣减库存成功,因为只要有一条线程将库存修改了,那么其他所有拿到相同数量库存的线程在进行库存数量的判断时都会发现库存数量已经被修改了,导致这些线程扣减库存执行失败,哪怕此刻库存仍然十分充足

修改代码方案二

基于上述方案失败的经验,我们可以将sql语句改写成:

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

也就是说,只要stock > 0,就允许线程修改库存,由于这里的判断是交给数据库进行的,而数据库在执行更新操作时是会为数据加上行锁的,因此就不用担心会发生并发问题。


6 一人一单问题

发行优惠券的目的是为了引流,但是目前的情况是,每个人都可以无限制的对优惠券进行抢购,所以我们应该修改一下当前的业务逻辑,让一个用户只能对同一个优惠券下单一次。

具体业务逻辑如下:

在VoucherOrderServiceImpl中修改代码:

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Autowiredprivate ISeckillVoucherService seckillVoucherService;@Autowiredprivate RedisIdWorker redisIdWorker;/*** 实现秒杀下单* @param voucherId* @return*/@Override@Transactionalpublic Result seckillVoucher(Long voucherId) {//获取秒杀券信息SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);//判断秒杀券是否存在if(seckillVoucher == null){return Result.fail("秒杀券不存在");}//判断秒杀是否开始或是否结束LocalDateTime now = LocalDateTime.now();//如果现在时间在开始时间之前或者在结束时间之后说明秒杀活动未开始或者已结束if(now.isBefore(seckillVoucher.getBeginTime())||now.isAfter(seckillVoucher.getEndTime())){return Result.fail("不在活动时间内");}//判断库存是否足够if(seckillVoucher.getStock() < 1){return  Result.fail("库存不足");}//判断该用户有没有下单过Integer count = query().eq("user_id", UserHolder.getUser().getId()).eq("voucher_id", voucherId).count();if(count>0){return Result.fail("已经购买过了!");}//扣减库存boolean success = seckillVoucherService.update().setSql("stock = stock -1").eq("voucher_id", voucherId).gt("stock",0).update();if(!success){return  Result.fail("库存不足");}//生成订单信息VoucherOrder voucherOrder = new VoucherOrder();//使用自定义的全局id生成器生成订单idLong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);//保存用户idvoucherOrder.setUserId(UserHolder.getUser().getId());//保存优惠券idvoucherOrder.setVoucherId(voucherId);save(voucherOrder);//返回订单idreturn Result.ok(orderId);}
}

上述代码在多线程环境下也可能会出现线程安全问题,实际上,以下代码

 //判断该用户有没有下单过
Integer count = query().eq("user_id", UserHolder.getUser().getId()).eq("voucher_id", voucherId).count();
if(count>0){return Result.fail("已经购买过了!");
}

与库存超卖的原因类似,这种判断在多线程环境下几乎趋近于摆设,一个用户同样能够完成多次下单,因此我们需要对这些代码进行优化,在解决库存超卖问题时,我们使用的是乐观锁,但在这里由于是查询操作,因此我们只能选择悲观锁。

初始方案是将下单的操作封装成一个createVoucherOrder方法,同时为了确保线程安全,在方法上添加一把synchronized 锁

   /*** 实现秒杀下单* @param voucherId* @return*/@Overridepublic Result seckillVoucher(Long voucherId) {//获取秒杀券信息SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);//判断秒杀券是否存在if(seckillVoucher == null){return Result.fail("秒杀券不存在");}//判断秒杀是否开始或是否结束LocalDateTime now = LocalDateTime.now();//如果现在时间在开始时间之前或者在结束时间之后说明秒杀活动未开始或者已结束if(now.isBefore(seckillVoucher.getBeginTime())||now.isAfter(seckillVoucher.getEndTime())){return Result.fail("不在活动时间内");}//判断库存是否足够if(seckillVoucher.getStock() < 1){return  Result.fail("库存不足");}return createVoucherOrder(voucherId);}/*** 实现秒杀下单的具体业务逻辑* @param voucherId* @return*/@Transactionalpublic synchronized Result createVoucherOrder(Long voucherId) {//判断该用户有没有下单过Integer count = query().eq("user_id", UserHolder.getUser().getId()).eq("voucher_id", voucherId).count();if(count>0){return Result.fail("已经购买过了!");}//扣减库存boolean success = seckillVoucherService.update().setSql("stock = stock -1").eq("voucher_id", voucherId).gt("stock",0).update();if(!success){return Result.fail("库存不足");}//生成订单信息VoucherOrder voucherOrder = new VoucherOrder();//使用自定义的全局id生成器生成订单idLong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);//保存用户idvoucherOrder.setUserId(UserHolder.getUser().getId());//保存优惠券idvoucherOrder.setVoucherId(voucherId);save(voucherOrder);//返回订单idreturn Result.ok(orderId);}

但是按照上述方式添加锁,锁的粒度太粗,在使用锁过程中,控制锁粒度是一件非常重要的事情,而直接在方法上添加synchronized会使用当前类对象作为锁对象,由于当前类对象在ioc容器中是单例的,所以在高并发环境下,多个线程共用一把锁,线程串行执行,严重影响效率

那么我们应该使用什么作为锁呢?让我们回归到业务上来分析,由于我们当前希望的是一个用户最多只能下单一次,那么我们就可以使用当前用户id来作为锁,这样就能在控制锁粒度的同时保证线程安全,具体代码如下:

    @Transactionalpublic Result createVoucherOrder(Long voucherId) {/*这里使用intern()表示从常量池中获取字符串,由于我们直接使用toString()底层默认是new一个字符串并返回,无法保证锁对象唯一,而常量池中的字符串是唯一的*/synchronized (UserHolder.getUser().getId().toString().intern()) {//判断该用户有没有下单过Integer count = query().eq("user_id", UserHolder.getUser().getId()).eq("voucher_id", voucherId).count();if(count>0){return Result.fail("已经购买过了!");}//扣减库存boolean success = seckillVoucherService.update().setSql("stock = stock -1").eq("voucher_id", voucherId).gt("stock",0).update();if(!success){return Result.fail("库存不足");}//生成订单信息VoucherOrder voucherOrder = new VoucherOrder();//使用自定义的全局id生成器生成订单idLong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);//保存用户idvoucherOrder.setUserId(UserHolder.getUser().getId());//保存优惠券idvoucherOrder.setVoucherId(voucherId);save(voucherOrder);//返回订单idreturn Result.ok(orderId);}}

上述代码还是存在问题,问题的原因在于当前方法被spring的事务控制,如果我们在方法内部加锁,可能会导致当前方法事务还没有提交,但是锁已经释放,这种情况下其他事务就会抢到锁然后执行方法,这时候由于事务还未提交,当前用户在数据库中仍然是没有订单信息的,此时就会出现重复下单的情况。

为了解决上述问题,我们必须要将锁的范围扩大到整个事务,那这应该怎样操作呢?

我们可以针对seckillVoucher调用createVoucherOrder方法的代码进行加锁,如下所示:

    /*** 实现秒杀下单* @param voucherId* @return*/@Overridepublic Result seckillVoucher(Long voucherId) {//获取秒杀券信息SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);//判断秒杀券是否存在if(seckillVoucher == null){return Result.fail("秒杀券不存在");}//判断秒杀是否开始或是否结束LocalDateTime now = LocalDateTime.now();//如果现在时间在开始时间之前或者在结束时间之后说明秒杀活动未开始或者已结束if(now.isBefore(seckillVoucher.getBeginTime())||now.isAfter(seckillVoucher.getEndTime())){return Result.fail("不在活动时间内");}//判断库存是否足够if(seckillVoucher.getStock() < 1){return  Result.fail("库存不足");}//针对调用方法的代码加锁synchronized (UserHolder.getUser().getId().toString().intern()) {return createVoucherOrder(voucherId);}}

但是以上做法仍然有问题,由于我们在调用createVoucherOrder方法时,调用者是this,而我们知道spring控制事务的原理是通过创建类的代理对象来调用方法,而在这里方法的调用者并非是代理对象,因此会出现事务失效的情况。

这里我们的解决方案是获取当前类的父接口的代理对象,并由代理对象来执行createVoucherOrder方法

synchronized (UserHolder.getUser().getId().toString().intern()) {//由spring帮我们创建当前类的代理对象,由代理对象来调用方法//由于代理对象是spring创建的,自然就能进行事务的管理了IVoucherOrderService proxy = (IVoucherOrderService)AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);
}

为了上述代码的正常运行,我们还需要做几件事

在IVoucherOrderService接口中创建createVoucherOrder方法:

public interface IVoucherOrderService extends IService<VoucherOrder> {Result seckillVoucher(Long voucherId);Result createVoucherOrder(Long voucherId);
}

在pom文件中导入一下依赖:

<dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId>
</dependency>

在启动类上打上注解@EnableAspectJAutoProxy

@MapperScan("com.hmdp.mapper")
@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true)//暴露代理对象
public class HmDianPingApplication {public static void main(String[] args) {SpringApplication.run(HmDianPingApplication.class, args);}
}

这样就大功告成了

关于秒杀的后续优化,写在了另一篇文章中:【Redis】Redis实战:黑马点评之秒杀优化 ,欢迎大佬们访问

【Redis】Redis实战:黑马点评之优惠券秒杀相关推荐

  1. Redis实战—黑马点评项目—优惠券秒杀

    一.全局ID生成器 对于优惠券业务,亦即订单业务,其中的优惠券订单id存储到数据库中时将不采用mysql自增,因为这是一种不安全且当有庞大的订单时又不能保证分布式全局唯一性的做法.因此,就需要有一这样 ...

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

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

  3. 【Redis】实战篇:优惠卷秒杀 (库存超卖问题、一人一单问题)

    文章目录 3.1 全局唯一ID 3.2 -Redis实现全局唯一Id 3.3 添加优惠卷 3.4 实现秒杀下单 3.5 库存超卖问题分析 3.6 乐观锁解决超卖问题 3.7 优惠券秒杀-一人一单 3. ...

  4. Redis消息队列 | 黑马点评

    目录 一.认识消息队列 二.List模拟消息队列 三.PubSub的消息队列 四.Stream的消息队列(重点) 1.单消费模式 2.消费者组 五.redis三种消息队列对比 六.优化秒杀实战 1.创 ...

  5. Redis分布式锁 | 黑马点评

    目录 一.分布式锁概述 二.基于Redis的分布式锁 1.思路分析 2.初级版本 3.误删问题 4.改进分布式锁 5.原子性问题 6.使用Lua脚本解决原子性问题 7.setnx实现分布式锁存在问题 ...

  6. Redis解决优惠券秒杀

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

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

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

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

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

  9. 黑马点评Redis实战(短信登录;商户查询缓存)

    黑马点评 通过一个类似于大众点评的项目了解学习redis在实战项目中的使用,下面是项目中会涉及到的模块: 一.导入黑马点评项目 导入springboot项目,导入sql脚本到数据库,开启nginx,更 ...

最新文章

  1. ‘vue-cli-service‘ 不是内部或外部命令,也不是可运行的程序
  2. python爬取抖音评论_怎样用Python3爬取抖音神曲
  3. pythonweb框架Flask学习笔记05-简单登陆
  4. 2246xt u盘开卡详细教程_U盘制作PE及系统安装详细教程!
  5. Spring中策略模式实现方法
  6. wxWidgets:wxMoveEvent类用法
  7. 【WPF】WPF中调用Winform
  8. 设置背景色为渐变色 css
  9. 魔兽地图编辑器使用自定义图标的方法
  10. 【Unity 资源分享】☀️ | Unity 超好看的 精品四季蔚蓝自然场景模型 ,让我们离二次元开发更近一步!
  11. 微信小程序请求后台接口(完整版)
  12. MySQL中的极限值
  13. 滚珠螺杆螺母的安装教程来了
  14. 2021湖北省普通高考成绩查询果,2021年湖北高考体检时间项目及体检结果查询公布时间...
  15. unity中使用render texture全黑的可能解决方案
  16. Error LNK2005:_main already defined in test.obj
  17. FastDFS文件系统单机环境搭建
  18. windows7 x64系统 不能安装MTP驱动
  19. 输入的英文字母隔得很开
  20. Axure的动态面板

热门文章

  1. 使用(运行)win+r 快速打开所有的软件
  2. AIX添加lun 方式
  3. 新店如何吸引客户进店?连环方案不仅获得大量客户还锁定消费!
  4. kthreadd和init进程的启动(二)
  5. linux CPU动态调频
  6. DUKPT IPEK KSNPOS机的加密传输过程
  7. 带有字数统计的 TextInputLayout
  8. linux 定时器 jiffies,linux下jiffies定时器和hrtimer高精度定时器(示例代码)
  9. 弘辽科技:2021年天猫618现货商品活动价格要求是什么?
  10. CDMA 1xRTT的分组数据业务(转)