news 2026/3/8 17:05:49

Redis在秒杀业务中的应用

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Redis在秒杀业务中的应用

总结:本文探讨了Redis在秒杀业务中的应用,重点介绍了全局唯一ID生成方案和分布式锁的实现。首先提出基于Redis的全局ID生成器设计方案,通过时间戳+序列号的组合方式保证ID唯一性。针对秒杀业务中的库存超卖问题,分析了悲观锁和乐观锁的解决方案及各自优缺点。对于一人一单场景,详细说明了synchronized锁的局限性及分布式锁的必要性。最后深入讲解了Redis分布式锁的实现原理,包括误删问题的解决方案和Lua脚本保证原子性的方法,提供了一套完整的分布式锁实现代码。这些技术方案共同构成了高并发秒杀系统的核心保障。

Redis解决秒杀业务

全局唯一ID:用全局ID生成器,是一种在分布式系统下用来生成唯一ID的工具,满足了唯一性、高可用、高性能、递增性、安全性。

Redis实现全局唯一ID:使用String类型的increment满足了前4种特性,但是不满足安全性,因此我们可以不使用它的自增,给它拼接一些其它信息。拼接ID的组成由符号位+时间戳+序列号来组成,符号位1bit永远为正的是0,时间戳31bit以秒为单位可以使用69年,序列号32bit每秒产生2的32次方个不同ID,用于在同一秒很多人下单时间戳一样时防止ID一样。

