温故而知新-秒杀项目篇【面试复习】

news2024/11/21 2:29:31

温故而知新-秒杀项目篇【面试复习】

  • 前言
  • 版权
  • 推荐
  • 温故而知新-论坛项目篇【面试】
    • 秒杀项目中注册模块怎么实现的?
    • 秒杀项目中登录模块怎么实现的?
    • 秒杀项目中显示登录用户信息怎么实现的?
    • SessionStorage是什么?
    • 为什么不用session而用token
    • 什么是序列化和反序列化
    • 秒杀接口限流防刷
    • 秒杀项目中为什么使用rocketmq
    • 异步化扣减库存是怎么实现的
    • 下单的流程
    • 如何解决超卖
      • 使用redis的原子命令:decrement
      • 改成一个Lua脚本
      • redis分布式锁实现
      • 怎么合理地设置分布式锁的过期时间
      • Redisson
    • redisson分布式锁的原理
    • redisson分布式锁比setnx有什么优势?
  • 优化
    • 安全优化
    • 功能优化
    • 性能优化
  • 设计
    • 高并发下如何设计秒杀系统?
      • 页面静态化
      • 按钮至灰控制
      • 服务单一职责
      • 秒杀链接加盐
      • 限流
      • 分布式锁
      • MQ 异步处理
      • 限流&降级&熔断
    • 在秒杀场景中,常用的限流算法有哪些?
      • 1、什么是限流?
      • 2、限流算法
  • 最后

前言

2023-7-31 16:01:39

以下内容源自《【面试复习】》
仅供学习交流使用

版权

禁止其他平台发布时删除以下此话
本文首次发布于CSDN平台
作者是CSDN@日星月云
博客主页是https://blog.csdn.net/qq_51625007
禁止其他平台发布时删除以上此话

推荐

温故而知新-论坛项目篇【面试】

秒杀项目中注册模块怎么实现的?

用户:

  • 点击个人注册
  • 输入手机号
  • 点击获取验证码

前端请求后端:获取验证码

// 生成OTP 
UUID4位的随机数(0~9)
// 绑定OTP
2用户注册与登录:后端:把验证码存入到session中
6分布式状态管理:存入到redis中的<phone,otp,5mins>
// 发送OTP
输出到控制台里

改为短信发送 
短信验证码【java提高】 https://jsss-1.blog.csdn.net/article/details/125790714

用户填写信息,点击立即注册

前端请求后端:注册

UserController:调用register()

// 验证OTP 
2用户注册与登录:session.getAttribute()
6分布式状态管理:取到phone对应的otp
// 加密处理
// 注册用户

秒杀项目中登录模块怎么实现的?

用户输入手机号和密码

前端请求后端:登录
后端调用login()

校验用户名和密码

2用户注册与登录:存入到session中
6分布式状态管理:存入到redis中的token中 返回token给前端
前端:把返回的token存入到浏览器sessionStorage
每次ajax异步请求都会带上token,为了得到当前登录用户

秒杀项目中显示登录用户信息怎么实现的?

调用UserController:getUser()

2用户注册与登录:从session中取出
6分布式状态管理:在redis中取出

SessionStorage是什么?

https://juejin.cn/post/6844903975800537096

本质就是存在于浏览器上的 hash(哈希表)。
localStorage生命周期是永久,这意味着除非用户显示在浏览器提供的UI上清除localStorage信息,否则这些信息将永远存在。存放数据大小为一般为5MB,而且它仅在客户端(即浏览器)中保存,不参与和服务器的通信。

sessionStorage 的所有性质基本上与 localStorage 一致,唯一的不同区别在于:
sessionStorage 的有效期是页面会话持续,如果页面会话(session)结束(关闭窗口或标签页),sessionStorage 就会消失。而 localStorage 则会一直存在。

为什么不用session而用token

cookie和session及token,为什么选择使用token?JWT使用

「揭秘」Token与Session到底有何异同?
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

什么是序列化和反序列化

https://blog.csdn.net/weixin_44211968/article/details/128112325

序列化:把对象转化为可传输的字节序列过程称为序列化。
反序列化:把字节序列还原为对象的过程称为反序列化。

秒杀接口限流防刷

Java项目笔记之秒杀接口防刷限流
https://www.shangyexinzhi.com/article/2741037.html

秒杀项目中为什么使用rocketmq

RocketMQ和其他消息中间件最大的一个区别是支持了事务消息,
这也是分布式事务里面的基于消息的最终一致性方案。

【RocketMQ】高级使用:四个问题详解事务消息

在redis中预减缓存,防止超卖
mq保证mysql中库存最终被扣减

异步化扣减库存是怎么实现的

