‘秒杀’功能实现

全局唯一id

当用户抢购时,就会生成订单并保存到订单表中,而订单表如果使用数据库自增id就会存在一些问题:

  • id的规律性太明显

  • 受单表数据量限制,如果订单很多,多张表存储还采用自增就会出现订单id重复。

全局ID生成器:

使用redis来生成全局唯一id。

全局ID生成器,是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足以下特性:

  • 唯一性、递增性、安全性

  • 高可用、高性能

为了增加id的安全性,我们可以不直接使用redis自增的数值,而是拼接一些其他的信息:

把一个Long类型,64位用来构成id

  • 符合位:1bit,永远为0

  • 时间戳:31bit,以秒为单位,可以使用69年,确定一个标准时间,用当前时间减去标准时间得到秒值作为时间戳

  • 序列号:32bit,秒内的计数器,支持每秒产生2的32次方个不同ID

全局唯一ID生成策略:

  • UUID

  • Redis自增

  • 雪花算法64位

Redis自增ID策略:

  • 每天一个key,方便统计订单量

  • ID构造是 时间戳+计数器

秒杀下单基本流程:
  1. 查询优惠卷信息,判断时间是否到秒杀时间

  2. 判断库存是否充足

  3. 都满足则扣减库存

  4. 创建订单,返回订单id

并发问题导致超卖:

当库存为1时,线程a执行扣减库存操作前,其他线程同时查询了库存并拿到库存为1,导致问题发生。

超卖问题是典型的多线程安全问题,针对这一问题的常见解决方案就是加锁:

  • 悲观锁 :认为线程安全问题一定会发生,因此在操作数据之前先获取锁,确保线程串行,比如Synchronized、Lock都属于悲观锁

  • 乐观锁 :认为线程安全问题不一定发生,因此不加锁,只是在更新数据时去判断有没有其他线程对数据做了修改。如果没有修改则认为是安全的,自己才更新数据,如果已经发生修改说明发生了安全问题,此时可以重试或异常。

乐观锁实现方式:
  1. 版本号法:在操作的记录加上版本号字段,每次查询库存同步查出来版本号,后续扣减库存时,同步增加版本号,同时判断版本号是否为最初拿到的版本号。

    复制代码
    set store=store-1,version=version+1 where id=xx
    and version =当初查出来的版本号
  2. cas法:也就是版本号的升级,我们都是比较版本号跟最初是否发生变化,其实可以直接拿库存跟最初是否发生变化来实现

    复制代码
    set store=store-1 where id=xxx and store=最初查出来的库存 因为库存一定是会变化的,所以可以根据这个来实现

    乐观锁的弊端 :当同一时刻有大量线程拿到同一个初始值的时候,就会出现大量的线程它不能去执行操作,这个时候就需要去重试,也就是再重新去拿初始值,然后再比较,在执行。因此在并发量很高的时候,乐观锁的效率其实不高,因为要大量的自旋重试

    所以在扣减库存的时候,同一时刻大量线程拿到同一初始值,就会导致很多扣减库存的操作执行不了,及时下载库存大于0,属于就需要改造一下,不再判断必须等于原来初始值,只需要让条件变为库存大于0即可

    复制代码
    set store=store-1
    where id=xxx and store>0
实现一人一单:

要实现一个人只能买一单,也就是说在扣减库存之前我们先判断一下订单哩是否有这个userid跟优惠卷id这条记录就可以了。

但这时候也有并发问题,也就是在同一时刻大量线程都判断了订单里还没有这条记录,就会导致一人多单。

这个时候就需要加锁,由于订单还没创建没有初始值,就无法使用乐观锁,就只能上悲观锁Synchronize 。

上悲观锁需要注意的就是锁的对象以及锁的范围,这里锁的范围就是 判断是否存在、减库存,创建订单 ,锁的对象就是userid 因为我们不能让同一个用户多次执行下单操作,所以对用户id上锁。这里锁的对象就是:userid.toString.instren() 把这个这个userid的值作为字符串放到常量值,锁住这个值,后面只要这个值一样拿到的就是同一个对象。

前面说锁的范围包括了多个数据库操作,就要上事务,但是我们事务的模块也是只针对那个方法,所以这里要注意事务的失效,不能单纯的使用this. 去调用方法,需要拿到这个类对应的代理对象,然后调用这个方法。

jvm自带的锁在集群模式下的问题:

当服务部署在集群模式下的时候,多个请求被负载均衡到不同的服务上,这时候每个服务对应的jvm是不同的,拿到的锁监视器也是不同的。这时候锁就失效了。

就需要分布式的锁,不再依赖jvm内部的锁。

