Docker + Nodejs + Kafka + Redis + MySQL搭建简单秒杀环境

下面我将详细讲解“Docker + Nodejs + Kafka + Redis + MySQL搭建简单秒杀环境”的完整攻略。

1. 前置条件

在开始搭建秒杀环境之前,需要先安装Docker和Docker Compose,并确保已经熟悉Docker和Docker Compose的基本使用。

2. 搭建过程

2.1 新建项目目录

首先,新建一个项目目录,比如seckill

$ mkdir seckill
$ cd seckill

2.2 新建Docker Compose文件

在项目目录中新建一个名为docker-compose.yml的文件,内容如下:

version: '3'

services:
  mysql:
    image: mysql:5.7
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: seckill
  redis:
    image: redis:latest
  kafka:
    image: spotify/kafka
    ports:
      - "2181:2181"
      - "9092:9092"
    environment:
      ADVERTISED_HOST: localhost
      ADVERTISED_PORT: "9092"
  node:
    build: .
    ports:
      - "3000:3000"
    depends_on:
      - kafka
      - redis
      - mysql
    environment:
      MYSQL_HOST: mysql
      MYSQL_PORT: 3306
      MYSQL_USER: root
      MYSQL_PASSWORD: root
      MYSQL_DATABASE: seckill
      REDIS_HOST: redis
      REDIS_PORT: 6379
      KAFKA_HOST: kafka
      KAFKA_PORT: 9092

该文件定义了5个服务:

  • mysql:使用MySQL 5.7的镜像,设置了root用户的密码为root,创建了一个名为seckill的数据库。
  • redis:使用最新版的Redis镜像。
  • kafka:使用Spotify的Kafka镜像,暴露了2181和9092两个端口,设置了Kafka的ADVERTISED_HOSTlocalhostADVERTISED_PORT9092
  • node:基于项目目录下的Dockerfile进行构建,暴露3000端口,依赖于mysql、redis和kafka三个服务,并设置了环境变量以供应用程序使用。

2.3 新建Dockerfile

在项目目录中新建一个名为Dockerfile的文件,内容如下:

FROM node:latest

WORKDIR /app

COPY package*.json ./

RUN npm install

COPY . .

EXPOSE 3000

CMD [ "npm", "run", "start:docker" ]

该文件定义了一个基于最新版的Node.js镜像的Dockerfile,其中:

  • 使用WORKDIR指令创建了一个/app目录并将其作为工作目录。
  • 使用COPY指令将package*.json文件复制到工作目录中。
  • 使用RUN指令执行npm install命令安装应用程序的依赖。
  • 使用COPY指令将当前目录下的所有文件复制到工作目录中。
  • 使用EXPOSE指令暴露了应用程序监听的端口。
  • 使用CMD指令指定容器启动时运行的命令。

2.4 编写应用程序

在项目目录下创建一个src目录,并在该目录下创建一个名为index.js的文件,内容如下:

const kafka = require('kafka-node')
const redis = require('redis')
const mysql = require('mysql')
const express = require('express')

const app = express()

const mysqlHost = process.env.MYSQL_HOST
const mysqlPort = process.env.MYSQL_PORT || 3306
const mysqlUser = process.env.MYSQL_USER || 'root'
const mysqlPassword = process.env.MYSQL_PASSWORD
const mysqlDatabase = process.env.MYSQL_DATABASE

const redisHost = process.env.REDIS_HOST || 'localhost'
const redisPort = process.env.REDIS_PORT || 6379

const kafkaHost = process.env.KAFKA_HOST || 'localhost'
const kafkaPort = process.env.KAFKA_PORT || 9092

const redisClient = redis.createClient(redisPort, redisHost)
const kafkaClient = new kafka.KafkaClient({ kafkaHost: `${kafkaHost}:${kafkaPort}` })
const producer = new kafka.Producer(kafkaClient)

producer.on('ready', () => {
  console.log('Kafka producer is ready')
})

producer.on('error', (error) => {
  console.error('Kafka producer encountered error:', error)
})

const connection = mysql.createConnection({
  host: mysqlHost,
  port: mysqlPort,
  user: mysqlUser,
  password: mysqlPassword,
  database: mysqlDatabase,
})

connection.connect((error) => {
  if (error) {
    console.error('MySQL connection failed:', error)
  } else {
    console.log('MySQL connection is ready')
  }
})

const getRemainingSeconds = (endTime) => {
  const remainingSeconds = endTime - Math.floor(Date.now() / 1000)
  return remainingSeconds > 0 ? remainingSeconds : 0
}