redis存储库存,mq保证最终一致性

OrderController.create()
抛出:下单失败

OrderServiceImpl.createOrderAsync()

如果已经售罄了:hasKey()
抛出:已经售罄

生成库存流水:createItemStockLog()
输出:生成库存流水完成

输出:投递扣减库存事务消息
发送事务消息:sendMessageInTransaction
抛出:创建订单失败

LocalTransactionListenerImpl.executeLocalTransaction()

执行本地事务:createOrder()

抛出:执行MQ本地事务时发生错误

LocalTransactionListenerImpl.createOrder()

输出:本地事务提交完成
return RocketMQLocalTransactionState.COMMIT;

抛出:创建订单失败&更新流水完成
return RocketMQLocalTransactionState.ROLLBACK;

OrderServiceImpl.createOrder()

预扣库存:decreaseStockInCache()

**ItemServiceImpl.decreaseStockInCache()**
在redis中实现:decrement
库存为0是添加售罄标识 `<"item:stock:over:" + itemId, 1>`

输出:回补库存完成|售罄标识完成
return result>=0;

输出:预扣减库存完成
抛出:库存不足

生成订单
输出:生成订单完成

更新销量,发送异步消息
输出:投递增加商品销量消息成功|输出:投递增加商品销量消息失败

{
	**IncreaseSalesConsumer.onMessage()**
	itemService.increaseSales(itemId, amount);
	输出:更新销量完成|更新销量失败
}

更新库存流水的状态
输出:更新流水完成

DecreaseStockConsumer.onMessage()

itemService.decreaseStock(itemId, amount)
输出:最终扣减库存完成|输出:从DB扣减库存失败

下单的流程

点击商品之后,查询缓存(会初始化的)

点击购买输入验证码(redis验证码 1min)

redis验证码 1 min

提交验证码

判断验证码是否正确

限流
获取秒杀凭证(先判断有没有售罄标识 redis decrement)
获取不到,下单失败

防刷
RateLimiter;一个限制访问速率的工具

判断售罄标识

生成库存流水

执行本地事务
预减库存(redis decrement)
生成订单


检查本地事务(查看库存流水表)


执行本地事务
扣减库存

下单成功

如何解决超卖

使用redis的原子命令:decrement


    //预扣库存
    //redisTemplate.opsForValue().decrement(key, amount);
    //售罄标识
    //redisTemplate.opsForValue().set("item:stock:over:" + itemId, 1);
    @Override
    public boolean decreaseStockInCache(int itemId, int amount) {
        if (itemId <= 0 || amount <= 0) {
            throw new BusinessException(PARAMETER_ERROR, "参数不合法!");
        }

        String key = "item:stock:" + itemId;
        long result = redisTemplate.opsForValue().decrement(key, amount);
        if (result < 0) {
            // 回补库存
            this.increaseStockInCache(itemId, amount);
            logger.debug("回补库存完成 [" + itemId + "]");
        } else if (result == 0) {
            // 售罄标识
            redisTemplate.opsForValue().set("item:stock:over:" + itemId, 1);
            logger.debug("售罄标识完成 [" + itemId + "]");
        }

        return result >= 0;
    }


在上述代码中,使用Redis的decrement操作来预扣库存。如果库存数量减为负数(result < 0),则说明库存不足,需要回补库存。如果库存数量减至0(result == 0),则设置售罄标识。

这段代码可以一定程度上缓解超卖问题,但并不能完全解决。在高并发场景下,多个线程可能会同时进行decrement操作,导致库存数量减为负数。

可以通过以下方式来进一步解决超卖问题:

使用悲观锁或乐观锁来保证同一时间只有一个线程能够进行decrement操作,避免超卖问题。

在decrement操作前,通过GET命令获取当前库存数量,然后进行判断。如果库存数量小于等于请求的数量,则不进行decrement操作,避免超卖问题。

使用Lua脚本来确保decrement操作的原子性,同时进行判断和减少库存数量,避免并发问题。

综上所述,单纯使用Redis的decrement操作无法完全解决超卖问题,需要结合其他策略来确保库存的准确性和一致性。


