基于Redis实现秒杀【代码为先】

基于Redis实现秒杀【代码为先】

需求

现库存某一商品存货量为100,实现每秒10000请求QPS大约为20的秒杀,并能完成正常的库存扣减,防止超卖

前置知识

  1. 了解jmeter的简单使用用于模拟并发;
  2. redisJava语言下的基本操作;

环境说明

1、redis version 3.2
2Maven 3.6    
3、redis的数据结构 商品编号为key 当前时间+过期时间组成的时间戳为Value 
4Jmeter 5.4.3 项目基于springboot实现

代码结构如下

---com.liang
	---controller
	   GoosController
	---entity
	   GoodsStore
	---respository
	   GoodsStoreRespository
    ---service
        ---impl
           GoodsStoreServiceImpl
        GoodsStoreService
    ---lock
       RedisLockD
        

POM依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.liang</groupId>
    <artifactId>springbootRedis001</artifactId>
    <version>1.0-SNAPSHOT</version>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.5.RELEASE</version>
    </parent>
    <!--  引入各种各样的组件  -->
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--    jpa    -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <!--  整合redis 虽说是springboot整合redis 但其实为 Spring Data Redis 操作redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!--    引入连接池    -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <!--   引入lombok     -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!--fastJson-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.56</version>
        </dependency>
        <!--  导入thymleaf      -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!--   mysql     -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!--   测试     -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>RELEASE</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <!--    没有该配置,devtools不生效    -->
                    <fork>true</fork>
                    <addResources>true</addResources>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

数据结构

在这里插入图片描述

Entity

package com.liang.entity;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;


import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;


/**
 * Created with Intellij IDEA
 *
 * @Auther: liangjy
 * @Date: 2022/01/07/11:14
 * @Description: 秒杀demo 连接数据库
 */
@Entity
@Table(name = "goods_store", schema = "seckill")
@Getter
@Setter
@ToString
public class GoodsStore implements Serializable {
    private static final long serialVersionUID = 1L;
    @Id
    private String code;

    @Column(name = "store")
    private int store;
}

Repository代码

package com.liang.respository;

import com.liang.entity.GoodsStore;
import io.lettuce.core.dynamic.annotation.Param;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import javax.transaction.Transactional;

/**
 * Created with Intellij IDEA
 *
 * @Auther: liangjy
 * @Date: 2022/01/07/11:48
 * @Description:
 */

public interface GoodsStoreRespository extends JpaRepository<GoodsStore,String> {
    /**
     * 更新库存
     * @param code
     * @param store
     * @return
     */
    @Modifying
    @Transactional
    @Query("update GoodsStore gs set gs.store=?2 where gs.code=?1")
    int updateStore(@Param("code") String code, @Param("store")Integer store);
}

service

package com.liang.service;

import com.liang.entity.GoodsStore;

public interface GoodsStoreService {
    /**
     * 根据产品编号更新库存
     * @param code
     * @return
     */
    String updateGoodsStore(String code,int count);

    /**
     * 获取库存对象
     * @param code
     * @return
     */
    GoodsStore getGoodsStore(String code);
}

impl
package com.liang.service.impl;

import com.liang.entity.GoodsStore;
import com.liang.respository.GoodsStoreRespository;
import com.liang.rlock.RedisLockD;
import com.liang.service.GoodsStoreService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.util.Optional;

@Service
public class GoodsStoreServiceImpl implements GoodsStoreService {

    @Autowired
    private GoodsStoreRespository goodsStoreRespository;

    @Autowired
    private RedisLockD redisLock;

    /**
     * 超时时间 5s
     */
    private static final int TIMEOUT = 5 * 1000;