JVM只能保证单个服务内的线程互斥,无法保证多个服务间的线程互斥。

Redis实现分布式锁:

分布式锁:满足分布式系统或集群模式下多进程可见并互斥的锁。

实现分布式锁时需要实现的两个基本方法:

  • 获取锁:

    • 互斥:确保只能有一个线程获取锁

    • 非阻塞:尝试一次,成功返回true,失败返回false

      复制代码
      set lock thread1 nx ex 10
  • 释放锁:

    • 手动释放

    • 超时释放:获取锁时添加一个超时时间

      复制代码
      del key
  1. 尝试获取锁

  2. 获取锁成功则执行业务,执行完业务后释放锁

  3. 获取锁失败则返回或等待

  4. 业务超时或服务宕机则自动释放锁

改进误释放其他线程的锁问题:

  1. 在获取锁时存入线程标识(使用UUID表示)

  2. 在释放锁时先获取锁中的线程标识,判断是否与当前线程标识一致

    • 如果一致则释放锁

    • 不一致则不释放锁

改进无原子性带来的并发问题:

当线程判断后又因为特殊原因阻塞,恢复后直接删除锁,导致误删其他线程的锁。

核心原因:判断锁是否属于当前线程跟释放锁的操作不是原子操作。

Redis的Lua脚本:

能够在一个脚本中编写多条redis命令,确保多条命令执行时的原子性。

Lua脚本的读取:

我们可以在resource路径下定义一个lua脚本的文件,把要执行的lua脚本配置进去,来避免硬编码,后续只需去读取特定文件即可:

复制代码
private static final DefaultRedisScript
UNLOCK_SCRIPT;
static{
    UNLOCK_SCRIPT =new DefaultRedisScript();
    UNLOCK_SCRIPT.setLocation(new ClassPathResoure("unlock.lua"));
    UNLOCK_SCRIPT.setResultType(Long.class);
}

Lua脚本的调用:

复制代码
RedisTemplate.execute(UNLOCK_SCRIPT,keys,args)
keys为lua脚本中要用到的key,集合类型
args 为要用到的参数,可变参数类型
Redisson:

在基础功能上解决:不可重入、不可重试、超时释放、主从一致性

使用Redisson:
Redisson可重入锁原理:

利用hash结构,在存储value的时候 filed存储线程标识,value存储锁计数。

再尝试获取锁的时候判断锁标识是否是自己,如果是自己则将锁计数加1。

释放锁的时候锁计数-1

全部过程采用lua脚本的形式执行。

Redisson的锁重试机制和锁续期(watchDog机制):

Redisson默认尝试获取锁时间:-1,也就是不尝试,直接失败

Redisson默认看门狗续期时间:30秒

  • 可重入:利用hash结构记录线程id和冲入次数

  • 可重试:利用信号量和Pubsub功能实现等待、唤醒,获取锁失败的重试机制

  • 超时续约:利用watchDog,每隔一段时间(releaseTime/3),重置超时时间,当有指定锁自动释放时间时,看门狗机制会失效。当释放锁时,会取消锁续期。

Redis优化秒杀业务:

在判断库存是否充足和一人一单的环节,之前都是读取数据库,我们可以尝试把数据拿到redis中,通过redis1来做判断提高性能。

这里只需要记录一下库存,再用set结构记录userid,如果id已存在在集合里就说明已经下单。

对于数据库的写操作,我们可以缩减流程,不再业务中同步完成,因为这些环节用户是感知不到的,我们可以通过消息队列异步的去处理这些写操作。

·

相关推荐
IT大白鼠3 天前
Redis 系列 · 第 01 篇——认知入门:Redis 是什么
redis·nosql
彧azz3 天前
Linux 环境下 Redis 学习总结:数据类型、持久化、锁、事务、主从与缓存问题
linux·redis·笔记·学习·面试
lbb 小魔仙3 天前
OpenClaw + cpolar 实战:远程 NAS、分享小游戏、RDP,再配置公网 AI 入口
数据库·人工智能·redis·oracle·prometheus
夕除3 天前
redis--018
redis
两锭子3 天前
Redis基础
redis
烟沙九洲3 天前
Redis 缓存穿透、缓存击穿、缓存雪崩
数据库·redis
字节探索3 天前
Redis 只会当缓存用?这 10 大实战场景,让你的系统快到飞起
redis·后端
于樱花森上飞舞3 天前
【Redis】哨兵详解
java·开发语言·数据库·redis
Rain的Java大神之路3 天前
如何快速上传10G文件
java·spring boot·redis·后端·mysql·spring cloud·面试
Wang's Blog3 天前
Java 接入Redis: 通用命令与键管理
java·服务器·redis