改成一个Lua脚本

    //使用Lua脚本
    //得到stock
    //判断stock>amount
    //DECRBY stockKey, amount
    //SET stockOverKey, 1
    @Override
    public boolean decreaseStockInCache(int itemId, int amount) {
        if (itemId <= 0 || amount <= 0) {
            throw new BusinessException(PARAMETER_ERROR, "参数不合法!");
        }

        String script = "local itemId = ARGV[1]\n" +
                "local amount = tonumber(ARGV[2])\n" +
                "local stockKey = \"item:stock:\" .. itemId\n" +
                "local stockOverKey = \"item:stock:over:\" .. itemId\n" +
                "local stock = tonumber(redis.call(\"GET\", stockKey))\n" +
                "if stock == nil or stock < amount then\n" +
                "    return 0\n" +
                "else\n" +
                "    local result = redis.call(\"DECRBY\", stockKey, amount)\n" +
                "    if result == 0 then\n" +
                "        redis.call(\"SET\", stockOverKey, 1)\n" +
                "    end\n" +
                "    return 1\n" +
                "end";

        List<String> keys = new ArrayList<>();
        keys.add("item:stock:" + itemId);
        keys.add("item:stock:over:" + itemId);

        Object result = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), keys, itemId, amount);

        return result != null && (Long) result == 1;
    }

使用Lua脚本可以解决一些并发操作的问题,特别是在Redis中。由于Redis的单线程特性,执行Lua脚本可以保证原子性操作,并避免了多个命令的竞态条件。

在预扣库存的场景中,使用Lua脚本可以确保以下几点:

原子性操作:Lua脚本在Redis中被作为一个整体进行执行,中间不会被其他操作打断,从而保证了预扣库存操作的原子性。

避免超卖:通过在Lua脚本中检查库存数量,并在预扣成功后减少库存,可以避免超卖问题的发生。

但是需要注意的是,即使使用Lua脚本也不能完全解决所有并发问题。在高并发场景下,可能仍然存在一些问题,比如竞争条件和网络延迟等。因此,在实际应用中,还需要结合其他技术和策略来提高系统的并发性和可靠性,例如分布式锁、分布式事务等。



上述代码是一个使用Lua脚本来实现分布式锁和解决超卖问题的例子。具体步骤如下:

首先,定义了一个Lua脚本,脚本中包含了获取库存、检查库存是否足够、扣减库存和标记库存是否售罄的逻辑。

在Java代码中,通过RedisTemplate的execute方法来执行Lua脚本。在执行过程中,需要传入脚本、键列表和参数。

脚本:使用DefaultRedisScript指定脚本和返回值类型。
键列表:包含了需要操作的键,这里是"item:stock:“+itemId和"item:stock:over:”+itemId。
参数:itemId和amount,即商品ID和购买数量。
执行结果返回一个Object对象,需要根据具体业务逻辑进行判断。这里判断如果返回的结果不为空,且结果等于1,表示扣减库存成功;否则表示库存不足或者其他错误。

需要注意的是,在实际应用中,还需要考虑到异常处理、分布式锁的释放、并发情况下的处理等问题。此处的代码只是一个简化的示例,具体的实现方式需要根据具体业务需求和实际情况进行调整和完善。


redis分布式锁实现

    //1.定义一个公共的方法来获取分布式锁。
    private boolean acquireLock(String lockKey, String requestId, int expireTime) {
        boolean lockAcquired = false;
        try {
            lockAcquired = redisTemplate.opsForValue().setIfAbsent(lockKey, requestId, expireTime, TimeUnit.MILLISECONDS);
            logger.info("获取分布式锁成功");
        } catch (Exception e) {
            // 锁获取失败的处理逻辑
            logger.info("获取分布式锁失败");
            return false;
        }
        return lockAcquired;
    }

    //2.在方法中添加获取分布式锁的逻辑。
    @Override
    public boolean decreaseStockInCache(int itemId, int amount) {
        if (itemId <= 0 || amount <= 0) {
            throw new BusinessException(PARAMETER_ERROR, "参数不合法!");
        }

        String lockKey = "item:lock:" + itemId;
        String requestId = UUID.randomUUID().toString();
        int expireTime = 10000; // 锁的过期时间,单位为毫秒

        boolean lockAcquired = acquireLock(lockKey, requestId, expireTime);
        if (!lockAcquired) {
            // 获取锁失败的处理逻辑
            logger.info("获取分布式锁失败");
            return false;
        }

        try {
            // 执行库存扣减的逻辑
            String script = "local itemId = ARGV[1]\n" +
                    "local amount = tonumber(ARGV[2])\n" +
                    "local stockKey = \"item:stock:\" .. itemId\n" +
                    "local stockOverKey = \"item:stock:over:\" .. itemId\n" +
                    "local stock = tonumber(redis.call(\"GET\", stockKey))\n" +
                    "if stock == nil or stock < amount then\n" +
                    "    return 0\n" +
                    "else\n" +
                    "    local result = redis.call(\"DECRBY\", stockKey, amount)\n" +
                    "    if result == 0 then\n" +
                    "        redis.call(\"SET\", stockOverKey, 1)\n" +
                    "    end\n" +
                    "    return 1\n" +
                    "end";

            List<String> keys = new ArrayList<>();
            keys.add("item:stock:" + itemId);
            keys.add("item:stock:over:" + itemId);

            Object result = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), keys, itemId, amount);

            return result != null && (Long) result == 1;
        } finally {
            // 释放锁
            boolean releaseLock = releaseLock(lockKey, requestId);
            if (releaseLock){
                logger.info("释放分布式锁成功");
            }else {
                logger.info("释放分布式锁失败");
            }
        }
    }

    //3.定义一个公共的方法来释放分布式锁。
    private boolean releaseLock(String lockKey, String requestId) {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                "   return redis.call('del', KEYS[1]) " +
                "else " +
                "   return 1 " +
                "end";

        List<String> keys = new ArrayList<>();
        keys.add(lockKey);

        Object result = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), keys, requestId);

        return result != null && (Long) result == 1;
    }

