下面我将详细讲解“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_HOST
为localhost
,ADVERTISED_PORT
为9092
。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的库存信息:
$ curl http://localhost:3000/seckill/1
Not Found
- 查询商品1的库存信息(再次):
$ curl http://localhost:3000/seckill/1
Not Found
- 查询商品2的库存信息:
$ curl http://localhost:3000/seckill/2
Not Found
- 查询商品1的库存信息:
$ curl http://localhost:3000/seckill/1
Not Found
- 查询商品2的库存信息:
$ curl http://localhost:3000/seckill/2
Not Found
- 发起商品1的抢购请求:
$ curl http://localhost:3000/seckill/1
OK
- 再次发起商品1的抢购请求:
$ curl http://localhost:3000/seckill/1
Conflict
- 发起商品2的抢购请求:
$ curl http://localhost:3000/seckill/2
OK
- 查询商品1的库存信息:
$ curl http://localhost:3000/seckill/1
Not Found
- 查询商品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技术站