app.get('/seckill/:itemId', async (req, res, next) => {
  const itemId = req.params.itemId

  // check if the item is in stock
  const stockName = `stock:${itemId}`
  redisClient.get(stockName, async (error, stock) => {
    if (error) {
      console.error('Failed to retrieve stock information from Redis:', error)
      res.status(500).send('Internal Server Error')
      return
    }

    if (!stock) {
      console.log(`Item ${itemId} is out of stock`)
      res.status(404).send('Not Found')
      return
    }

    // check if the item is being snapped up
    const snapUpName = `snap_up:${itemId}`
    redisClient.get(snapUpName, async (error, snapUp) => {
      if (error) {
        console.error('Failed to retrieve snap-up information from Redis:', error)
        res.status(500).send('Internal Server Error')
        return
      }

      if (snapUp) {
        console.log(`Item ${itemId} is being snapped up`)
        res.status(409).send('Conflict')
        return
      }

      // prepare to snap up the item
      redisClient.incr(snapUpName, async (error, snapUp) => {
        if (error) {
          console.error('Failed to increment snap-up information in Redis:', error)
          res.status(500).send('Internal Server Error')
          return
        }

        if (snapUp > 1) {
          console.log(`Item ${itemId} has already been snapped up`)
          res.status(409).send('Conflict')
          return
        }

        // decrement the item's available stock
        redisClient.decr(stockName, async (error, stock) => {
          if (error) {
            console.error('Failed to decrement stock information in Redis:', error)
            res.status(500).send('Internal Server Error')
            return
          }

          if (stock < 0) {
            // roll back the snap-up
            redisClient.del(snapUpName, async (error) => {
              if (error) {
                console.error('Failed to delete snap-up information from Redis:', error)
              }

              console.log(`Item ${itemId} has run out of stock`)
              res.status(404).send('Not Found')
            })
          } else {
            // create the order in MySQL
            const order = { item_id: itemId }
            connection.query('INSERT INTO orders SET ?', order, async (error) => {
              if (error) {
                console.error('Failed to create order in MySQL:', error)

                // roll back the snap-up
                redisClient.del(snapUpName, async (error) => {
                  if (error) {
                    console.error('Failed to delete snap-up information from Redis:', error)
                  }

                  // restore the item's stock
                  redisClient.incr(stockName, async (error) => {
                    if (error) {
                      console.error('Failed to increment stock information in Redis:', error)
                    }
                  })

                  res.status(500).send('Internal Server Error')
                })
              } else {
                // publish the order information to Kafka
                producer.send([{ topic: 'orders', messages: JSON.stringify(order) }], (error) => {
                  if (error) {
                    console.error('Failed to publish order information to Kafka:', error)
                  }

                  // set the remaining seconds of snap-up time to Redis
                  const remainingSeconds = getRemainingSeconds(1619799120) // 2021/5/1 0:12:00
                  redisClient.set(snapUpName, remainingSeconds, async (error) => {
                    if (error) {
                      console.error('Failed to set snap-up information to Redis:', error)
                    }

                    console.log(`Item ${itemId} has been snapped up`)
                    res.status(200).send('OK')
                  })
                })
              }
            })
          }
        })
      })
    })
  })
})

app.listen(3000, () => {
  console.log('Server is listening on port 3000')
})

该文件是一个基于Node.js的Express应用程序,使用了Kafka、Redis和MySQL的模块,并暴露了一个名为/seckill/:itemId的GET接口,该接口接受一个itemId参数表示秒杀的商品ID。在接口执行过程中,它会首先从Redis中查询商品的库存信息,如果库存不足,就会返回404 Not Found;如果商品正在被抢购中,则返回409 Conflict;如果商品可以抢购,则在Redis中标记该商品正在被抢购,然后将商品的库存量减少1,最后将抢购订单记录到MySQL中,同时将订单信息发布到Kafka中进行异步处理,返回200 OK

2.5 启动容器

在项目根目录下使用以下命令启动Docker容器:

$ docker-compose up

2.6 示例

上述过程完成后,可以使用一些工具来模拟抢购行为。比如使用curl命令,执行以下步骤:

  1. 查询商品1的库存信息:
$ curl http://localhost:3000/seckill/1
Not Found
  1. 查询商品1的库存信息(再次):
$ curl http://localhost:3000/seckill/1
Not Found
  1. 查询商品2的库存信息:
$ curl http://localhost:3000/seckill/2
Not Found
  1. 查询商品1的库存信息:
$ curl http://localhost:3000/seckill/1
Not Found
  1. 查询商品2的库存信息:
$ curl http://localhost:3000/seckill/2
Not Found
  1. 发起商品1的抢购请求:
$ curl http://localhost:3000/seckill/1
OK
  1. 再次发起商品1的抢购请求:
$ curl http://localhost:3000/seckill/1
Conflict
  1. 发起商品2的抢购请求:
$ curl http://localhost:3000/seckill/2
OK
  1. 查询商品1的库存信息:
$ curl http://localhost:3000/seckill/1
Not Found
  1. 查询商品2的库存信息:
$ curl http://localhost:3000/seckill/2
Not Found