@Component public class RedisIdWorker{ @Resource private StringRedisTemplate stringRedisTemplate; private static final long BEGIN_TIMESTAMP=”获取到的规定的时间多少秒”; private static final long COUNT_BITS=32; public long nextId(String keyPrefix){ //1.生成时间戳 LocalDateTime now = LocalDateTime.now(); long nowSecond = now.toEpochSecond(ZoneOffset.UTC); long timestamp = nowSecond - BEGIN_TIMESTAMP; //2.生成序列号 //2.1获取当前日期,精确到天 Sting date = now.format(DateTimeFormatter.ofPattern(“yyyy:MM:dd”)); //拼上date每天一个key防止总量超过 long count = stringRedisTemplate.opsForValue().increment(“incr:”+keyPrefix+”:”+date); //3.拼接并返回,用到位运算,因为返回值是long类型 return timestamp << COUNT_BITS | count; } }

全局唯一ID生成策略:UUID、Redis自增、snowflake算法、数据库自增(按理说是不能用的,但是这自增的是使用一张表的自增,而不是字段一个个自增,跟redis自增很像)。

实现优惠券秒杀下单:

@Override @Transactional 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); //7.返回订单id return Result.ok(orderId); }

库存超卖问题:在高并发下,原本可能100的库存会卖出多于100件,出现超卖问题。也就是java中学到的线程并发安全问题。

解决超卖问题:加锁,悲观锁和乐观锁。

悲观锁:像synchronized、lock互斥锁都属于悲观锁,认为线程安全问题一定会发生,因此在获取数据之前先加锁,确保线程串行执行。

乐观锁:认为线程安全问题不一定会发生,因此不加锁,只是在更新数据时去判断有没有其他线程对数据进行了修改。乐观锁的关键是如何判断之前查询的数据有没有被修改,常见的方式有2种:版本号法和CAS法,一个是通过sql条件查询或用eq()来判断版本号的变化来判断库存是否变化,一个是通过sql条件查询或用eq()库存直接判断库存的变化来判断。

乐观锁的弊端:虽然乐观锁解决了库存超卖,但是会出现库存没卖完就结束了,如每个都抢占100号库存,一个线程拿走后,其他线程判断到的是库存不等于100,就失败直接退出了,那这就是乐观锁的一个弊端成功率太低了。

解决弊端:

方案1,直接将库存判断是否相等改为判断库存是否大于0

方案2,有些就不是库存,只能通过数据来判断是否安全,这时就可以用分段锁也就是分批加锁,也就是将数据分成好几份,用户在抢的时候可以在多张表中分别去抢,这样就可以提高成功率。

当然,乐观锁解决库存超卖其实并不是最完美的,最终毕竟还要去访问数据库,会给数据库带来很大的压力,所以在真正的秒杀场景高并发下,还需要一些优化。

一人一单

根据优惠券id和用户id查询订单,判断订单是否已经存在,已经存在则不让再买,不存在则创建订单,由于都是未存在,可能多个线程同时查询访问抢库存时,都会创建订单,这样会有并发安全问题。所以还要给查询订单、判断订单、创建订单这一整段代码加上事务加上synchronized锁。

//先获取用户id,再对这一整段封装的代码加synchronized锁,保证先提交事务再释放锁,确保事务生效,还要用到事务代理 Long userId = UserHolder.getUser().getId(); synchronized(userId.toString().intern()){ IVoucherOrderService proxy=(IVoucherOrderService)AopContext.currentproxy(); return proxy.createVoucherOrder(voucherId); } //由于每次释放完锁,每次下一个用户都会new获取这个id对象,耗费内存所以用intern()来减少开销。

intern 方法是 Java 中String 类提供的一种优化内存使用的机制,用于将字符串对象存储到字符串常量池(String Constant Pool)中,并返回池中字符串的引用。它的主要作用是通过复用字符串对象,减少内存开销并提高性能。

注意:虽然已经解决了一人一单的并发安全问题,但是在集群模式下或有些分布式系统还是会出现并发安全问题,synchronized锁失效,它只用于同一台jvm,也就是说进行新的部署,部署新的tomcat服务器,也就是有2台jvm时,它们有自己的堆栈、自己的方法区,会有新的锁监视器,相当于又变成2个线程创建同一个订单,甚至要是有10台20台jvm就会导致更多线程安全问题。要解决这个问题,就是想办法让多台jvm都用同一把锁,分布式锁。

分布式锁

分布式锁:满足分布式系统下或集群模式下多进程可见(多个jvm都能被看到,像redis、mysql、或让锁监视器都能监控每个jvm线程)并且互斥的锁。

分布式锁的特性:多进程可见、互斥、高性能、高可用、安全性等等。

分布式锁的实现核心是多进程之间的互斥,常见的有3种实现:

基于Redis实现分布式锁:

public class SimpleRedisLock implements ILock{ private String name; private StringRedisTemplate stringRedisTemplate; public SimpleRedisLock(String name , StringRedisTemplate stringRedisTemplate){ this.name = name; this,stringRedisTemplate = stringRedisTemplate; } private static final Sring KEY_PREFIX = “lock”; //下面介绍的分布式锁误删问题,用UUID+线程ID来做线程标示 Private static final String ID_PREFIX = UUID.randomUUID().toString(true+”-”); @Override public boolean tryLock(long timeoutSec){ //获取线程标示 String threadId = ID_PREFIX + Thread.currentThread().getId(); //获取锁 Boolean success = stringRedisTemplate.opsForValue() .setIfAbsent(KEY_PREFIX+name,threadId,timeoutSec,TimeUnit.SECONDS); //避免空指针风险 return Boolean.TRUE.equals(success); } public void unlock(){ //获取线程标示 String threadId = ID_PREFIX+Thread.currentThread().getId(); //获取锁中的标示 String id = stringRedisTempalte.opsForValue().get(KEY_PREFIX+name); //判断线程标示是否一致 if(threadId.equals(id)){ //释放锁 stringRedisTemplate.delete(KEY_PREFIX+name); } } }

Redis分布式锁误删问题:某些极端情况下还是会有线程安全问题,线程1由于业务阻塞,业务还没执行完达到了超时时间,导致锁提前释放,这时线程2趁虚而入获取到了锁,但是线程1不知道呀,线程1业务好了开始释放锁,它释放的是线程2的锁。要解决这个问题,关键是获取锁时要把线程标示一起存进去,在释放锁前做一个判断,判断这个线程标示是不是自己的,如果是自己的才能释放锁。

分布式锁的原子性问题:解决了分布式锁误删问题,其实还是有问题。在释放锁的时候也可能发生阻塞,它不是业务阻塞而是如jvm垃圾回收,就会导致锁超时释放,别的线程又可以趁虚而入,这时别的线程就获取到了锁,这时原本准备释放锁的线程开始释放线程,但是已经判断完了锁是自己的,可事实上锁并不是自己的,就把别的线程的锁删掉了又发生误删问题。要解决这个问题,就要保证判断锁标示和释放锁是一个原子性操作,就是保证它们是一起的。

Redis的Lua脚本:Redis提供了Lua脚本功能,在一个脚本编写多条Redis命令,确保多条命令执行时的原子性。Lua是一种编程语言,基本语法参考官方文档。

那要如何调利用Lua语言去调redis?

用redis官方提供的函数redis.call(‘命令名称’ , ’key’ , ’其它参数’ , ...);

例如,redis.call(‘set’ , ’name’ , ’jack’);

写好Lua脚本后要用redis命令来调用脚本,EVAL script numkeys key [key ...] arg [arg ...]

例如,EVAL ”return redis.call(‘set’ , ‘name’ , ‘jack’)” 0 //0是指key类型的参数个数

如果脚本的key、value不想写死,可以作为参数传递。Key类型参数会放入KEYS数组,value类型参数会放入ARGV数组,在脚本中可以从KEYS、ARGV数组中获取参数。

例如,EVAL “return redis.call(‘set’ , ‘KEYS[1]’ , ‘ARGV[1]’) ”1 name jack //Lua的索引是从1开始

调用Lua脚本改造分布式锁代码实现:建个lua文件,然后调用,

在unlock.lua文件中编写:

if(redis.call(‘get’ ,KEYS[1] ) == ARGV[1]) then return redis.call(‘del’ , KEYS[1]) end return 0; private static final DefaultRedisScript<Long> UNLOCK_SCRIPT; static{ UNLOCK_SCRIPT = new DefaultRedisScript<>(); UNLOCK_SCRIPT.setLocation(new ClassPathResource(“unlock.lua”)); UNLOCK_SCRIPT.setResultType(Long.class); } @Override public void unlock(){ //调用Lua脚本 stringRedisTemplate.execute(UNLOCK_SCRIPT, //这边参数传的是集合,所以要将字符串转成集合 Collections.singletonList(KEY_PREFIX+name), ID_PREFIX+Thread.currentThread().getId()); }
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/3/5 11:08:08

产品经理如何拥抱AI大模型:从入门到实战的全面指南

本文针对产品经理&#xff0c;探讨了在AI大模型时代如何保持竞争力。文章首先强调了产品经理需具备的核心能力&#xff0c;包括理解用户需求、把握市场趋势等&#xff1b;其次详细阐述了AI大模型为产品经理带来的五大价值&#xff0c;如提升用户洞察、实现个性化推荐等&#xf…

作者头像 李华
网站建设 2026/3/5 3:57:20

PHP 8.6扩展性能优化秘籍:提升执行效率300%的底层策略

第一章&#xff1a;PHP 8.6 扩展开发概述PHP 8.6 作为 PHP 语言持续演进的重要版本&#xff0c;进一步优化了内核性能并增强了扩展开发的灵活性与稳定性。扩展开发允许开发者使用 C 语言直接与 Zend 引擎交互&#xff0c;实现高性能功能模块&#xff0c;适用于底层系统集成、算…

作者头像 李华
网站建设 2026/3/5 2:56:21

SoapUI接口测试脚本开发:从基础到进阶实践

接口测试在现代化软件测试体系中的关键地位 随着微服务架构和分布式系统的普及&#xff0c;接口测试已成为保证软件质量的核心环节。根据业界统计数据&#xff0c;现代软件系统中超过70%的功能交互通过接口实现&#xff0c;这使得接口测试的覆盖率直接影响产品的稳定性和可靠性…

作者头像 李华
网站建设 2026/3/5 2:45:29

Matlab+YALMIP+CPLEX求解带储能的微电网优化调度问题的解决方案

MatlabYALMIPCPLEX求解带储能的微电网优化调度问题最近在折腾微电网优化调度的课题&#xff0c;发现用MatlabYALMIPCPLEX这套组合拳处理这类问题贼方便。特别是涉及到储能系统的时间耦合约束&#xff0c;用YALMIP建模比手写矩阵舒服太多了。今天咱们就通过一个24小时调度案例&a…

作者头像 李华
网站建设 2026/3/5 2:48:28

PostgreSQL 中的“脏页(Dirty Pages)”是什么?

PostgreSQL 以固定大小的数据块&#xff08;Page&#xff09;存储数据&#xff0c;默认大小为 8 KB。当客户端执行更新或插入操作时&#xff0c;PostgreSQL 并不会立即将变更写入磁盘&#xff0c;而是先将相关数据页加载到共享内存&#xff08;Shared Buffers&#xff09;中&am…

作者头像 李华
网站建设 2026/3/5 3:28:29

Simpack与Abaqus联合仿真,探索轨道与结构的动态魅力

simpack与abaqus联合仿真&#xff0c;包括柔性钢轨建模&#xff0c;fbi文件生成&#xff0c;钢弹簧浮置板搭建&#xff0c;轨道不平顺激励等&#xff0c;包括模型。轨道与结构的动力学仿真一直是我研究的重点领域。最近&#xff0c;我有幸接触到Simpack与Abaqus的联合仿真方法&…

作者头像 李华