怎么合理地设置分布式锁的过期时间

上面设置了10s

需要确保RedisLock过期时间大于业务执行时间的问题


设置分布式锁的过期时间是否合理取决于你的具体需求和业务场景。一般来说,过期时间需要根据以下因素进行考虑:

业务处理时间:你需要根据业务处理的预估时间来设置过期时间。如果你的业务处理时间通常在几秒钟内完成,可以设置一个稍微长一点的过期时间,以免在处理期间锁已经过期失效。
并发访问量:如果同时对某个资源进行大量的并发操作,你可能需要更短的过期时间,以便尽快释放锁,让其他请求能够获得锁。
锁争用频率:如果对某个资源的锁争用频率很高,建议设置较短的过期时间,以便其他请求能够更快地获取到锁。
根据你的描述,如果你的业务处理时间在几秒钟内,而且对资源的锁争用频率不是特别高,10秒的过期时间可能是一个合理的选择。但请注意,在实际应用中,你可能需要根据实际情况进行调试和优化,以找到最适合你的业务场景的过期时间设置。


Redisson

        <!-- redisson -->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.13.4</version>
        </dependency>
@Configuration
@PropertySource("classpath:application.properties")
public class RedisConfiguration {
    @Autowired
    private Environment env;
    @Bean
    public Redisson redisson() {
        Config config = new Config();

        String redisHost = env.getProperty("spring.redis.host");
        String redisPort = env.getProperty("spring.redis.port");
        String redisPassword = env.getProperty("spring.redis.password");
        int redisDatabaseNumber = env.getProperty("spring.redis.database", Integer.class, 0);

        String redisAddress = "redis://" + redisHost + ":" + redisPort;
        config.useSingleServer().setAddress(redisAddress).setPassword(redisPassword).setDatabase(redisDatabaseNumber);
        return (Redisson)Redisson.create(config);
    }
}
    @Override
    public boolean decreaseStockInCache(int itemId, int amount) {
        if (itemId <= 0 || amount <= 0) {
            throw new BusinessException(PARAMETER_ERROR, "参数不合法!");
        }

        String lockKey = "item:lock:" + itemId;
        String REDIS_LOCK = lockKey+":"+UUID.randomUUID().toString();
        RLock redissonLock = redisson.getLock(REDIS_LOCK);

        redissonLock.lock();
        logger.debug("redissonLock加锁成功");

        try {
            // 执行库存扣减的逻辑
            String script = "local itemId = ARGV[1]\n" +
                    "local amount = tonumber(ARGV[2])\n" +
                    "local stockKey = \"item:stock:\" .. itemId\n" +
                    "local stockOverKey = \"item:stock:over:\" .. itemId\n" +
                    "local stock = tonumber(redis.call(\"GET\", stockKey))\n" +
                    "if stock == nil or stock < amount then\n" +
                    "    return 0\n" +
                    "else\n" +
                    "    local result = redis.call(\"DECRBY\", stockKey, amount)\n" +
                    "    if result == 0 then\n" +
                    "        redis.call(\"SET\", stockOverKey, 1)\n" +
                    "    end\n" +
                    "    return 1\n" +
                    "end";

            List<String> keys = new ArrayList<>();
            keys.add("item:stock:" + itemId);
            keys.add("item:stock:over:" + itemId);

            Object result = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), keys, itemId, amount);

            logger.debug("执行库存扣减的逻辑成功");

            return result != null && (Long) result == 1;
        } finally {
            // 释放锁
            //避免IllegalMonitorStateException: attempt to unlock lock,not locked by current thread by node id
            if (redissonLock.isLocked()){
                if (redissonLock.isHeldByCurrentThread()){
                    redissonLock.unlock();
                    logger.debug("redissonLock释放成功");
                }
            }
        }
    }


