‘秒杀’功能实现

全局唯一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已存在在集合里就说明已经下单。

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

·

相关推荐
无籽西瓜a2 小时前
Docker 环境下 Redis Lua 脚本部署与执行
redis·docker·lua
疯狂成瘾者2 小时前
Redis 实用学习清单
redis·学习
七夜zippoe2 小时前
消息队列选型:Kafka vs RabbitMQ vs Redis 深度对比
redis·python·kafka·消息队列·rabbitmq
iMingzhen2 小时前
不想引入 Redis,我用一张 SQLite 表实现了消息队列
数据库·redis·ai·sqlite
Curvatureflight2 小时前
Redis实战:缓存设计与高频场景全解析
数据库·redis·缓存
我真会写代码3 小时前
从入门到精通:Redis实战指南,解锁高性能缓存核心能力
数据库·redis·缓存
haixingtianxinghai5 小时前
Redis的定期删除和惰性删除
数据库·redis·缓存
JavaGuide17 小时前
MiniMax M2.7 发布!Redis 故障排查 + 跨语言重构场景实测,表现如何?
redis·后端·ai·ai编程
weixin_4563216418 小时前
Java架构设计:Redis持久化方案整合实战
java·开发语言·redis