一、准备数据库表
创建商品库存表 db_stock ,插入一条数据
DROP TABLE IF EXISTS `db_stock`;
CREATE TABLE `db_stock` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`goods_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '商品id',
`inventory_total` int(11) NULL DEFAULT NULL COMMENT '总库存',
`inventory_remain` int(11) NULL DEFAULT NULL COMMENT '剩余库存',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '记录实时库存表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of db_stock
-- ----------------------------
INSERT INTO `db_stock` VALUES (1, 'goods_01', 100, 100);
SET FOREIGN_KEY_CHECKS = 1;
创建出库信息表 db_checkout
DROP TABLE IF EXISTS `db_checkout`;
CREATE TABLE `db_checkout` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`goods_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '商品id',
`opt_num` int(11) NULL DEFAULT NULL COMMENT '操作数量',
`inventory_remain` int(11) NULL DEFAULT NULL COMMENT '剩余库存',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '出库记录表' ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
查看数据库
mysql> select * from db_stock;
+----+----------+-----------------+------------------+
| id | goods_id | inventory_total | inventory_remain |
+----+----------+-----------------+------------------+
| 1 | goods_01 | 100 | 100 |
+----+----------+-----------------+------------------+
1 row in set (0.00 sec)
mysql> select * from db_checkout;
Empty set (0.00 sec)
mysql>
二、准备Nodejs接口(基于Koa2)
接口说明:
url dm/testConcurrentOrder
method get
data initDB //首次初始化数据
goods_id //商品id
opt_num //出库数量
router.js
const router = require('koa-router')();
const dm_model = require("../models/dm_model");
router.preffix("/dm");
/**
* 测试并发
* testConcurrentOrder
*/
router.get('/testConcurrentOrder', async function (ctx) {
let reqParams = ctx.reqParams;
try {
ctx.body = await dm_model.testConcurrentOrder(reqParams);
} catch (e) {
ctx.body = {
success: false,
msg: `出库异常: ${e.toString()}`,
}
}
});
module.exports = router;
model.js
let db = require("../utils/mysql_util").db; let moment = require("moment");
let model = {};
/**
* 测试并发
* @param reqParams
* @returns {Promise<{msg: string, success: boolean}>}
*/
model.testConcurrentOrder = async function (reqParams) {
let initDB = !!reqParams.initDB || 0;// 是否初始化db
let goods_id = reqParams.goods_id || "goods_01";// 商品id
let opt_num = reqParams.opt_num || 1;// 出库数量
if (initDB) {
// 清除数据
await db.query(`TRUNCATE db_stock`);
await db.query(`TRUNCATE db_checkout`);
// 插入库存数据
await db.query(`INSERT INTO db_stock(goods_id, inventory_total, inventory_remain) VALUES ('${goods_id}', 100, 100)`);
return {
success: true,
msg: '初始化DB成功',
}
} else {
let tran = await db.beginTransaction();
// 查询剩余库存
let querySql = `SELECT
t.inventory_total,
t.inventory_remain
FROM
db_stock t
WHERE
1 = 1
AND t.goods_id = ?`;
try {
let result = {};
let queryResult = await tran.queryOne(querySql, [goods_id]);
if (!queryResult) {
result = {
success: false,
msg: `无法匹配商品id:${goods_id}`
}
} else {
let inventory_remain = queryResult.inventory_remain - 1;
// 新增出库记录
let checkoutSql = `INSERT INTO db_checkout(goods_id, opt_num, inventory_remain, create_time) VALUES (?, ?, ?, ?)`;
await tran.query(checkoutSql, [goods_id, opt_num, inventory_remain, new Date()]);
// 更新库存信息
let updateStockSql = `UPDATE db_stock t SET inventory_remain = ? WHERE t.goods_id = ?`;
// 更新库存
await tran.query(updateStockSql, [inventory_remain, goods_id]);
result = {
success: true,
msg: '出库成功',
}
}
await tran.commit();
return result;
} catch (e) {
await tran.rollback();
throw e;
}
}
};
module.exports = model;
三、并发测试
总共请求20次,20个并发同时请求
C:\WINDOWS\system32>ab.exe -n 20 -c 20 http://localhost:3000/dm/testConcurrentOrder
This is ApacheBench, Version 2.3 <$Revision: 1843412 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking localhost (be patient).....done
Server Software:
Server Hostname: localhost
Server Port: 3000
Document Path: /dm/testConcurrentOrder
Document Length: 228 bytes
Concurrency Level: 20
Time taken for tests: 24.284 seconds
Complete requests: 20
Failed requests: 0
Total transferred: 7420 bytes
HTML transferred: 4560 bytes
Requests per second: 0.82 [#/sec] (mean)
Time per request: 24284.077 [ms] (mean)
Time per request: 1214.204 [ms] (mean, across all concurrent requests)
Transfer rate: 0.30 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.4 0 1
Processing: 1590 2674 4696.9 1638 22629
Waiting: 1589 2673 4696.2 1637 22625
Total: 1590 2674 4696.9 1638 22629
Percentage of the requests served within a certain time (ms)
50% 1638
66% 1644
75% 1648
80% 1651
90% 1655
95% 22629
98% 22629
99% 22629
100% 22629 (longest request)
C:\WINDOWS\system32>
查看并发请求后,数据库表的信息
mysql> select * from db_stock;
+----+----------+-----------------+------------------+
| id | goods_id | inventory_total | inventory_remain |
+----+----------+-----------------+------------------+
| 1 | goods_01 | 100 | 97 |
+----+----------+-----------------+------------------+
1 row in set (0.00 sec)
mysql> select * from db_checkout;
+----+----------+---------+------------------+---------------------+
| id | goods_id | opt_num | inventory_remain | create_time |
+----+----------+---------+------------------+---------------------+
| 61 | goods_01 | 1 | 99 | 2019-10-09 17:31:01 |
| 62 | goods_01 | 1 | 98 | 2019-10-09 17:31:01 |
| 63 | goods_01 | 1 | 98 | 2019-10-09 17:31:01 |
| 64 | goods_01 | 1 | 98 | 2019-10-09 17:31:01 |
| 65 | goods_01 | 1 | 98 | 2019-10-09 17:31:01 |
| 66 | goods_01 | 1 | 98 | 2019-10-09 17:31:01 |
| 67 | goods_01 | 1 | 98 | 2019-10-09 17:31:01 |
| 68 | goods_01 | 1 | 98 | 2019-10-09 17:31:01 |
| 69 | goods_01 | 1 | 98 | 2019-10-09 17:31:01 |
| 70 | goods_01 | 1 | 98 | 2019-10-09 17:31:01 |
| 71 | goods_01 | 1 | 98 | 2019-10-09 17:31:01 |
| 72 | goods_01 | 1 | 97 | 2019-10-09 17:31:01 |
| 73 | goods_01 | 1 | 97 | 2019-10-09 17:31:01 |
| 74 | goods_01 | 1 | 97 | 2019-10-09 17:31:01 |
| 75 | goods_01 | 1 | 97 | 2019-10-09 17:31:01 |
| 76 | goods_01 | 1 | 97 | 2019-10-09 17:31:01 |
| 77 | goods_01 | 1 | 97 | 2019-10-09 17:31:01 |
| 78 | goods_01 | 1 | 97 | 2019-10-09 17:31:01 |
| 79 | goods_01 | 1 | 97 | 2019-10-09 17:31:01 |
| 80 | goods_01 | 1 | 97 | 2019-10-09 17:31:01 |
+----+----------+---------+------------------+---------------------+
20 rows in set (0.00 sec)
mysql>
结果:并发请求20次,由于代码和数据库没有控制并发,导致数据错误。
四:解决方案
1、加行锁
在查询剩余库存的时候调整sql为
// 查询剩余库存
let querySql = `SELECT
t.inventory_total,
t.inventory_remain
FROM
db_stock t
WHERE
1 = 1
AND t.goods_id = ? for update`;
这个是基于数据库的行锁实现,对于单节点Mysql实现没有难度,测试结果就不放了。
2、使用redis分布式锁(基于Redlock的实现)
调整model.js
let db = require("../utils/mysql_util").db; let moment = require("moment");
let {async_redis_client, redlock} = require("../utils/redis_util");
let model = {};
/**
* 测试并发
* @param reqParams
* @returns {Promise<{msg: string, success: boolean}>}
*/
model.testConcurrentOrder = async function (reqParams) {
// 之前本人有个误区,设置资源锁的key为 `goods_${inventory_remain}`,但是仍然有写入错误的数据。
// 思考了一下,觉得是很多请求同时进来,从加锁->释放锁相当于进入队列,前一个操作完成后释放了锁,后一个才要开始加锁。
// 假如有100个请求同时进来,其中50个请求在加锁之前查询到的库存都是100,提交事务的出库信息都是剩余库存99,那么有49条数据都会形成脏数据。
// 所以这里必须设置资源锁的key一致,只要有一个资源还没有释放锁,其他资源就不允许进行查询库存的操作,问题解决。
// 上锁的资源
let resource_lock_key = `redislock_goods`;
// 上锁资源5s,操作完成或者到时间后自动释放
let resource_ttl = 5 * 1000;
// 开始加锁
let lock = await redlock.lock(resource_lock_key, resource_ttl);
console.log(resource_lock_key, lock.value);
console.log(moment().format('YYYY-MM-DD HH:mm:ss:SSS'));
let initDB = !!reqParams.initDB || 0;// 是否初始化db
let goods_id = reqParams.goods_id || "goods_01";// 商品id
let opt_num = reqParams.opt_num || 1;// 出库数量
let result = {};
if (initDB) {
// 清除数据
await db.query(`TRUNCATE db_stock`);
await db.query(`TRUNCATE db_checkout`);
// 插入库存数据
await db.query(`INSERT INTO db_stock(goods_id, inventory_total, inventory_remain) VALUES ('${goods_id}', 100, 100)`);
result = {
success: true,
msg: '初始化DB成功',
};
} else {
let querySql = `SELECT
t.inventory_total,
t.inventory_remain
FROM
db_stock t
WHERE
1 = 1
AND t.goods_id = ?`;
let queryResult = await db.queryOne(querySql, [goods_id]);
if (!queryResult) {
result = {
success: false,
msg: `无法匹配商品id:${goods_id}`
}
} else {
// 当前库存
let inventory_remain = queryResult.inventory_remain; // 等待设置的新库存
let inventory_remain_new = inventory_remain - 1;
let tran = await db.beginTransaction();
try {
// 新增出库记录
let checkoutSql = `INSERT INTO db_checkout(goods_id, opt_num, inventory_remain, create_time) VALUES (?, ?, ?, ?)`;
await tran.query(checkoutSql, [goods_id, opt_num, inventory_remain_new, new Date()]);
// 更新库存信息
let updateStockSql = `UPDATE db_stock t SET inventory_remain = ? WHERE t.goods_id = ?`;
// 更新库存
await tran.query(updateStockSql, [inventory_remain_new, goods_id]);
await tran.commit();
result = {
success: true,
msg: '出库成功',
}
} catch (e) {
await tran.rollback();
// 抛出异常之前,及时释放资源锁
await lock.unlock();
throw e;
}
}
}
if (lock) {
// 及时释放资源锁
await lock.unlock();
}
return result;
};
module.exports = model;
附上redis_util.js的代码
// Redis单节点
let date_util = require("../utils/date_util");
let RedLock = require("redlock");
// redis
let redis = require("redis");
// async_redis
let async_redis = require("async-redis");
// redis_option
let redis_option = {
//redis服务器的ip地址
host: '127.0.0.1',
//redis服务器的端口
port: '6379',
// 授权密码,修改redis.windows.conf requirepass 1234,密码错误报错: NOAUTH Authentication required
password: '1234',
// redis select $index,默认0
db: '0',
// reply number => string 默认null
string_numbers: null,
// replay strings => buffer,默认false
return_buffers: false,
// 是否检测缓冲区,默认false
detect_buffers: false,
// 长链接,默认true
socket_keepalive: true,
// 长链接延迟时间,默认0
socket_initialdelay: 0,
// 禁用ready检查,默认false
no_ready_check: false,
// 启用离线命令队列,默认true
enable_offline_queue: true,
// // @Deprecated 重连最大延迟,默认null
// retry_max_delay: null,
// // @Deprecated 连接超时时间,默认60*60*1000,默认1h
// connect_timeout: 60 * 60 * 1000,
// // @Deprecated 最大连接次数,默认0,设置为1将阻止任何重新连接尝试
// max_attempts: 0,
// 默认false, 如果设置为true,则在重新建立连接后,将重试连接丢失时未执行的所有命令。如果使用状态更改命令(例如incr),请小心使用此命令。这在使用阻塞命令时特别有用。
retry_unfulfilled_commands: false,
retry_strategy: function (options) {
if (options.error && options.error.code === 'ECONNREFUSED') {
// 拒绝连接
let errMsg = 'The server refused the connection';
console.log(date_util.format(), 'retry_strategy', errMsg);
return new Error(errMsg);
}
// 默认 60*60*1000
if (options.total_retry_time > 60 * 60 * 1000) {
// 超过最大重试时间
let errMsg = 'Retry time exhausted';
console.log(date_util.format(), 'retry_strategy', errMsg);
return new Error(errMsg);
}
// 默认10
if (options.attempt > 10) {
// 超过最大重试次数
let errMsg = 'maximum connection attempts exceeded';
console.log(date_util.format(), 'retry_strategy', errMsg);
return new Error(errMsg);
}
// reconnect after
return Math.min(options.attempt * 1000, 3000);
}
};
// redis_client
let redis_client = redis.createClient(redis_option);
// async_redis_client
let async_redis_client = async_redis.createClient(redis_option);
redis_client.on("error", function (err) {
console.log(date_util.format(date_util.pattern().ymdhmsSSS), err.toString());
});
async_redis_client.on("error", function (err) {
console.log(date_util.format(date_util.pattern().ymdhmsSSS), err.toString());
});
// NOTE: 这里只能使用redis_client
let redlock = new RedLock([redis_client]);
redlock.on('clientError', function (err) {
console.error(date_util.format(date_util.pattern().ymdhmsSSS), 'A redis error has occurred:', err);
});
module.exports = {
redis_client,
async_redis_client,
redlock
};
ab测试
C:\WINDOWS\system32>ab.exe -n 95 -c 95 http://localhost:3000/dm/testConcurrentOrder
This is ApacheBench, Version 2.3 <$Revision: 1843412 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking localhost (be patient).....done
Server Software:
Server Hostname: localhost
Server Port: 3000
Document Path: /dm/testConcurrentOrder
Document Length: 46 bytes
Concurrency Level: 95
Time taken for tests: 1.867 seconds
Complete requests: 95
Failed requests: 0
Total transferred: 17860 bytes
HTML transferred: 4370 bytes
Requests per second: 50.88 [#/sec] (mean)
Time per request: 1867.041 [ms] (mean)
Time per request: 19.653 [ms] (mean, across all concurrent requests)
Transfer rate: 9.34 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.2 0 1
Processing: 29 932 437.8 909 1840
Waiting: 21 931 437.9 908 1839
Total: 29 932 437.8 909 1840
Percentage of the requests served within a certain time (ms)
50% 907
66% 1123
75% 1257
80% 1346
90% 1578
95% 1727
98% 1783
99% 1840
100% 1840 (longest request)
C:\WINDOWS\system32>
Mysql查询结果:
mysql> select * from db_stock;
+----+----------+-----------------+------------------+
| id | goods_id | inventory_total | inventory_remain |
+----+----------+-----------------+------------------+
| 1 | goods_01 | 100 | 5 |
+----+----------+-----------------+------------------+
1 row in set (0.00 sec)
mysql> select * from db_checkout;
+----+----------+---------+------------------+---------------------+
| id | goods_id | opt_num | inventory_remain | create_time |
+----+----------+---------+------------------+---------------------+
| 1 | goods_01 | 1 | 99 | 2019-10-10 18:36:15 |
| 2 | goods_01 | 1 | 98 | 2019-10-10 18:36:15 |
| 3 | goods_01 | 1 | 97 | 2019-10-10 18:36:15 |
| 4 | goods_01 | 1 | 96 | 2019-10-10 18:36:15 |
| 5 | goods_01 | 1 | 95 | 2019-10-10 18:36:15 |
| 6 | goods_01 | 1 | 94 | 2019-10-10 18:36:15 |
| 7 | goods_01 | 1 | 93 | 2019-10-10 18:36:15 |
| 8 | goods_01 | 1 | 92 | 2019-10-10 18:36:15 |
| 9 | goods_01 | 1 | 91 | 2019-10-10 18:36:15 |
| 10 | goods_01 | 1 | 90 | 2019-10-10 18:36:15 |
| 11 | goods_01 | 1 | 89 | 2019-10-10 18:36:15 |
| 12 | goods_01 | 1 | 88 | 2019-10-10 18:36:15 |
| 13 | goods_01 | 1 | 87 | 2019-10-10 18:36:15 |
| 14 | goods_01 | 1 | 86 | 2019-10-10 18:36:15 |
| 15 | goods_01 | 1 | 85 | 2019-10-10 18:36:15 |
| 16 | goods_01 | 1 | 84 | 2019-10-10 18:36:15 |
| 17 | goods_01 | 1 | 83 | 2019-10-10 18:36:15 |
| 18 | goods_01 | 1 | 82 | 2019-10-10 18:36:15 |
| 19 | goods_01 | 1 | 81 | 2019-10-10 18:36:15 |
| 20 | goods_01 | 1 | 80 | 2019-10-10 18:36:15 |
| 21 | goods_01 | 1 | 79 | 2019-10-10 18:36:15 |
| 22 | goods_01 | 1 | 78 | 2019-10-10 18:36:15 |
| 23 | goods_01 | 1 | 77 | 2019-10-10 18:36:15 |
| 24 | goods_01 | 1 | 76 | 2019-10-10 18:36:15 |
| 25 | goods_01 | 1 | 75 | 2019-10-10 18:36:15 |
| 26 | goods_01 | 1 | 74 | 2019-10-10 18:36:15 |
| 27 | goods_01 | 1 | 73 | 2019-10-10 18:36:15 |
| 28 | goods_01 | 1 | 72 | 2019-10-10 18:36:15 |
| 29 | goods_01 | 1 | 71 | 2019-10-10 18:36:15 |
| 30 | goods_01 | 1 | 70 | 2019-10-10 18:36:15 |
| 31 | goods_01 | 1 | 69 | 2019-10-10 18:36:15 |
| 32 | goods_01 | 1 | 68 | 2019-10-10 18:36:15 |
| 33 | goods_01 | 1 | 67 | 2019-10-10 18:36:15 |
| 34 | goods_01 | 1 | 66 | 2019-10-10 18:36:15 |
| 35 | goods_01 | 1 | 65 | 2019-10-10 18:36:15 |
| 36 | goods_01 | 1 | 64 | 2019-10-10 18:36:15 |
| 37 | goods_01 | 1 | 63 | 2019-10-10 18:36:15 |
| 38 | goods_01 | 1 | 62 | 2019-10-10 18:36:15 |
| 39 | goods_01 | 1 | 61 | 2019-10-10 18:36:15 |
| 40 | goods_01 | 1 | 60 | 2019-10-10 18:36:15 |
| 41 | goods_01 | 1 | 59 | 2019-10-10 18:36:15 |
| 42 | goods_01 | 1 | 58 | 2019-10-10 18:36:15 |
| 43 | goods_01 | 1 | 57 | 2019-10-10 18:36:15 |
| 44 | goods_01 | 1 | 56 | 2019-10-10 18:36:15 |
| 45 | goods_01 | 1 | 55 | 2019-10-10 18:36:15 |
| 46 | goods_01 | 1 | 54 | 2019-10-10 18:36:16 |
| 47 | goods_01 | 1 | 53 | 2019-10-10 18:36:16 |
| 48 | goods_01 | 1 | 52 | 2019-10-10 18:36:16 |
| 49 | goods_01 | 1 | 51 | 2019-10-10 18:36:16 |
| 50 | goods_01 | 1 | 50 | 2019-10-10 18:36:16 |
| 51 | goods_01 | 1 | 49 | 2019-10-10 18:36:16 |
| 52 | goods_01 | 1 | 48 | 2019-10-10 18:36:16 |
| 53 | goods_01 | 1 | 47 | 2019-10-10 18:36:16 |
| 54 | goods_01 | 1 | 46 | 2019-10-10 18:36:16 |
| 55 | goods_01 | 1 | 45 | 2019-10-10 18:36:16 |
| 56 | goods_01 | 1 | 44 | 2019-10-10 18:36:16 |
| 57 | goods_01 | 1 | 43 | 2019-10-10 18:36:16 |
| 58 | goods_01 | 1 | 42 | 2019-10-10 18:36:16 |
| 59 | goods_01 | 1 | 41 | 2019-10-10 18:36:16 |
| 60 | goods_01 | 1 | 40 | 2019-10-10 18:36:16 |
| 61 | goods_01 | 1 | 39 | 2019-10-10 18:36:16 |
| 62 | goods_01 | 1 | 38 | 2019-10-10 18:36:16 |
| 63 | goods_01 | 1 | 37 | 2019-10-10 18:36:16 |
| 64 | goods_01 | 1 | 36 | 2019-10-10 18:36:16 |
| 65 | goods_01 | 1 | 35 | 2019-10-10 18:36:16 |
| 66 | goods_01 | 1 | 34 | 2019-10-10 18:36:16 |
| 67 | goods_01 | 1 | 33 | 2019-10-10 18:36:16 |
| 68 | goods_01 | 1 | 32 | 2019-10-10 18:36:16 |
| 69 | goods_01 | 1 | 31 | 2019-10-10 18:36:16 |
| 70 | goods_01 | 1 | 30 | 2019-10-10 18:36:16 |
| 71 | goods_01 | 1 | 29 | 2019-10-10 18:36:16 |
| 72 | goods_01 | 1 | 28 | 2019-10-10 18:36:16 |
| 73 | goods_01 | 1 | 27 | 2019-10-10 18:36:16 |
| 74 | goods_01 | 1 | 26 | 2019-10-10 18:36:16 |
| 75 | goods_01 | 1 | 25 | 2019-10-10 18:36:16 |
| 76 | goods_01 | 1 | 24 | 2019-10-10 18:36:16 |
| 77 | goods_01 | 1 | 23 | 2019-10-10 18:36:16 |
| 78 | goods_01 | 1 | 22 | 2019-10-10 18:36:16 |
| 79 | goods_01 | 1 | 21 | 2019-10-10 18:36:16 |
| 80 | goods_01 | 1 | 20 | 2019-10-10 18:36:16 |
| 81 | goods_01 | 1 | 19 | 2019-10-10 18:36:16 |
| 82 | goods_01 | 1 | 18 | 2019-10-10 18:36:16 |
| 83 | goods_01 | 1 | 17 | 2019-10-10 18:36:16 |
| 84 | goods_01 | 1 | 16 | 2019-10-10 18:36:16 |
| 85 | goods_01 | 1 | 15 | 2019-10-10 18:36:16 |
| 86 | goods_01 | 1 | 14 | 2019-10-10 18:36:16 |
| 87 | goods_01 | 1 | 13 | 2019-10-10 18:36:16 |
| 88 | goods_01 | 1 | 12 | 2019-10-10 18:36:16 |
| 89 | goods_01 | 1 | 11 | 2019-10-10 18:36:16 |
| 90 | goods_01 | 1 | 10 | 2019-10-10 18:36:16 |
| 91 | goods_01 | 1 | 9 | 2019-10-10 18:36:16 |
| 92 | goods_01 | 1 | 8 | 2019-10-10 18:36:16 |
| 93 | goods_01 | 1 | 7 | 2019-10-10 18:36:16 |
| 94 | goods_01 | 1 | 6 | 2019-10-10 18:36:16 |
| 95 | goods_01 | 1 | 5 | 2019-10-10 18:36:16 |
+----+----------+---------+------------------+---------------------+
95 rows in set (0.00 sec)
mysql>
完美! |