用node.js一步步完善 秒杀代码

本文详细介绍了如何在电商平台秒杀活动中,通过Redis实现库存控制,防止超卖问题。从最初的简单计数到使用Redis的setnx命令加锁,再到为锁设置过期时间和通过用户标识确保锁的正确释放,逐步完善了秒杀过程的并发安全性。同时,提出了在服务器宕机或Redis集群中节点故障的情况下的潜在问题,并提及了自动化测试和压测的建议。
摘要由CSDN通过智能技术生成

逐步完善秒杀过程

场景:电商平台秒杀袜子,共计100双,每人限购1双;

普通代码 (在并发量很少时没问题)

import redis from '../modules/redis';
class DemoService{
    static async miaosha(){
        const redisName='mssp-001'
        //await redis.set(redisName,100,'EX',-1) //向redis 写入数据
        const count=await redis.get(redisName);
        if(Number(count) <= 0) return {statusCode:1001,data:{},message:'error,Insufficient quantity'};//商品数量不足
        const count1=await redis.decr(redisName);
        console.log('count1=',count1);
        return count1 > 0 ? {statusCode:1000,data:{},message:'成功'}:{statusCode:1001,data:{},message:'失败'}
    }
}
export default DemoService;

上面的代码,如果在高并发下,当多个用户同一时刻都走过了

const count=await redis.get(redisName);

就会出现超卖

完善代码1,用setnx加锁

import redis from '../modules/redis';
class DemoService{
    static async miaosha(){
        const redisName='mssp-001'
        const lockName='lock-001';
        //await redis.set(redisName,100,'EX',-1) //向redis 写入数据
        const addLock=await redis.setnx(lockName,'');//加锁成功返回true 已存在锁,枷锁失败都返回false
        if(!addLock) return {statusCode:1001,data:{},message:'error,Please try again'};//解决多人
        const count=await redis.get(redisName);
        if(Number(count) <= 0) return {statusCode:1001,data:{},message:'error,Insufficient quantity'};//商品数量不足
        const count1=await redis.decr(redisName);
        console.log('count1=',count1);
        redis.del(lockName);//秒杀成功,将所删除
        return count1 > 0 ? {statusCode:1000,data:{},message:'成功'}:{statusCode:1001,data:{},message:'失败'}
    }
}
export default DemoService;

上面代码,在正常情况下,是可以的,
如果,加完锁后,在扣减数量时出现网络异常等情况,锁就会一直存在,后面的用户就一直不能参与秒杀

完善代码2

通过try …catch 解决异常情况下锁的问题

import redis from '../modules/redis';
class DemoService{
    static async miaosha(){
        const redisName='mssp-001'
        const lockName='lock-001';
        //await redis.set(redisName,100,'EX',-1) //向redis 写入数据
        const addLock=await redis.setnx(lockName,'');//加锁成功返回true 已存在锁,枷锁失败都返回false
        if(!addLock) return {statusCode:1001,data:{},message:'error,Please try again'};//解决多人
        const count=await redis.get(redisName);
        if(Number(count) <= 0) return {statusCode:1001,data:{},message:'error,Insufficient quantity'};//商品数量不足
        let count1=0;
        try {
            count1=await redis.decr(redisName);  
        } catch (_error:any) {
            console.error(_error);
        }finally{
            redis.del(lockName);//秒杀成功,将所删除
        }
        console.log('count1=',count1);
        return count1 > 0 ? {statusCode:1000,data:{},message:'成功'}:{statusCode:1001,data:{},message:'失败'}
    }
}
export default DemoService;

上面的代码是不是看起来很完善了?
no! 如果删除锁的时候,出现一些问题,造成删除锁失败,怎么办

完善代码3 给锁加一个合理的过期时间

import redis from '../modules/redis';
class DemoService{
    static async miaosha(){
        const redisName='mssp-001'
        const lockName='lock-001';
        //await redis.set(redisName,100,'EX',-1) //向redis 写入数据
        const addLock=await redis.setnx(lockName,'');//加锁成功返回true 已存在锁,枷锁失败都返回false
        if(!addLock) return {statusCode:1001,data:{},message:'error,Please try again'};//解决多人
        await redis.setex(lockName, 30,redisName);// 增加30秒过期处理
        const count=await redis.get(redisName);
        if(Number(count) <= 0) return {statusCode:1001,data:{},message:'error,Insufficient quantity'};//商品数量不足
        let count1=0;
        try {
            count1=await redis.decr(redisName);  
        } catch (_error:any) {
            console.error(_error);
        }finally{
            redis.del(lockName);//秒杀成功,将所删除
        }
        console.log('count1=',count1);
        return count1 > 0 ? {statusCode:1000,data:{},message:'成功'}:{statusCode:1001,data:{},message:'失败'}
    }
}
export default DemoService 