    /**
     * 根据产品编号更新库存
     *
     * @param code  商品编号
     * @param count 前端传过来的数量
     * @return
     */
    @Override
    public String updateGoodsStore(String code, int count) {
        //上锁
        BigDecimal count1 = new BigDecimal(count);
        long time = System.currentTimeMillis() + TIMEOUT;
        /**
         * 拿不到资源 那么就会等待
         */
        if (!redisLock.lock(code, String.valueOf(time))) {
            return "排队人数太多,请稍后再试.";
        }
     
        try {
            GoodsStore goodsStore = getGoodsStore(code);
            if (goodsStore != null) {
                if (goodsStore.getStore() <= 0) {
                    return "对不起,卖完了,库存为:" + goodsStore.getStore();
                }
                if (goodsStore.getStore() < count) {
                    return "对不起,库存不足,库存为:" + goodsStore.getStore() + " 您的购买数量为:" + count;
                }
                int store = goodsStore.getStore();
                BigDecimal stroe = new BigDecimal(store);
                BigDecimal remainstore = stroe.subtract(count1);
                goodsStoreRespository.updateStore(code, Integer.parseInt(String.valueOf(remainstore)));
                try {
                    //为了更好的测试多线程同时进行库存扣减,在进行数据更新之后先等1秒,让多个线程同时竞争资源
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return "恭喜您,购买成功!";
            } else {
                return "获取库存失败。";
            }
        } finally {
            //释放锁
            redisLock.release(code, String.valueOf(time))}
    }

    /**
     * 获取库存对象
     *
     * @param code
     * @return
     */
    @Override
    public GoodsStore getGoodsStore(String code) {
        Optional<GoodsStore> optional = goodsStoreRespository.findById(code);
        return optional.get();
    }
}

lock

package com.liang.rlock;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

/**
 * Created with Intellij IDEA
 *
 * @Auther: liangjy
 * @Date: 2022/01/07/14:36
 * @Description:
 */
@Component
public class RedisLockD {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 加锁
     *
     * @param lockKey   加锁的Key
     * @param timeStamp 时间戳:当前时间+超时时间
     * @return
     */
    public boolean lock(String lockKey, String timeStamp) {
        /**
         * 在这个场景下商品编码作为key 时间戳来作为value setnx
         * 设置成功返回true
         */
        if (stringRedisTemplate.opsForValue().setIfAbsent(lockKey, timeStamp)) {
            // 对应setnx命令,可以成功设置,也就是key不存在,获得锁成功
            return true;
        }

        //设置失败的原因是因为这个商品已经被操作了,获得锁失败
        // 判断锁超时 - 防止原来的操作异常,没有运行解锁操作 ,防止死锁
        String currentLock = stringRedisTemplate.opsForValue().get(lockKey);
        // 如果锁过期 currentLock不为空且小于当前时间
        if (!StringUtils.isEmpty(currentLock) && Long.parseLong(currentLock) < System.currentTimeMillis()) {
            //如果lockKey对应的锁已经存在,获取上一次设置的时间戳之后并重置lockKey对应的锁的时间戳
            /**
             * getAndSet
             * 获取原来key键对应的值并重新赋新值。
             * 拿到原来的时间戳并复制新的时间戳
             * prelock 为原来的时间戳
             */
            String preLock = stringRedisTemplate.opsForValue().getAndSet(lockKey, timeStamp);

            //假设两个线程同时进来这里,因为key被占用了,而且锁过期了。
            //获取的值currentLock=A(get取的旧的值肯定是一样的),两个线程的timeStamp都是B,key都是K.锁时间已经过期了。
            //而这里面的getAndSet一次只会一个执行,也就是一个执行之后,上一个的timeStamp已经变成了B。
            //只有一个线程获取的上一个值会是A,另一个线程拿到的值是B。
            if (!StringUtils.isEmpty(preLock) && preLock.equals(currentLock)) {
                return true;
            }
        }

        return false;
    }

    /**
     * 释放锁
     *
     * @param lockKey
     * @param timeStamp
     */
    public void release(String lockKey, String timeStamp) {
        try {
            String currentValue = stringRedisTemplate.opsForValue().get(lockKey);
            if (!StringUtils.isEmpty(currentValue) && currentValue.equals(timeStamp)) {
                // 删除锁状态
                stringRedisTemplate.opsForValue().getOperations().delete(lockKey);
            }
        } catch (Exception e) {
            System.out.println("警报!警报!警报!解锁异常");
        }
    }
}

controller

package com.liang.controller;

import com.liang.service.GoodsStoreService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;

/**
 * Created with Intellij IDEA
 *
 * @Auther: liangjy
 * @Date: 2022/01/07/14:42
 * @Description:
 */
@Controller
@RequestMapping("/")
public class GoosController {
    @Autowired
    private GoodsStoreService goodsStoreService;
    /**
     * 秒杀提交
     *
     * @param code
     * @param num
     * @return
     */
    @PostMapping("secKill")
    @ResponseBody
    public String secKill(@RequestParam(value = "code", required = true) String code, @RequestParam(value = "num", required = true) Integer num) {
        String reString = goodsStoreService.updateGoodsStore(code, num);
        return reString;
    }
}

执行测试

Jmeter 建立线程组

建立线程组【一秒设立10000个线程数】

在这里插入图片描述

http请求设置

在这里插入图片描述

查看结果树

在这里插入图片描述

最终效果就是一万个线程数,只有随机11个线程数秒杀成功。相当于实现了非公平锁。但在用户角度来看反而挺公平的

获得锁的时间戳:1645169458647
剩余库存:1000
扣除库存:1
释放锁的时间戳:1645169458647
获得锁的时间戳:1645169459801
剩余库存:999
扣除库存:1
释放锁的时间戳:1645169459801
获得锁的时间戳:1645169460897
剩余库存:998
扣除库存:1
获得锁的时间戳:1645169461948
释放锁的时间戳:1645169460897
剩余库存:997
扣除库存:1
释放锁的时间戳:1645169461948
获得锁的时间戳:1645169463224
剩余库存:996
扣除库存:1
获得锁的时间戳:1645169464540
释放锁的时间戳:1645169463224
剩余库存:995
扣除库存:1
释放锁的时间戳:1645169464540
获得锁的时间戳:1645169465695
剩余库存:994
扣除库存:1
释放锁的时间戳:1645169465695
获得锁的时间戳:1645169466746
剩余库存:993
扣除库存:1
获得锁的时间戳:1645169467859
释放锁的时间戳:1645169466746
剩余库存:992
扣除库存:1
释放锁的时间戳:1645169467859
获得锁的时间戳:1645169468955
剩余库存:991
扣除库存:1
释放锁的时间戳:1645169468955
获得锁的时间戳:1645169469966
剩余库存:990
扣除库存:1
释放锁的时间戳:1645169469966

查看数据库是否出现超卖

在这里插入图片描述

数据显示正常,简单实现了开篇需求

今天菜鸟就陪大家到这里了,代码注释关键部分做了解释,部分不明白可以私信聊哈~ 下次再见

附上仓库地址

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值