以上步骤可以验证秒杀环境的正常工作。

总结

以上就是使用Docker和Node.js等技术搭建简单秒杀环境的完整攻略。在搭建过程中,我们通过Docker Compose统一管理了MySQL、Redis、Kafka和Node.js等服务的启动和配置,使得整个环境的部署和维护非常方便。同时,我们还演示了如何通过Node.js编写一个模拟秒杀的应用程序,使用了Kafka、Redis和MySQL等技术,并利用Docker Compose将这些组件一起协同工作,以供开发、测试和演示等目的使用。

本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:Docker + Nodejs + Kafka + Redis + MySQL搭建简单秒杀环境 - Python技术站

(0)
上一篇 2023年6月8日
下一篇 2023年6月8日

相关文章

  • 纯异步nodejs文件夹(目录)复制功能

    下面是“纯异步nodejs文件夹(目录)复制功能”的完整攻略。 一、了解异步编程 在介绍纯异步nodejs文件夹(目录)复制功能之前,需要先了解一下异步编程。 异步编程是指在一个执行单元(A)中调用另一个执行单元(B),而在B执行的同时,执行单元A可以继续执行,不必等待B完成。这种编程方式在Node.js中非常常见,因为Node.js处理大量I/O(输入输出…

    node js 2023年6月8日
    00
  • Apache和Nginx的优缺点详解_动力节点Java学院整理

    Apache和Nginx的优缺点详解 1. Apache的优缺点 1.1 优点 可定制性强:Apache 提供了大量的模块和插件,用户可以根据实际需求来安装和配置使用。 支持大部分脚本语言:Apache 支持大部分脚本语言,如PHP、Python、Perl等。 广泛的文档支持:Apache 作为一个老牌的Web服务器,文档非常丰富,用户可以轻松地找到任何想要…

    node js 2023年6月8日
    00
  • Node.js+jade抓取博客所有文章生成静态html文件的实例

    下面我来详细讲解一下Node.js+jade抓取博客所有文章生成静态html文件的实例的完整攻略: 1. 准备工作 在进行实例前,我们需要完成几个准备工作: 安装Node.js 首先,我们需要在电脑上安装Node.js。这个比较简单,在Node.js官网上下载对应操作系统的安装包,然后一路点击安装即可。 初始化Node项目 在命令行中通过npm init命令…

    node js 2023年6月8日
    00
  • Windows环境下npm install 报错: operation not permitted, rename的解决方法

    当我们在Windows环境下使用npm进行包的安装时,有时候可能会遇到”operation not permitted, rename”的问题,这是因为Windows系统有时候会给文件锁定,从而导致文件重命名失败。下面我将为大家提供两种解决方法。 方法一:使用管理员权限打开命令行 打开命令行时,需要使用管理员权限。在Windows系统下有两种方法打开命令行。…

    node js 2023年6月8日
    00
  • node.js中的fs.openSync方法使用说明

    node.js中的fs.openSync方法使用说明 fs.openSync() 方法用于使用文件路径字符串之前,获取对文件的访问。该方法通过一个文件路径字符串,与一组选项对象进行调用,返回一个整数(文件描述符),代表了一个通过该文件描述符可以进行操作的文件。 方法语法 fs.openSync(path[, flags[, mode]]) 方法参数 path…

    node js 2023年6月8日
    00
  • 如何用node优雅地打印全链路日志

    下面是详细的攻略。 1. 需求分析 在开发过程中,我们需要记录应用程序的全链路日志以便于排查问题和进行性能优化。要实现全链路日志,需要收集每个请求的相关信息,如请求方法、请求参数、响应状态码、响应时间、错误类型等信息。这些信息需要保留到一个日志文件中。 2. 策略设计 要优雅地打印全链路日志,我们需要使用以下策略: 定义一个格式化文本日志中间件,将收集的日志…

    node js 2023年6月8日
    00
  • 浅谈Express.js解析Post数据类型的正确姿势

    浅谈Express.js解析Post数据类型的正确姿势 在使用Node.js开发Web应用程序时,我们通常会使用Express.js框架来帮助我们搭建应用程序的基本结构。而处理Post请求,获取Post数据则是开发Web应用程序时必不可少的一部分。本篇文章将会详细讲解,在Express.js中,如何正确地解析不同类型的Post数据。 解析applicatio…

    node js 2023年6月8日
    00
  • Node.js创建一个Express服务的方法详解

    下面为你详细讲解创建一个Express服务的方法。 步骤一:安装Node.js和Express 在开始创建一个Express服务之前,你需要确保已经安装了Node.js和Express。如果还没有安装,可以前往官方网站进行下载和安装。 步骤二:创建项目文件夹 在创建Express服务之前,需要先创建项目文件夹。可以在终端中使用以下命令创建: mkdir my…

    node js 2023年6月8日
    00
合作推广
合作推广
分享本页
返回顶部