尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

Redis...2

Redis...2
📅 发布时间:2026/6/24 5:36:08

优惠卷秒杀问题

01.优惠券秒杀-全局唯一ID

1.id是会展示给用户的,如果id的规律性太强会让用户观察出一定的信息

2.受表单数据的限制:订单量是和容易积累的,在多年的运营下,订单量很可能超过了单表所能存储的最大数量,此时开始新的一个表来存储的时候,自增的ID就会出现重复的情况

确保安全性

前三十一位为时间戳,记录下单时间,以秒为单位

序列号:秒内的计数器,支持每秒产生2的三十二次方个id

确保唯一性的方法:不同时间(不在同一秒)的id依靠时间戳来决定

相同时间(在同一秒)的id依靠序列号来分辨

02-Redis实现全局唯一id

变化的key,

1.因为如果时间的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-库存超卖问题分析

库存超卖问题出现的原因:资源临界时同时访问,都判断为可以执行,结果超卖了

悲观锁:认为线程安全问题一定会发生,因此在操作数据之前先获取锁,确保线程串行执行

synchronized,lock都属于悲观锁

乐观锁:认为线程安全问题不一定会发生,只是在更新数据的时候判断有没有其他线程对数据作出修改,

没有修改则认为是安全的,自己才更新数据

如果已经被其他线程修改则说明发生了线程安全问题,此时可以重试或者异常

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分布式锁版本1

ILook接口

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对虚拟机作出区分,再拼接上不同的线程的id

public 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 name

2. 第二条报错

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')" 0

(0表示本次脚本没有通过KEYS传递的 key)


3. 第三条报错

redis.call('set','name','jack') # ERR syntax error

原因:redis.call只能在EVAL/EVALSHA脚本中执行,不能直接在命令行运行。修正:

EVAL "return redis.call('set','name','jack')" 0

✅ 最终正确写法(两种推荐方式)

  1. 规范写法(推荐,用 KEYS 传递 key)

    EVAL "return redis.call('set', KEYS[1], ARGV[1])" 1 name jack

    这种写法通过KEYS和ARGV传递参数,符合 Redis 的最佳实践,支持集群环境。

  2. 简单写法(直接写死 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 DefaultRedisScript<Long> 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_PREFIX+name), Thread.currentThread().getId() ); }

09-Redisson功能介绍

相关新闻

  • 反射的定义、使用方式、优缺点和具体使用场景
  • 大模型内省适配器:基于LoRA的可解释性微调实践
  • 教育视频摘要技术TR-EduVSum的创新与应用

最新新闻

  • DeepSeek-V4终端编程助手:深思考+上下文感知的AI协作者
  • OpenClaw:Windows原生零代码AI工作流引擎
  • PXN20微控制器时钟系统深度解析:从架构原理到低功耗实战
  • 一个人干五人活:Claude-mem、Agents HQ与GitHub CLI协同实战
  • Hermes Agent安装指南:本地AI工作台的零配置部署实践
  • 本地AI Agent+Obsidian构建离线智能工作流

日新闻

  • 终极指南:如何用shadPS4在电脑上免费畅玩PS4游戏
  • 打造个性化Instagram Clone:主题定制与用户体验优化技巧
  • 未来展望:RoseTTAFold-All-Atom的发展路线图与社区支持资源汇总

周新闻

  • Visual C++运行库修复终极指南:5分钟快速解决Windows软件启动错误
  • 手把手教你构建统计局地区经济数据爬虫:从环境搭建到数据持久化全指南
  • 2026多Agent深度解析:用AI团队替代单一模型,四种架构实战落地

月新闻

  • 【总结】入门篇:50句话让你记住架构核心概念
  • WeChatMsg技术方案解析:实现Mac微信数据自主管理的完整解决方案
  • WeChatMsg:革新性微信数据备份方案,打造你的专属数字记忆库

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号