在上述代码中,通过使用分布式锁确保了库存扣减的原子性操作,从而避免了多线程环境下的超卖问题。当多个线程同时执行库存扣减操作时,只有一个线程能够获取到锁,其他线程会在redissonLock.lock()处阻塞等待。

在获取到锁后,通过Lua脚本执行库存扣减的逻辑,并且使用DECRBY命令原子性地减少库存数量。如果库存不足,返回0表示扣减失败,否则返回1表示扣减成功。在扣减成功后,通过SET命令将标识库存已售罄的键设置为1。

这种方式可以有效地解决超卖问题。但是需要注意的是,使用分布式锁会增加系统的复杂性和延迟,并且需要保证Redis服务器的可用性和性能。因此,在设计系统时需要权衡分布式锁的使用场景和影响,确保在高并发环境下依然能够保持系统的稳定性和性能。

另外,除了使用分布式锁来解决超卖问题,还可以考虑其他方案,如使用乐观锁或悲观锁、使用消息队列等。具体选择哪种方案取决于系统的需求和特点。


2023-8-17 17:14:14

redisson分布式锁的原理

Redisson是一个基于Redis的分布式Java对象和服务库,它提供了多种分布式功能,包括分布式锁。Redisson的分布式锁机制主要基于Redis的特性和命令实现。

Redisson分布式锁的原理如下:

客户端通过Redisson框架的API请求获取锁。
Redisson将客户端的请求转化为Lua脚本,并通过eval命令发送给Redis服务器。
Redis服务器在单线程下执行Lua脚本,保证了脚本的原子性和线程安全性。
Lua脚本在Redis服务器中执行以下操作:
首先,通过SET命令尝试将锁对应的键设置为指定值(通常是一个唯一标识符)。
如果设置成功,则表示获取锁成功,返回成功标识。
如果设置失败,则表示锁已被其他客户端占用,返回失败标识。
同时,设置锁的过期时间,防止锁无限占用。
Redisson客户端接收到Redis服务器返回的结果,根据结果判断锁的获取是否成功,如果成功则继续执行业务逻辑,否则等待或进行其他处理。
Redisson的分布式锁实现利用了Redis的原子操作,通过设置指定的值来实现锁的获取和释放。同时,使用Lua脚本可以保证获取锁的操作是原子的,避免了竞争条件和并发问题。

需要注意的是,Redisson的分布式锁还具备可重入性和红锁特性,使得锁的使用更加灵活和可靠。

redisson分布式锁比setnx有什么优势?

Redisson分布式锁相对于直接使用SETNX命令实现分布式锁具有以下几个优势:

  1. 可重入性:Redisson分布式锁支持可重入,也就是同一线程可以多次获取同一个锁而不会被阻塞。这种可重入性能够解决一些特定场景下的并发问题。

  2. 锁的自动续期:Redisson分布式锁支持锁的自动续期,即在获取锁后,锁的过期时间会不断更新,防止持有锁的客户端在执行业务逻辑时由于各种原因导致锁过期而被其他客户端抢占。

  3. 防止死锁的解锁机制:Redisson分布式锁通过监听锁的失效事件来实现解锁操作,避免了因为客户端崩溃或其他原因导致的锁无法释放的情况。

  4. 红锁特性:Redisson分布式锁支持红锁特性,即在多个Redis节点之间实现锁的互斥。当多个Redis节点之间进行通信时,可以使用分布式锁的红锁特性保证在大多数节点上加锁成功才算真正获取到锁,避免了分布式环境下的数据不一致问题。

  5. 强大的扩展性:Redisson分布式锁是基于Redisson框架的分布式Java对象和服务库实现的,提供了很多其他功能和特性,如分布式服务、分布式集合等。使用Redisson分布式锁可以很方便地与其他分布式功能进行集成和扩展。

综上所述,Redisson分布式锁相比于直接使用SETNX命令实现分布式锁,具有更多的功能和特性,可以更方便地应对分布式环境中的并发和竞争问题。

优化

安全优化

对用户注册登录的密码使用公私钥加密进行前后端传输
对密码进行MD5+盐值存入数据库

功能优化

sms验证码登录

秒杀链接动态化

订单超时未支付,取消订单,回补库存

性能优化

超卖:分布式锁

订单号生成使用雪花算法

设计

高并发下如何设计秒杀系统?

设计一个秒杀系统,需要考虑这些问题:

在这里插入图片描述

如何解决这些问题呢?

  • 页面静态化
  • 按钮至灰控制
  • 服务单一职责
  • 秒杀链接加盐
  • 限流
  • 分布式锁
  • MQ 异步处理
  • 限流&降级&熔断

