2026/1/16 1:48:10
网站建设
项目流程
网站制作 青岛,遵义市住房和城乡建设局官方网站6,wordpress显示全文,婚庆设备租赁网站源码总结#xff1a;本文探讨了Redis在秒杀业务中的应用#xff0c;重点介绍了全局唯一ID生成方案和分布式锁的实现。首先提出基于Redis的全局ID生成器设计方案#xff0c;通过时间戳序列号的组合方式保证ID唯一性。针对秒杀业务中的库存超卖问题#xff0c;分析了悲观锁和乐观…总结本文探讨了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_BITS32; 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(“stockstock - 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_PREFIXname,threadId,timeoutSec,TimeUnit.SECONDS); //避免空指针风险 return Boolean.TRUE.equals(success); } public void unlock(){ //获取线程标示 String threadId ID_PREFIXThread.currentThread().getId(); //获取锁中的标示 String id stringRedisTempalte.opsForValue().get(KEY_PREFIXname); //判断线程标示是否一致 if(threadId.equals(id)){ //释放锁 stringRedisTemplate.delete(KEY_PREFIXname); } } }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 DefaultRedisScriptLong 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_PREFIXname), ID_PREFIXThread.currentThread().getId()); }