Redis学习日记(四)

发布时间:2026/11/12 12:36:38

Redis学习日记(四) Redis实战篇优惠劵秒杀1.全局唯一ID每个店铺都可以发布优惠券当用户抢购时就会生成订单并保存到tb_voucher_order这张表中而订单表如果使用数据库自增ID就存在一些问题1id的规律性太明显2受单表数据量的限制1场景分析场景分析一如果我们的id具有太明显的规则用户或者说商业对手很容易猜测出来我们的一些敏感信息比如商城在一天时间内卖出了多少单这明显不合适。场景分析二随着我们商城规模越来越大mysql的单表的容量不宜超过500W数据量过大之后我们要进行拆库拆表但拆分表了之后他们从逻辑上讲他们是同一张表所以他们的id是不能一样的 于是乎我们需要保证id的唯一性。2全局ID生成器是一种在分布式系统下用来生成全局唯一ID的工具一般要满足下列特性为了增加ID的安全性我们可以不直接使用Redis自增的数值而是拼接一些其它信息ID的组成部分符号位1bit永远为0时间戳31bit以秒为单位可以使用69年序列号32bit秒内的计数器支持每秒产生2^32个不同ID2.Redis实现全局唯一Id1代码实现Component public class RedisIdWorker { /** * 开始时间戳 */ 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; } }测试类知识小贴士关于countdownlatchcountdownlatch名为信号枪主要的作用是同步协调在多线程的等待于唤醒问题我们如果没有CountDownLatch 那么由于程序是异步的当异步程序没有执行完时主线程就已经执行完了然后我们期望的是分线程全部走完之后主线程再走所以我们此时需要使用到CountDownLatchCountDownLatch 中有两个最重要的方法1、countDown2、awaitawait 方法 是阻塞方法我们担心分线程没有执行完时main线程就先执行所以使用await可以让main线程阻塞那么什么时候main线程不再阻塞呢当CountDownLatch 内部维护的 变量变为0时就不再阻塞直接放行那么什么时候CountDownLatch 维护的变量变为0 呢我们只需要调用一次countDown 内部变量就减少1我们让分线程和变量绑定 执行完一个分线程就减少一个变量当分线程全部走完CountDownLatch 维护的变量就是0此时await就不再阻塞统计出来的时间也就是所有分线程执行完后的时间。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)); }3.添加优惠卷每个店铺都可以发布优惠券分为平价券和特价券。平价券可以任意购买而特价券需要秒杀抢购tb_voucher优惠券的基本信息优惠金额、使用规则等 tb_seckill_voucher优惠券的库存、开始抢购时间结束抢购时间。特价优惠券才需要填写这些信息平价卷由于优惠力度并不是很大所以是可以任意领取而代金券由于优惠力度大所以像第二种卷就得限制数量从表结构上也能看出特价卷除了具有优惠卷的基本信息以外还具有库存抢购时间结束时间等等字段新增普通卷代码VoucherControllerPostMapping public Result addVoucher(RequestBody Voucher voucher) { voucherService.save(voucher); return Result.ok(voucher.getId()); }新增秒杀卷代码VoucherControllerPostMapping(seckill) public Result addSeckillVoucher(RequestBody Voucher voucher) { voucherService.addSeckillVoucher(voucher); return Result.ok(voucher.getId()); }VoucherServiceImplOverride 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()); }4.实现秒杀下单下单核心思路当我们点击抢购时会触发右侧的请求我们只需要编写对应的controller即可秒杀下单应该思考的内容下单时需要判断两点秒杀是否开始或结束如果尚未开始或已经结束则无法下单库存是否充足不足则无法下单下单核心逻辑分析当用户开始进行下单我们应当去查询优惠卷信息查询到优惠卷信息判断是否满足秒杀条件比如时间是否充足如果时间充足则进一步判断库存是否足够如果两者都满足则扣减库存创建订单然后返回订单id如果有一个条件不满足则直接结束。VoucherOrderServiceImplOverride 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扣减库存 boolean success seckillVoucherService.update() .setSql(stock stock -1) .eq(voucher_id, voucherId).update(); if (!success) { //扣减库存 return Result.fail(库存不足); } //6.创建订单 VoucherOrder voucherOrder new VoucherOrder(); // 6.1.订单id long orderId redisIdWorker.nextId(order); voucherOrder.setId(orderId); // 6.2.用户id Long userId UserHolder.getUser().getId(); voucherOrder.setUserId(userId); // 6.3.代金券id voucherOrder.setVoucherId(voucherId); save(voucherOrder); ​ return Result.ok(orderId); ​ }5.库存超卖问题分析有关超卖问题分析在我们原有代码中是这么写的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那么这两个线程都会去扣减库存最终多个线程相当于一起去扣减库存此时就会出现库存的超卖问题。超卖问题是典型的多线程安全问题针对这一问题的常见解决方案就是加锁而对于加锁我们通常有两种解决方案见下图悲观锁悲观锁可以实现对于数据的串行化执行比如syn和lock都是悲观锁的代表同时悲观锁中又可以再细分为公平锁非公平锁可重入锁等等乐观锁乐观锁会有一个版本号每次操作数据会对版本号1再提交回数据时会去校验是否比之前的版本大1 如果大1 则进行操作成功这套机制的核心逻辑在于如果在操作过程中版本号只比原来大1 那么就意味着操作过程中没有人对他进行过修改他的操作就是安全的如果不大1则数据被修改过当然乐观锁还有一些变种的处理方式比如cas乐观锁的典型代表就是cas利用cas进行无锁化机制加锁var5 是操作前读取的内存值while中的var1var2 是预估值如果预估值 内存值则代表中间没有被人修改过此时就将新值去替换 内存值其中do while 是为了在操作失败时再次进行自旋操作即把之前的逻辑再操作一次。int var5; do { var5 this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 var4)); ​ return var5;课程中的使用方式课程中的使用方式是没有像cas一样带自旋的操作也没有对version的版本号1 他的操作逻辑是在操作时对版本号进行1 操作然后要求version 如果是1 的情况下才能操作那么第一个线程在操作后数据库中的version变成了2但是他自己满足version1 所以没有问题此时线程2执行线程2 最后也需要加上条件version 1 但是现在由于线程1已经操作过了所以线程2操作时就不满足version1 的条件了所以线程2无法执行成功。6.乐观锁解决超卖问题修改代码方案一、VoucherOrderServiceImpl 在扣减库存时改为boolean success seckillVoucherService.update() .setSql(stock stock -1) //set stock stock -1 .eq(voucher_id, voucherId).eq(stock,voucher.getStock()).update(); //where id and stock ?以上逻辑的核心含义是只要我扣减库存时的库存和之前我查询到的库存是一样的就意味着没有人在中间修改过库存那么此时就是安全的但是以上这种方式通过测试发现会有很多失败的情况失败的原因在于在使用乐观锁过程中假设100个线程同时都拿到了100的库存然后大家一起去进行扣减但是100个人中只有1个人能扣减成功其他的人在处理时他们在扣减时库存已经被修改过了所以此时其他线程都会失败。修改代码方案二、之前的方式要修改前后都保持一致但是这样我们分析过成功的概率太低所以我们的乐观锁需要变一下改成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的值进行递增最后返回一个完整的值7.优惠券秒杀-一人一单需求修改秒杀业务要求同一个优惠券一个用户只能下一单现在的问题在于优惠卷是为了引流但是目前的情况是一个人可以无限制的抢这个优惠卷所以我们应当增加一层逻辑让一个用户只能下一个单而不是让一个用户下多个单具体操作逻辑如下比如时间是否充足如果时间充足则进一步判断库存是否足够然后再根据优惠卷id和用户id查询是否已经下过这个订单如果下过这个订单则不再下单否则进行下单VoucherOrderServiceImpl初步代码增加一人一单逻辑​ Override 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.一人一单逻辑 // 5.1.用户id Long userId UserHolder.getUser().getId(); 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(); // 7.1.订单id long orderId redisIdWorker.nextId(order); voucherOrder.setId(orderId); ​ voucherOrder.setUserId(userId); // 7.3.代金券id voucherOrder.setVoucherId(voucherId); save(voucherOrder); ​ return Result.ok(orderId); ​ }存在问题现在的问题还是和之前一样并发过来查询数据库都不存在订单所以我们还是需要加锁但是乐观锁比较适合更新数据而现在是插入数据所以我们需要使用悲观锁操作注意在这里提到了非常多的问题我们需要慢慢的来思考首先我们的初始方案是封装了一个createVoucherOrder方法同时为了确保他线程安全在方法上添加了一把synchronized 锁Transactional public synchronized Result createVoucherOrder(Long voucherId) { ​ 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.订单id long orderId redisIdWorker.nextId(order); voucherOrder.setId(orderId); // 7.2.用户id voucherOrder.setUserId(userId); // 7.3.代金券id voucherOrder.setVoucherId(voucherId); save(voucherOrder); ​ // 7.返回订单id return Result.ok(orderId); }但是这样添加锁锁的粒度太粗了在使用锁过程中控制锁粒度是一个非常重要的事情因为如果锁的粒度太大会导致每个线程进来都会锁住所以我们需要去控制锁的粒度以下这段代码需要修改为 intern() 这个方法是从常量池中拿到数据如果我们直接使用userId.toString() 他拿到的对象实际上是不同的对象new出来的对象我们使用锁必须保证锁必须是同一把所以我们需要使用intern()方法Transactional public Result createVoucherOrder(Long voucherId) { Long userId UserHolder.getUser().getId(); synchronized(userId.toString().intern()){ // 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.订单id long orderId redisIdWorker.nextId(order); voucherOrder.setId(orderId); // 7.2.用户id voucherOrder.setUserId(userId); // 7.3.代金券id voucherOrder.setVoucherId(voucherId); save(voucherOrder); ​ // 7.返回订单id return Result.ok(orderId); } }但是以上代码还是存在问题问题的原因在于当前方法被spring的事务控制如果你在方法内部加锁可能会导致当前方法事务还没有提交但是锁已经释放也会导致问题所以我们选择将当前方法整体包裹起来确保事务不会出现问题如下在seckillVoucher 方法中添加以下逻辑这样就能保证事务的特性同时也控制了锁的粒度但是以上做法依然有问题因为你调用的方法其实是this.的方式调用的事务想要生效还得利用代理来生效所以这个地方我们需要获得原始的事务对象 来操作事务8.集群环境下的并发问题通过加锁可以解决在单机情况下的一人一单安全问题但是在集群模式下就不行了。1、我们将服务启动两份端口分别为8081和80822、然后修改nginx的conf目录下的nginx.conf文件配置反向代理和负载均衡有关锁失效原因分析由于现在我们部署了多个tomcat每个tomcat都有一个属于自己的jvm那么假设在服务器A的tomcat内部有两个线程这两个线程由于使用的是同一份代码那么他们的锁对象是同一个是可以实现互斥的但是如果现在是服务器B的tomcat内部又有两个线程但是他们的锁对象写的虽然和服务器A一样但是锁对象却不是同一个所以线程3和线程4可以实现互斥但是却无法和线程1和线程2实现互斥这就是 集群环境下syn锁失效的原因在这种情况下我们就需要使用分布式锁来解决这个问题。
延伸阅读

更多相关文章

2026/11/12 12:36:38

降AI工具避坑指南:新手必知的5大消费陷阱

1. 为什么你需要这份降AI工具避坑指南?第一次接触降AI工具的新手,往往会在各种隐性消费和功能陷阱上栽跟头。我见过太多人因为不了解行业潜规则,在订阅服务、插件购买和算力消耗上白白浪费几百块。就拿最常见的文字转视频工具来说&#xff0c…

2026/11/10 22:56:29

从零构建个人网页:HTML5基础、CSS布局与JavaScript交互实战

1. 项目概述:为什么从HTML个人网页开始?如果你对编程感兴趣,或者想在网上有一个属于自己的小天地,但又觉得那些复杂的框架和工具让人望而却步,那么从制作一个简单的HTML个人网页开始,绝对是条正确的路。我叫…

2026/11/12 2:06:35

Unity新手避坑指南:从零到一的实战入门与核心模块解析

1. 项目概述:一份来自一线开发者的Unity入门实战笔记最近在整理硬盘,翻到了自己刚接触Unity时写的一大堆笔记。从最初连GameObject和Component都分不清,到后来能独立完成一个小项目,这些零零散散的记录几乎见证了我整个入门期的困…

2026/11/12 12:35:56

宝坻验电工证电话千万别踩坑,新手怎么避坑全在这了

宝坻验电工证电话千万别踩坑,新手怎么避坑全在这了 你不知道去哪报名怕被中介坑?别慌,这期内容专为宝坻地区准备,带你一步步搞清楚【宝坻验电工证电话】到底怎么找、怎么避坑,尤其是那些藏在细节里的“套路”和“坑”,我全都告诉你。…

2026/11/12 12:35:56

2022电工证换证别走弯路,含金量高的证书怎么考

2022电工证换证别走弯路,含金量高的证书怎么考 不知道去哪报名怕被中介坑?2022年电工证换证政策变动不少,很多人因为信息不对称,花了冤枉钱还拿不到证。其实只要掌握好流程,避开套路,含金量高的电工证也能轻松搞定。下面我从洛阳涧西区市政公用…

2026/11/12 12:35:56

曹县在哪里办电工证官方报名入口在哪怎么找

曹县在哪里办电工证官方报名入口在哪怎么找 工地太忙根本没时间复习考试,曹县电工证到底怎么考?很多人在工地干活,手里活儿干不完,脑子里想着怎么把电工证给办了,但一想到考试、复习这些事,就头大。今天我就从一个干了多年特种作业培训的老手角度,给你…

2026/11/12 12:35:56

浦北电工证快速拿证通过率揭秘:如何避开中介陷阱,一步到位

浦北电工证快速拿证通过率揭秘:如何避开中介陷阱,一步到位 不知道去哪报名怕被中介坑?浦北电工证快速拿证到底靠不靠谱?别急,先听我说说真实情况。去年三门峡某项目现场,有位电工小李因为走了“捷径”,结果不仅花了冤枉钱,还耽误了工期。现在他每次提…

2026/11/12 12:35:55

延安宝塔区电工证保姆级教程:外省证能不能转过来?一文搞懂

延安宝塔区电工证保姆级教程:外省证能不能转过来?一文搞懂 之前考的证在外省能不能转过来?这个问题我每天都能听到,特别是像你这样在延安宝塔区想从事电工工作的朋友。别急,今天这波保姆级教程,就是让你明明白白知道该怎么操作,怎么转,怎么考,怎么用…

2026/11/12 12:30:55

茂名化州电工证考证怕考不过白交培训费?电子证书怎么拿?

茂名化州电工证考证怕考不过白交培训费?电子证书怎么拿? 你花了几千块报名,结果一考试就挂,培训费打了水漂,这事儿太常见了。尤其是茂名化州电工证考证,很多人在报名前都没搞清楚流程,结果浪费了时间和金钱。别急,本文就给你讲明白怎么考,怎么拿…

2026/11/11 12:42:59

电工证丢失还能补吗?千万别踩坑,丢了证等于失业

电工证丢失还能补吗?千万别踩坑,丢了证等于失业 你正在找工作,但手里的电工证突然丢了,这下怎么办?别急,先别慌, 电工证丢失还能补吗 ?答案是肯定的,但 千万别踩坑 ,补办流程如果不了解,可能会耽误你上岗、甚至影响到你的收入。…

2026/11/12 0:04:01

工地太忙根本没时间复习考试?学习电工证到哪里学通过率揭秘

工地太忙根本没时间复习考试?学习电工证到哪里学通过率揭秘 工地太忙根本没时间复习考试?你不是一个人。作为一名在南阳卧龙区干了七八年市政公用工程的电工,我深知这个问题的现实性。电工证不仅是工作的“入场券”,更是职业发展的“通行证”,但备考过程…

2026/11/12 0:04:01

双流电工证考别怕挂科!郑州报考避坑指南全攻略

双流电工证考别怕挂科!郑州报考避坑指南全攻略 实操考试心里没底怕挂科?双流电工证考在郑州报名,很多人因为不了解流程、准备不充分,白白浪费时间和金钱。作为三门峡应届工程类毕业生,我也曾经历过这样的焦虑。今天就从零开始,手把手教你如何避开郑州报…

2026/11/12 0:04:01

之前考的证在外省能不能转过来 怎么报名考电工证培训价格多少

之前考的证在外省能不能转过来 怎么报名考电工证培训价格多少 之前考的证在外省能不能转过来?这个问题确实让不少准备换城市工作的朋友犯了难,尤其像电工证这种实操性很强的证书,很多人担心跨省后证书能不能继续用。其实,只要符合相关流程,是可以顺利转…

2026/11/11 20:27:01

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/12 9:51:42

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