在这里插入图片描述

秒杀系统的架构设计

秒杀系统,是典型的短时大量突发访问类问题。对这类问题,有三种优化性能的思路:

  1. 写入内存而不是写入硬盘
  2. 异步处理而不是同步处理
  3. 分布式处理

用上这三招,不论秒杀时负载多大,都能轻松应对, Redis正好能完美满足上述三点。因此,用Redis就能轻松实现秒杀系统。

在这里插入图片描述


秒杀测试代码编写:

package com.xiao.springbootredisseckill.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.atomic.AtomicInteger;

@RestController
public class TestController {

    @Autowired
    private RedisTemplate redisTemplate;
    
    // 记录实际卖出的商品数量
    private AtomicInteger successNum =  new AtomicInteger(0);

    // 设置库存
    @GetMapping("/setStockNum")
    public String setStockNum(@RequestParam Integer num){
        // 初始化库存数量
        redisTemplate.opsForValue().set("product_stock",num);
        //初始化实际卖出的商品数量0
        successNum.set(0);
        return "Ok";
    }

    // 秒杀核心代码
    @GetMapping("/grabBuy")
    public String grabBuy() {
        Integer sku = (Integer)redisTemplate.opsForValue().get("product_stock");
        sku = sku - 1;
        if (sku < 0) {
            return "库存不足";
        }
        redisTemplate.opsForValue().set("product_stock", sku);
        //记录实际卖出的商品数量
        return "减少库存成功,共减少" + successNum.incrementAndGet();
        
    }

    // 获取库存数量
    @GetMapping("/getStockNum")
    public Object getStockNum() {
        Integer product_stock = Integer.parseInt((String) redisTemplate.opsForValue().get("product_stock"));
        System.out.println(product_stock);
        return product_stock;
    }

    @GetMapping(value = "/successNum")
    public String successNum() {
        return "顾客成功抢到的商品数量:" + successNum.get();
    }
}


测试:

库存设为 100, 访问 127.0.0.1:8080/grabBuy 进行抢购。为了让测试场景更像实际抢购实际场景,这里使用 Apache 其下的 JMeter 压力测试工具,具体如何使用请自行百度

访问127.0.0.1:8080/setStockNum 设置库存,或者使用 rdm 设置库存

在这里插入图片描述

JMeter 配置压力测试脚本

在这里插入图片描述

在这里插入图片描述


测试结果:

在这里插入图片描述
访问 127.0.0.1:8080/successNum, 查看用户实际抢购成功的商品数量:

在这里插入图片描述
???? 为什么用户实际抢购数量为 1422 呢? 远超于库存 100 ,出现了超卖情况,如果实际应用中出现这样的错误,那跟用户怕是讲不清了 …
在这里插入图片描述

原因分析:

从上面测试结果,我们知道,高并发请求: 127.0.0.1:8080/grabBuy, 就会出现超卖现象。
其原因其实就是是库存数量product_stock的读和写操作不在同一个原子操作上,导致类似不可重复读的现象。可以类比多线程的问题。

什么是原子性呢?
在化学反应中,原子是不能再分的。在计算机中也就是某个操作不可分割的,就叫原子性。比如 a=0;(a非long和double类型) 这个操作是不可分割的,那么我们说这个操作时原子操作。再比如:a++; 这个操作实际是a = a + 1;是可分割的,所以他不是一个原子操作。非原子操作都会存在线程安全问题,需要我们使用同步技术(sychronized)来让它变成一个原子操作。一个操作是原子操作,那么我们称它具有原子性。
什么是不可重复读呢?
在学习数据库相关知识的时候,我们知道了事务的隔离级别:丢失更新 、脏读、不可重复读、幻读,其中不可重复读就是在同一事务中,多次读取同一数据但是返回不同的结果,也就是有其他事务更改了这些数据。


解决办法:

提供两种解决方法:

①: 通过redis事务解决超卖问题


@GetMapping("/grabBuy")
    public String grabBuy() {
 /*
        * 解决商品超卖 方法一
        * */
        redisTemplate.setEnableTransactionSupport(true);
        List<Object> result = (List<Object>) redisTemplate.execute(new SessionCallback<List<Object>>() {
            @Override
            public List<Object> execute(RedisOperations redisOperations) throws DataAccessException {
                // 监听
                redisOperations.watch("product_stock");
                Integer product_stock = (Integer)redisOperations.opsForValue().get("product_stock");
                // 开启事务
                redisOperations.multi();
                // 必要的空查询
                redisOperations.opsForValue().get("product_stock");
                product_stock = product_stock - 1;
                if (product_stock < 0) {
                    return null;
                }
                // 剩余库存存入
                redisOperations.opsForValue().set("product_stock", product_stock);
                return redisOperations.exec();
            }
        });
        if (result != null && result.size() > 0) {
            return "减少库存成功,共减少" + successNum.incrementAndGet();
        }

        return "库存不足";
  }

②:通过使用 redisson 的 Rlock 加锁方式解决超卖问题

pom.xml 引入依赖包:

 		<!-- https://mvnrepository.com/artifact/org.redisson/redisson-spring-boot-starter -->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.13.6</version>
        </dependency>

秒杀代码编写:

@GetMapping("/grabBuy")
    public String grabBuy() {
    	/*
        * 解决商品超卖 方法二
        * */
        RLock rLock = redissonClient.getLock("product");

        try {
            rLock.lock();
            Integer product_num = (Integer)redisTemplate.opsForValue().get("product_stock");
            System.out.println(product_num);

            if (product_num < 1) {
                return "库存不足!";
            }

            // 自减一
            redisTemplate.opsForValue().decrement("product_stock");

            return "减少库存成功,共减少" + successNum.incrementAndGet();
        } finally {
            rLock.unlock();
        }
  }

注意: redissonClient.getLock() 括号内的参数千万不能取redis数据库脸面已经含有的键名,否则报如下错误,我找半天才发现这里的错误。
在这里插入图片描述
org.redisson.client.RedisException: ERR Error running script (call to f_3ffe249c16dee540ac8ab32e39bd408626b9aff7): @user_script:1: WRONGTYPE Operation against a key holding the wrong kind of value . channel: [id: 0xfc81693b, L:/127.0.0.1:11732 - R:localhost/127.0.0.1:6379] command: (EVAL), params: [if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then return nil;end; local counter = redis.call('h..., 2, product_stock, redisson_lock__channel:{product_stock}, 0, 30000, 93656223-0013-4a55-a2d6-06ec7df0cda2:71]

结果展示:
在这里插入图片描述
在这里插入图片描述
成功解决超卖问题!

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