页面静态化

秒杀活动的页面,大多数内容都固定不变,如商品名称,商品图片等等,可以对活动页面做静态化处理,减少访问服务端的请求。秒杀用户会分布在全国各地,有在上海,有在深圳,地域相差很远,网速也各不相同。为了让用户最快访问到活动页面,可以使用 CDN(Content Delivery Network,内容分发网络)。CDN 可以让用户就近获取所需内容。

按钮至灰控制

秒杀活动开始前,按钮一般需要置灰的。只有时间到了,才能变得可以点击。这样防止,秒杀用户在时间快到时前几秒,疯狂请求服务器,然后秒杀时间点还没到,服务器就自己挂了。

服务单一职责

我们都知道微服务设计思想,也就是把各个功能模块拆分,功能那个类似的放
一起,再用分布式的部署方式。

如用户登录相关的,就设计个用户服务,订单相关就搞个订单服务,再到礼物相关的就搞个礼物服务等等。那么,秒杀相关业务逻辑也可以放到一起,搞个秒杀务,单独给它搞个秒杀数据库。

服务单一职责有个好处:如果秒杀没抗住高并发的压力,秒杀库崩了,服务挂了,也不会影响到系统的其他服务。

秒杀链接加盐

链接如果明文暴露的话,会有人获取到请求 Url,提前秒杀了。因此,需要给秒杀链接加盐。可以使URL 动态化,如通过 MD5 加密算法加密随机字符串去做 url。

限流

一般有两种方式限流:nginx 限流和redis 限流。

  • 为了防止某个用户请求过于频繁,我们可以对同一用户限流;
  • 为了防止黄牛模拟几个用户请求,我们可以对某个 IP 进行限流;
  • 为了防止有人使用代理,每次请求都更换 IP 请求,我们可以对接口进行限流。
  • 为了防止瞬时过大✁流量压垮系统,还可以使用阿里Sentinel、Hystrix 组件进行限流。

分布式锁

可以使用 redis 分布式锁解决超卖问题。
使用 Redis 使用 SET EX PX NX + 校验唯一随机值,再删除释放锁。

