Redis...2

📅 2026/6/24 5:33:00
Redis...2
优惠卷秒杀问题01.优惠券秒杀-全局唯一ID1.id是会展示给用户的如果id的规律性太强会让用户观察出一定的信息2.受表单数据的限制订单量是和容易积累的在多年的运营下订单量很可能超过了单表所能存储的最大数量此时开始新的一个表来存储的时候自增的ID就会出现重复的情况确保安全性前三十一位为时间戳记录下单时间以秒为单位序列号秒内的计数器支持每秒产生2的三十二次方个id确保唯一性的方法不同时间不在同一秒的id依靠时间戳来决定相同时间在同一秒的id依靠序列号来分辨02-Redis实现全局唯一id变化的key1.因为如果时间的key都是同一个那么同一秒内的时间戳包括时间戳和序列号可能会重复造成key相同所以每一天的key的前缀都不同的话就不会重复2.便于统计年月日是变化的前缀可以统计某年某月某日的订单量public class RedisWorker { /* 时间戳 */ private static long BEGIN_TIME 1640995200; /* 序号号位数 */ private static long COUNT_BIT 32; private StringRedisTemplate stringRedisTemplate; //全局ID生成器 public Long nextId(String prefix){ //1.生成时间戳 LocalDateTime now LocalDateTime.now(); long epochSecond now.toEpochSecond(ZoneOffset.UTC); long timestamp epochSecond - BEGIN_TIME; //2.生成序列号 //2.1生成当前的日期--需要传入的参数 String date now.format(DateTimeFormatter.ofPattern(yyyyMMdd)); //2.2自增长 long increment stringRedisTemplate.opsForValue().increment(icr prefix date); //3.拼接 return timestamp COUNT_BIT | increment; } public static void main(String[] args) { //生成当前时间的时间戳 LocalDateTime localDateTime LocalDateTime.of(2022, 1, 1, 0, 0, 0); //确定时区--不确定时区无法确定时间戳 long epochSecond localDateTime.toEpochSecond(ZoneOffset.UTC); //打印时间戳 System.out.println(epochSecond); } }03-添加优惠券{ shopId: 1, title: 100元代金卷, subtitle: 周一到周日均可使用, rules: 全场通用, payValue: 8000, actualValue: 10000, type: 1, stock: 100, beginTime: 2022-01-01T10:09:17, endTime: 2030-01-01T10:09:10 }04-实现秒杀下单05-库存超卖问题分析库存超卖问题出现的原因资源临界时同时访问都判断为可以执行结果超卖了悲观锁认为线程安全问题一定会发生因此在操作数据之前先获取锁确保线程串行执行synchronizedlock都属于悲观锁乐观锁认为线程安全问题不一定会发生只是在更新数据的时候判断有没有其他线程对数据作出修改没有修改则认为是安全的自己才更新数据如果已经被其他线程修改则说明发生了线程安全问题此时可以重试或者异常1.版本号法2.简化方法--用库存代替版本号--看前后两次查到的库存是否一致06-乐观锁解决超卖只加入这一句会导致在多个请求同时发生的时候只有一个能成功应该在库存前后不同的时候应该重试或者可以理解为在库存大于零的时候不用管前后库存是否一致只要0就可以卖出去07-实现一人一单功能在扣减库存之前加一步根据优惠卷id和用户id查询订单---根据订单是否存在判断该用户是否下过单/** * 优惠卷秒杀 * param voucherId * return */ Override public Result seckillVoucher(Long voucherId) { //1.查询优惠卷 SeckillVoucher voucher seckillVoucherService.getById(voucherId); //2.判断优惠卷是否存在 if(voucher null){ return Result.fail(优惠卷信息不存在); } //3.判断优惠卷是否过期或者未开始 if(voucher.getBeginTime().isAfter(LocalDateTime.now())){ return Result.fail(优惠卷还未开始); } if(voucher.getEndTime().isBefore(LocalDateTime.now())){ return Result.fail(优惠卷已经结束); } //4.判断优惠卷库存是否充足 if(voucher.getStock()1){ return Result.fail(优惠卷库存不足); } Long id UserHolder.getUser().getId(); //锁加在外面可以在事务提交之后再释放锁防止事务未提交订单未新增锁就释放导致不一致 synchronized (id.toString().intern()) { return createVoucherOrder(voucherId); } Transactional private Result createVoucherOrder(Long voucherId) { //5.判断当前用户购买的优惠卷是否超过单个用户的购买限制 User user UserHolder.getUser(); Long userId user.getId(); Integer count query().eq(voucher_id, voucherId).eq(user_id, userId).count(); if (count 0) { return Result.fail(该用户已经下过单了); } //6.扣减库存 if (!seckillVoucherService.update() .setSql(stock stock - 1) .eq(voucher_id, voucherId) .gt(stock, 0).update()) { return Result.fail(扣减库存失败); } //7.新建订单 VoucherOrder voucherOrder new VoucherOrder(); //7.1新建订单信息---订单id Long l redisWorker.nextId(SECKILL_STOCK_KEY); voucherOrder.setId(l); //7.2订单id voucherOrder.setVoucherId(voucherId); //7.3用户id voucherOrder.setUserId(userId); //8.将订单保存到数据库 save(voucherOrder); return Result.ok(); }不在方法上加锁的原因在方法上加锁会让锁的对象固定为this一个对象一个锁不满足我们对锁的要求08-集群下的线程并发安全问题1.修改服务的端口号编辑配置修改选项添加虚拟机选项2.修改nginx.conf文件配置反向代理和负载均衡分布式锁01-基本原理和不同实现方式对比在分布式锁中的锁监视器--确保多个进程看到同一个锁监视器02-Redis的分布式锁实现思路分布式锁要监视到所有的线程包括多个进程内的线程这个时候就要使用别的应用来监视线程如果在添加锁和添加锁过期时间之间出现了问题还是会导致死锁所以要保证添加锁和添加锁的过期时间的原子性可以把两步合成一步在获取锁失败之后有两种方式1.阻塞式等待在等待其他线程释放锁2.非阻塞式等待尝试获取锁失败之后立即返回一个结果而不是一直等待或者一直获取03-实现Redis分布式锁版本1ILook接口public interface ILook { /** * 尝试获取锁 * param timeOut * return */ boolean tryLook(Long timeOut); /** * 尝试删除锁 */ void delLook(); }ILook的实现类public class SimpleRedisLock implements ILook { private String name; private StringRedisTemplate stringRedisTemplate; private String prefix lock:; //锁的名称和StringRedisTemplate都是需要外面传递过来的 public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) { this.stringRedisTemplate stringRedisTemplate; this.name name; } Override public boolean tryLook(Long timeOut) { long threadId Thread.currentThread().getId(); //包装类有为null的风险此时直接传递给基本数据类型boolean就会报空指针 Boolean b stringRedisTemplate.opsForValue().setIfAbsent(prefix name, threadId , timeOut, TimeUnit.SECONDS); //所以选择判断是否为true return Boolean.TRUE.equals(b); } Override public void delLook() { stringRedisTemplate.delete(prefix name); } }修改代码synchronized只对单一进程有效redis的setnx可以对多个进程有效04-Redis分布式锁误删问题会导致的问题把别人的锁给删掉了导致别的线程获取到了锁开始执行业务造成多个线程并行执行任务根本原因线程一在删除锁的时候把线程二的锁给删了解决思路在删除锁的时候做一个判断判断是不是自己的锁05-解决Redis分布式锁误删问题不同的jvm虚拟机容易出现相同的线程的id所以要用uuid对虚拟机作出区分再拼接上不同的线程的idpublic class SimpleRedisLock implements ILook { private String name; private StringRedisTemplate stringRedisTemplate; private String KEY_PREFIX lock:; //不同的id前缀区分不同的虚拟机 private String ID_PREFIX UUID.randomUUID().toString()-; //锁的名称和StringRedisTemplate都是需要外面传递过来的 public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) { this.stringRedisTemplate stringRedisTemplate; this.name name; } Override public boolean tryLook(Long timeOut) { long threadId Thread.currentThread().getId(); //包装类有为null的风险此时直接传递给基本数据类型boolean就会报空指针 Boolean b stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX name, ID_PREFIX threadId , timeOut, TimeUnit.SECONDS); //所以选择判断是否为TRUE return Boolean.TRUE.equals(b); } Override public void delLook() { //删除锁的逻辑需要都修改为如下逻辑 //1.获取redis中的线程标识--也就是获取锁内的threadId String threadId stringRedisTemplate.opsForValue().get(KEY_PREFIX name); //2.获取当前线程的threadId long id Thread.currentThread().getId(); String nowId ID_PREFIX id; //3.比较当前的threadId与锁内的是否一致 if (threadId ! null threadId.equals(nowId)) { //如果相同则删除 stringRedisTemplate.delete(KEY_PREFIX name); } } }06-分布式锁的原子性问题判断锁标识和释放是两个动作之间产生了阻塞产生阻塞后相当于没有写判断锁误删的代码07-Lua脚本解决多条命令原子性问题redis的事务和mysql的事务不同redis的事务可以保证原子性但是保证不了一致性因为redis是把所有动作一次性处理没办法保证先查询再释放---做查询的时候拿不到结果只有所有动作全完成的时候才能拿到结果推荐使用lua脚本可以在一个lua脚本中执行多条redis指令确保多条命令执行时的原子性脚本需要的key类型的参数个数key中有含参的参数需要传入参数--参数为0就是没有含参的参数脚本的参数包括两个类型key类型和其它类型数字只表示key类型的参数的数量后面紧跟着的是key类型的参数key类型的参数后面是其他参数调用参数的时候key[]和argv[]不用加 只有命令部分需要用引起来参数部分不需要1. 第一条报错EVAL return redis.call(set,key[1],jack) 1 name # ERR user_script:1: Script attempted to access nonexistent global variable key原因Lua 脚本里必须用KEYS[1]而不是key[1]大小写敏感。修正EVAL return redis.call(set, KEYS[1], jack) 1 name2. 第二条报错EVAL return redis.call(set,name,jack) # ERR wrong number of arguments for eval command原因EVAL命令必须指定「key 的数量」即使你不用KEYS。修正EVAL return redis.call(set,name,jack) 00表示本次脚本没有通过KEYS传递的 key3. 第三条报错redis.call(set,name,jack) # ERR syntax error原因redis.call只能在EVAL/EVALSHA脚本中执行不能直接在命令行运行。修正EVAL return redis.call(set,name,jack) 0✅ 最终正确写法两种推荐方式规范写法推荐用 KEYS 传递 keyEVAL return redis.call(set, KEYS[1], ARGV[1]) 1 name jack这种写法通过KEYS和ARGV传递参数符合 Redis 的最佳实践支持集群环境。简单写法直接写死 key适合测试EVAL return redis.call(set,name,jack) 0关键要点总结EVAL命令格式EVAL lua脚本 key数量 [key1 key2 ...] [arg1 arg2 ...]Lua 脚本中访问 key 必须用KEYS[1]大写不能用key[1]即使不传递 key也必须写0作为 key 数量redis.call只能在EVAL脚本中执行不能直接在命令行调用动态传递参数08-Java调用lua脚本改造分布式锁--这是一个lua脚本 --redis.call(set,name:06:23,jack) --获取锁的key local lockKey KEY[1]; --获取当前线程的id local threadId ARGV[1]; --查询锁内的线程id local id redis.call(get,lockKey); --判断当前线程的id和锁内线程id是否一致 if (id threadId) then --一致则删除 return redis.call(del,locakKey); end return 0;private final String name; private final StringRedisTemplate stringRedisTemplate; private final String KEY_PREFIX lock:; //定义lua脚本 private static final DefaultRedisScriptLong UNLOCK_SCRIPT; //初始化lua脚本 static { UNLOCK_SCRIPT new DefaultRedisScript(); //设置lua脚本的位置 UNLOCK_SCRIPT.setLocation(new ClassPathResource(unlock.luau)); //设置lua脚本的返回值类型 UNLOCK_SCRIPT.setResultType(Long.class); } //不同的id前缀区分不同的虚拟机 private final String ID_PREFIX UUID.randomUUID().toString()-; //锁的名称和StringRedisTemplate都是需要外面传递过来的 public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) { this.stringRedisTemplate stringRedisTemplate; this.name name; } Override public boolean tryLook(Long timeOut) { long threadId Thread.currentThread().getId(); //包装类有为null的风险此时直接传递给基本数据类型boolean就会报空指针 Boolean b stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX name, ID_PREFIX threadId , timeOut, TimeUnit.SECONDS); //所以选择判断是否为TRUE return Boolean.TRUE.equals(b); } Override public void delLook() { //redis调用lua脚本 stringRedisTemplate.execute( UNLOCK_SCRIPT, Collections.singletonList(KEY_PREFIXname), Thread.currentThread().getId() ); }09-Redisson功能介绍