写到这里,心里想的是不是这下肯定没问题了

但是 问题还是有的

如果用户A 这个过程超过了设置锁的30s 锁过期,此时B用户进入,A删锁后,C又可以进入,
同样会出现超卖
怎么办呢?

完善代码4

给setnx value 标记是哪个用户的,此处可用uuid、guid 能标明唯一用户就可以
解决方案一:

import redis from '../modules/redis';
import uuid  from 'node-uuid';
class DemoService{
    static async miaosha(){
        const redisName='mssp-001'
        const lockName='lock-001';
        const uid=uuid.v4();
        //await redis.set(redisName,100,'EX',-1) //向redis 写入数据
        const addLock=await redis.setnx(lockName,'');//加锁成功返回true 已存在锁,枷锁失败都返回false
        if(!addLock) return {statusCode:1001,data:{},message:'error,Please try again'};//解决多人
        await redis.setex(lockName, 30, redisName+uid);// 增加30秒过期处理
        const count=await redis.get(redisName);
        if(Number(count) <= 0) return {statusCode:1001,data:{},message:'error,Insufficient quantity'};//商品数量不足
        let count1=0;
        try {
            count1=await redis.decr(redisName);  
        } catch (_error:any) {
            console.error(_error);
        }finally{
            const lockData=await redis.get(lockName);//
            if(lockData === redisName+uid) redis.del(lockName);//秒杀成功,如果是当前用户添加的锁,将所删除,防止锁时间超时,出现超卖
        }
        console.log('count1=',count1);
        return count1 > 0 ? {statusCode:1000,data:{},message:'成功'}:{statusCode:1001,data:{},message:'失败'}
    }
}
export default DemoService;

解决方案二 : 在加锁成功后去开启个定时器,间隔10秒去更新一次锁的时间,将时间后延30s

import redis from '../modules/redis';
import uuid  from 'node-uuid';
class DemoService{
    static async miaosha(){
        const redisName='mssp-001'
        const lockName='lock-001';
        const uid=uuid.v4();
        let t:any=null
        //await redis.set(redisName,100,'EX',-1) //向redis 写入数据
        const addLock=await redis.setnx(lockName,'');//加锁成功返回true 已存在锁,枷锁失败都返回false
        if(!addLock) return {statusCode:1001,data:{},message:'error,Please try again'};//解决多人
        await redis.setex(lockName, 30, redisName+uid);// 增加30秒过期处理
        //此处可添加定时器
        t=setInterval(function name() {
            redis.setex(lockName, 30, redisName+uid);// 增加30秒过期处理
        },5000)
        const count=await redis.get(redisName);
        if(Number(count) <= 0) return {statusCode:1001,data:{},message:'error,Insufficient quantity'};//商品数量不足
        let count1=0;
        try {
            count1=await redis.decr(redisName);  
        } catch (_error:any) {
            console.error(_error);
        }finally{
            
            clearInterval(t);//清除定时器
            const lockData=await redis.get(lockName);
            if(lockData === redisName+uid) redis.del(lockName);//秒杀成功,如果是当前用户添加的锁,将所删除,防止锁时间超时,出现超卖
            
        }
        console.log('count1=',count1);
        return count1 > 0 ? {statusCode:1000,data:{},message:'成功'}:{statusCode:1001,data:{},message:'失败'}
    }
}
export default DemoService;

以为这就完善了?
no
如果出现服务器脱机怎么办?
如果redis是主从、哨兵、集群模式,一个节点挂了怎么?添加一个新的节点怎么办?
非代码层面的问题,后面文章在详解

压测 建议用jmeter, 被卸载了,就用工具 Apifox 测试吧

自动化测试 线程数 100,循环5次 将redis商品数量设置成10000

在这里插入图片描述

运行

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一只倒霉的猫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值