if(jedis.set(key_resource_id, uni_request_id, "NX", "EX", 100s) == 1{ //加锁
	try {
		do something / / 业务处理
	}catch(){
	}
	finally {
		//判断是不是当前线程加锁,才释放
		if (uni_request_id.equals(jedis.get(key_resource_id))) {
			jedis.del(lockKey); //释放锁
		}
	}
}

在这里,判断是不是当前线程加锁和释放锁不是一个原子操作。如果调用
jedis.del()释放锁时候,可能这锁已经不属于当前客户端,会解除他人加
锁。
在这里插入图片描述
为了更严谨,一般也✁用 lua 脚本代替。lua 脚本如下:

if redis.call('get',KEYS[1]) == ARGV[1] then
	return redis.call('del',KEYS[1])
else
	return 0
end;

MQ 异步处理

如果瞬间流量特别大,可以使用消息队列削峰,异步处理。用户请求过来✁时
候,先放到消息队列,再拿出来消费。

限流&降级&熔断

  • 限流,就是限制请求,防止过大请求压垮服务器;
  • 降级,就是秒杀服务有问题了,就降级处理,不要影响别的服务;
  • 熔断,服务有问题就熔断,一般熔断降级是一起出现。

在秒杀场景中,常用的限流算法有哪些?

1、什么是限流?

所谓限流,就是指限制流量请求的频次。它主要是在高并发情况下,用于保护系统的一种策略,主要是避免在流量高峰导致系统崩溃,造成系统不可用的问题。

实现限流常见的算法4种,分别是计数器限流算法、滑动窗口限流算法、漏桶限流算法、令牌桶限流算法。下面,我给大家详细介绍每种算法的基本原理。

2、限流算法

1、计数器限流算法。
在这里插入图片描述

一般用在单一维度的访问频率限制上,比如短信验证码每隔 60s只能发送一次,或者接口调用
次数等。它的实现方法很简单,就是每调用一次就加 1,处理结束以后减1。

2、滑动窗口限流算法,
在这里插入图片描述

本质上也是一种计数器,只是通过以时间为维度的可滑动窗口设计,来减少了临界值带来的并发超过阈值的问题。每次进行数据统计的时候,只需要统计这 个窗口内每个时间刻度的访问量就可以了。

Spring Cloud 中的熔断框架 Hystrix,以及 Spring Cloud Alibaba 中的Sentinel 都采用滑动窗口来做数据统计。

3、漏桶限流算法。
在这里插入图片描述

它是一种恒定速率的限流算法,不管请求量是多少,服务端的处理效率是恒定的。基于 MQ 来实现
的生产者消费者模型,其实算是一种漏桶限流算法。

4、令牌桶限流算法。
在这里插入图片描述

相对漏桶算法来说,它可以处理突发流量的问题。它的核心思想是,令牌桶以恒定速率去生成令牌保存到令牌桶里面,桶的大小是固定的,令牌桶满了以后就不再生成令牌。

每个客户端请求进来的时候,必须要从令牌桶获得一个令牌才能访问,否则排队等待。在流量低峰的时候,令牌桶会出现堆积,因此当出现瞬时高峰的时候,有足够多的令牌可以获取,因此令牌桶能够允许瞬时流量的处理。

网关层面的限流、或者接口调用的限流,都可以使用令牌桶算法,像 Google 的Guava,和Redisson 的限流,都用到了令牌桶算法。

我认为,限流的本质是实现系统保护,最终选择什么样的算法,一方面取决于统计的精准度,另一方面考虑限流维度和场景的需求。

最后

2023-7-31 16:02:02

我们都有光明的未来

祝大家考研上岸
祝大家工作顺利
祝大家得偿所愿
祝大家如愿以偿
点赞收藏关注哦

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1686709.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2024年电工杯高校数学建模竞赛(B题) 建模解析| 大学生平衡膳食食谱的优化设计 |小鹿学长带队指引全代码文章与思路

我是鹿鹿学长&#xff0c;就读于上海交通大学&#xff0c;截至目前已经帮200人完成了建模与思路的构建的处理了&#xff5e; 本篇文章是鹿鹿学长经过深度思考&#xff0c;独辟蹊径&#xff0c;实现综合建模。独创复杂系统视角&#xff0c;帮助你解决电工杯的难关呀。 本题&…

南京中科微Ci2451+11dbm发射功率 国产8位RISC内核无线MCU芯片

Ci2451是一款在现有的2.4GHz射频芯片基础上&#xff0c;内部集成8位RISC内核&#xff08;精简指令集&#xff09;MCU的SOC芯片。 Ci2451引脚图↑ 无线MCU解决方案&#xff0c;集成丰富的MCU资源、更小尺寸&#xff0c;来满足设计中的各种内存、功率、尺寸要求&#xff0c;充分…

QCC30xx如何实单声道MONO输出

有客户提出需要将QCC30xx的输出改为单声道输出(我们的QCC30xx是双声道输出,如果采用单声道输出,我们需要进行混音操作)。 客户采用目前最新的 SDK上将INCLUDE_STEREO屏蔽掉,直接进行编译,会报一系列的问题,编译不过。 也有很多客户尝试在各个模式下强制将通道输出设置…

cert-s

绕不过&#xff0c;啊对对对。 安全公司的东西都是无敌的。 菜你就多练。

C中十进制转十六进制示例

uint8_t QR_code_RxBfr[255]{0}; uint8_t TouchCode[100];memcpy (&Sys.TouchCode[0], &QR_code_RxBfr[0], Sys.QR_code_Len);Str &Sys.TouchCode[TmpVble];Sys.Card_ID 0; while(0 ! isdigit(*Str)){Sys.Card_ID Sys.Card_ID*10 *Str - 0;Str;} 最后在通过以下…

Springboot阶段项目---《书城项目》

一 项目介绍 本项目采用集成开发平台IntelliJ IDEA开发了在线作业成绩统计系统的设计与实现&#xff0c;实现了图书商城系统的综合功能和图形界面的显示&#xff0c;可以根据每个用户登录系统后&#xff0c;动态展示书城首页图书&#xff0c;实现了分类还有分页查询&#xff0c…

CAN报文,Motorola和Intel格式

CAN报文&#xff0c;Motorola和Intel格式 车载测试系列&#xff1a;CAN报文之Intel格式与Motorola格式 当信号在一个字节内实现&#xff08;信号不跨字节&#xff09;时&#xff0c;Intel模式和Motorola模式的信号字节顺序&#xff0c;完全一样&#xff1a; 信号的高位&am…

leetcode-顺时针旋转矩阵-111

题目要求 思路 1.假设现在有一个矩阵 123 456 789 2.我们可以根据19这个对角线将数据进行交换&#xff0c;得到矩阵 147 258 369 3.然后将矩阵每一行的数据再翻转&#xff0c;得到矩阵 741 852 963 代码实现 class Solution { public:vector<vector<int> > rot…

【HMGD】STM32/GD32 I2C DMA 主从通信

STM32 I2C配置 主机配置 主机只要配置速度就行 从机配置 从机配置相同速度&#xff0c;可以设置第二地址 因为我的板子上面已经有了上拉电阻&#xff0c;所以可以直接通信 STM32 I2C DMA 定长主从通信代码示例 int state 0; static uint8_t I2C_recvBuf[10] {0}; stat…

ROS2安装教程之强大辅助

第一次接触ROS2&#xff0c;安装时跟这个&#xff0c;跟那个教程&#xff0c;真的是一把泪&#xff0c;耗费数多个小时&#xff0c;依旧存在一些或多或少的问题&#xff0c;最主要的是永远提示 直到遇到了一个鱼香大佬&#xff0c;是真的香啊&#xff0c;好不容易找到的资料&a…

深度学习——图像分类(CNN)—训练模型

训练模型 1.导入必要的库2.定义超参数3.读取训练和测试标签CSV文件4.确保标签是字符串类型5.显示两个数据框的前几行以了解它们的结构6.定义图像处理参数7.创建图像数据生成器8.设置目录路径9.创建训练和验证数据生成器10.构建模型11.编译模型12.训练模型并收集历史13.绘制损失…

145.栈和队列:删除字符串中的所有相邻重复项(力扣)

题目描述 代码解决 class Solution { public:string removeDuplicates(string s) {// 定义一个栈来存储字符stack<char> st;// 遍历字符串中的每一个字符for(int i 0; i < s.size(); i){// 如果栈为空或栈顶字符与当前字符不相同&#xff0c;则将当前字符入栈if(st.e…

SpringBoot项目中redis序列化和反序列化LocalDateTime失败

实体类中包含了LocalDateTime 类型的属性&#xff0c;把实体类数据存入Redis后变成这样&#xff1a; 此时&#xff0c;存入redis不会报错&#xff0c;但是从redis获取的时候&#xff0c;会报错&#xff1a; com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Ca…

AI - Transformer架构工作原理

一、概述 Transformer是由Vaswani等人在2017年提出的一种基于自注意力机制&#xff08;Self-Attention Mechanism&#xff09;的深度学习网络架构的大模型&#xff0c;被广泛应用于自然语言处理&#xff08;NLP&#xff09;领域&#xff0c;如机器翻译、文本生成等任务。它摒弃…

LabVIEW高温往复摩擦测试系统中PID控制

在LabVIEW开发高温往复摩擦测试系统中实现PID控制&#xff0c;需要注意以下几个方面&#xff1a; 1. 系统建模与参数确定 物理模型建立: 首先&#xff0c;需要了解被控对象的物理特性&#xff0c;包括热惯性、摩擦系数等。这些特性决定了系统的响应速度和稳定性。实验数据获取…

陕西煤矿化工集团如何投稿刊登到央媒

随着信息技术的飞速发展&#xff0c;国家级媒体平台已经成为了众多作者追求发表文章的热门选择。然而&#xff0c;要想在这些平台上成功发表文章&#xff0c;除了具备优秀的文稿质量外&#xff0c;还需要掌握一定的投稿技巧和策略。本文将为您详细介绍国家级媒体投稿方式&#…

samba_ubuntu_share_vmbox_vmware

_____ Ubuntu 利用 samba 与 win 直接共享文件夹 _____ samba Samba - 维基百科&#xff0c;自由的百科全书 (wikipedia.org) 用于 win 和 unix 直接访问资源 samba 为选定的 unix 目录建立网络共享&#xff0c; 使得 win 用户可以像访问普通 win 下的文件夹那样来通过网络来…

Discourse Discover 添加你的网站到 Discourse 官方

discourse discover 应该允许你把你的 Discourse 实例添加到 Discourse 的 https://discover.discourse.org/ 1 页面中。 直接在你网站的配置上搜索 Discourse Discover &#xff0c;余下的工作就可以交给 Discourse 了。 还没有选的&#xff0c;可以马上选上喔。 但显然排序…

Baidu Comate For Xcode 你的AI编程助手

前言 Baidu Comate 基于文心大模型&#xff0c;结合百度编程大数据&#xff0c;为你生成优质编程代码 你的AI编程助手&#xff0c;你的编码效率提升好帮手 Baidu Comate 释放“十倍”软件生产力 一、Xcode 安装配置 Baidu Comate 安装 已安装Xcode的情况下&#xff0c;下载B…

Windows下安装Hadoop(引导版)

Windows下安装Hadoop(引导版) 本环境只作为测试环境的搭建和学习使用 参考文档 环境&#xff1a; 首先确定环境为java1.8 或者hadoop适配的版本 cmd java -version查看 hadoop环境变量可以不用设置 关于hdfs的配置可以自行修改目录 具体的安装方式参考下面的两个文档 下载…