|
| 1 | + |
| 2 | +使用Sponge快速构建的微服务示例,主要实现抢购和订单功能,并通过分布式事务管理器DTM确保数据一致性,系统架构如下图所示: |
| 3 | + |
| 4 | + |
| 5 | + |
| 6 | +<br> |
| 7 | + |
| 8 | +这是[源码示例eshop](https://github.com/zhufuyi/sponge_examples/tree/main/_14_eshop),目录下包括了两个一样的代码示例,只是代码结构稍有不同,主要是为了展示说明sponge支持创建不同仓库模式的微服务项目,`example-1-multi-repo`是适合微服务多仓库(multi-repo),`example-2-mono-repo`是适合微服务单仓库(mono-repo)。 |
| 9 | + |
| 10 | +<br> |
| 11 | + |
| 12 | +### 准备环境 |
| 13 | + |
| 14 | +为了构建这个电商系统,准备生成代码工具: |
| 15 | + |
| 16 | +- **[Sponge](https://github.com/zhufuyi/sponge)**: 快速创建不同类型服务和模块代码。点击 [Sponge 安装说明](https://github.com/zhufuyi/sponge/blob/main/assets/install-cn.md) 以获取更多信息。 |
| 17 | + |
| 18 | +准备以下依赖服务: |
| 19 | + |
| 20 | +- **[DTM](https://github.com/dtm-labs/dtm)**:分布式事务管理器,用于保证系统在多服务调用中的数据一致性。 |
| 21 | +- **Redis**:结合 DTM,用作秒杀抢购的缓存。 |
| 22 | +- **Kafka**:作为订单系统的消息队列,用于处理订单异步消息。 |
| 23 | +- **MySQL**:为各个服务提供数据存储。 |
| 24 | + |
| 25 | +所有这些服务将运行在虚拟机中,虚拟机的 IP 地址为 `192.168.3.37`。 |
| 26 | + |
| 27 | +<br> |
| 28 | + |
| 29 | +### 启动 DTM 服务 |
| 30 | + |
| 31 | +DTM 是本系统的核心组件之一,负责管理订单和秒杀抢购的分布式事务。需要启动两个 DTM 服务实例,分别用于 MySQL 和 Redis 存储。 |
| 32 | + |
| 33 | +DTM下载地址:https://github.com/dtm-labs/dtm/releases/tag/v1.18.0 |
| 34 | + |
| 35 | +| 服务名 | 端口配置 | |
| 36 | +|-------------|-----------------------| |
| 37 | +| DTM-MySQL | HTTP: 36789, gRPC: 36790 | |
| 38 | +| DTM-Redis | HTTP: 35789, gRPC: 35790 | |
| 39 | + |
| 40 | +<br> |
| 41 | + |
| 42 | +#### 1. 启动使dtm-mysql服务 |
| 43 | + |
| 44 | +- 在 MySQL 中导入 DTM 需要的表结构: |
| 45 | + - [dtmcli.barrier.mysql.sql](https://github.com/dtm-labs/dtm/blob/main/sqls/dtmcli.barrier.mysql.sql) |
| 46 | + - [dtmsvr.storage.mysql.sql](https://github.com/dtm-labs/dtm/blob/main/sqls/dtmsvr.storage.mysql.sql) |
| 47 | + |
| 48 | +- 修改 DTM 配置文件 ([示例配置文件](https://github.com/dtm-labs/dtm/blob/main/conf.sample.yml)): |
| 49 | + |
| 50 | + ```yaml |
| 51 | + Store: # specify which engine to store trans status |
| 52 | + Driver: 'mysql' |
| 53 | + Host: '192.168.3.37' |
| 54 | + User: 'root' |
| 55 | + Password: '123456' |
| 56 | + Port: 3306 |
| 57 | + Db: 'dtm' |
| 58 | + |
| 59 | + # 使用注册与发现(sponge的dtm驱动已支持etcd、consul、nacos) |
| 60 | + #MicroService: |
| 61 | + #Driver: 'dtm-driver-sponge' |
| 62 | + #Target: 'etcd://127.0.0.1:2379/dtmservice' |
| 63 | + #EndPoint: 'grpc://127.0.0.1:36790' |
| 64 | + ``` |
| 65 | + |
| 66 | +- 启动 DTM 服务: |
| 67 | + |
| 68 | + ```bash |
| 69 | + #dtm 服务端口 http: 36789, grpc: 36790 |
| 70 | + ./dtm -c conf.yml |
| 71 | + ``` |
| 72 | + |
| 73 | +<br> |
| 74 | + |
| 75 | +#### 2. 启动dtm-redis服务 |
| 76 | + |
| 77 | +- 修改 DTM 配置文件 ([示例配置文件](https://github.com/dtm-labs/dtm/blob/main/conf.sample.yml)): |
| 78 | + |
| 79 | + ```yaml |
| 80 | + Store: # specify which engine to store trans status |
| 81 | + Driver: 'redis' |
| 82 | + Host: '192.168.3.37' |
| 83 | + User: 'default' |
| 84 | + Password: '123456' |
| 85 | + Port: 6379 |
| 86 | + |
| 87 | + |
| 88 | + # 修改默认dtm服务端口 |
| 89 | + HttpPort: 35789 |
| 90 | + GrpcPort: 35790 |
| 91 | + |
| 92 | + |
| 93 | + # 如果使用注册与发现(sponge的dtm驱动已支持etcd\consul\nacos),则需要配置如下(使用http协议) |
| 94 | + #MicroService: |
| 95 | + #Driver: 'dtm-driver-sponge' |
| 96 | + #Target: 'etcd://127.0.0.1:2379/dtmservice' |
| 97 | + #EndPoint: 'http://127.0.0.1:35789' |
| 98 | + ``` |
| 99 | + |
| 100 | +- 启动 DTM 服务: |
| 101 | + |
| 102 | + ```bash |
| 103 | + #dtm 服务端口 http: 35789, grpc: 35790 |
| 104 | + ./dtm -c conf.yml |
| 105 | + ``` |
| 106 | + |
| 107 | +<br> |
| 108 | + |
| 109 | +### 使用sponge快速构建电商系统 |
| 110 | + |
| 111 | +简易版电商系统由以下 8 个微服务组成: |
| 112 | + |
| 113 | +- **eshop_gw**:API 网关服务 |
| 114 | +- **user**:用户服务 |
| 115 | +- **product**:商品服务 |
| 116 | +- **order**:订单服务 |
| 117 | +- **stock**:库存服务 |
| 118 | +- **coupon**:优惠券服务 |
| 119 | +- **pay**:支付服务 |
| 120 | +- **flashSale**:秒杀抢购服务 |
| 121 | + |
| 122 | +<br> |
| 123 | + |
| 124 | +#### 1. 准备各个服务mysql数据库和表 |
| 125 | + |
| 126 | +将以下服务对应的数据库表导入 MySQL: |
| 127 | + |
| 128 | +- [user 服务的库和表](https://github.com/zhufuyi/sponge_examples/blob/main/_14_eshop/test/sql/user.sql) |
| 129 | +- [product 服务的库和表](https://github.com/zhufuyi/sponge_examples/blob/main/_14_eshop/test/sql/product.sql) |
| 130 | +- [order 服务的库和表](https://github.com/zhufuyi/sponge_examples/blob/main/_14_eshop/test/sql/order_record.sql) |
| 131 | +- [stock 服务的库和表](https://github.com/zhufuyi/sponge_examples/blob/main/_14_eshop/test/sql/stock.sql) |
| 132 | +- [coupon 服务的库和表](https://github.com/zhufuyi/sponge_examples/blob/main/_14_eshop/test/sql/coupon.sql) |
| 133 | +- [pay 服务的库和表](https://github.com/zhufuyi/sponge_examples/blob/main/_14_eshop/test/sql/pay.sql) |
| 134 | + |
| 135 | +<br> |
| 136 | + |
| 137 | +#### 2. 准备各个服务的protobuf文件 |
| 138 | + |
| 139 | +这些protobuf文件给sponge快速创建服务: |
| 140 | + |
| 141 | +- [user 服务的protobuf文件](https://github.com/zhufuyi/sponge_examples/tree/main/_14_eshop/test/protobuf/user.proto) |
| 142 | +- [product 服务的protobuf文件](https://github.com/zhufuyi/sponge_examples/tree/main/_14_eshop/test/protobuf/product.proto) |
| 143 | +- [order 服务的protobuf文件](https://github.com/zhufuyi/sponge_examples/tree/main/_14_eshop/test/protobuf/order.proto) |
| 144 | +- [stock 服务的protobuf文件](https://github.com/zhufuyi/sponge_examples/tree/main/_14_eshop/test/protobuf/stock.proto) |
| 145 | +- [coupon 服务的protobuf文件](https://github.com/zhufuyi/sponge_examples/tree/main/_14_eshop/test/protobuf/coupon.proto) |
| 146 | +- [pay 服务的protobuf文件](https://github.com/zhufuyi/sponge_examples/tree/main/_14_eshop/test/protobuf/pay.proto) |
| 147 | +- [flashSale 秒杀抢购服的protobuf文件](https://github.com/zhufuyi/sponge_examples/tree/main/_14_eshop/test/protobuf/flashSale.proto) |
| 148 | +- [eshop_gw api 网关服务的protobuf文件](https://github.com/zhufuyi/sponge_examples/tree/main/_14_eshop/test/protobuf/eshop_gw.proto) |
| 149 | + |
| 150 | +<br> |
| 151 | + |
| 152 | +#### 3. 基于protobuf生成gRPC+HTTP混合服务代码 |
| 153 | + |
| 154 | +打开sponge的UI页面,切换到菜单`Protobuf` --> `创建grpc+http服务`,填写参数,分别生成7个支持grpc+http混合服务代码user、product、order、stock、coupon、pay、flashSale,如下图所示: |
| 155 | + |
| 156 | + |
| 157 | + |
| 158 | +下载代码后,分别解压各个服务代码到eshop目录下。 |
| 159 | + |
| 160 | +> 注:在生成代码页面如果参数开启了大仓库类型,表示创建的服务适合微服务单仓库(mono-repo)模式。 |
| 161 | +
|
| 162 | +<br> |
| 163 | + |
| 164 | +#### 4. 基于mysql表生成CRUD代码 |
| 165 | + |
| 166 | +打开sponge的UI页面,切换到菜单`Public` --> `生成service+handler CRUD代码`,填写参数,分别生成user、product、order、stock、coupon、pay服务的CRUD代码,如下图所示: |
| 167 | + |
| 168 | + |
| 169 | + |
| 170 | +下载代码后,分别解压CRUD代码,把CRUD代码(api和internal两个目录)移动到对应服务代码中(如果提示proto文件重复,忽略即可)。 |
| 171 | + |
| 172 | +> 注:在生成代码页面如果参数开启了大仓库类型,表示创建的服务适合微服务单仓库(mono-repo)模式。 |
| 173 | +
|
| 174 | +<br> |
| 175 | + |
| 176 | +#### 5. 基于protobuf生成api网关服务代码 |
| 177 | + |
| 178 | +打开sponge的UI页面,切换到菜单`Protobuf` --> `创建grpc网关服务`,填写参数,生成eshop_gw的api网关服务代码,如下图所示: |
| 179 | + |
| 180 | + |
| 181 | + |
| 182 | +下载代码后,解压服务代码到eshop目录下。 |
| 183 | + |
| 184 | +为了让eshop_gw服务可以连接各个服务,需要生成连接代码。打开sponge的UI页面,切换到菜单`Public` --> `生成grpc连接代码`,填写参数,生成eshop_gw的连接各个grpc代码,如下图所示: |
| 185 | + |
| 186 | + |
| 187 | + |
| 188 | +下载代码后,解压代码,把连接代码(internal目录)移动到eshop_gw服务代码中。 |
| 189 | + |
| 190 | +> 注:在生成代码页面如果参数开启了大仓库类型,表示创建的服务适合微服务单仓库(mono-repo)模式。 |
| 191 | +
|
| 192 | +<br> |
| 193 | + |
| 194 | +#### 6. 填写业务逻辑代码 |
| 195 | + |
| 196 | +到此为止,服务框架已经基本搭建完毕。接下来分别在各服务的 `internal/service` 目录下编写实际的业务逻辑代码。 |
| 197 | + |
| 198 | +<br> |
| 199 | + |
| 200 | +#### 7. 启动服务 |
| 201 | + |
| 202 | +启动服务前先修改各个服务的配置文件,修改端口、数据库连接等信息。各个服务默认的http端口(8080),grpc端口(8282),因为在同一台主机本地(本地测试ip为192.168.3.90)运行,为了防止端口冲突,所以在配置已经修改了各个服务的端口(在`configs/xxx.yml`目录下和`api/xxx/v1/xxx.proto`中修改端口),下面是已修改的端口: |
| 203 | + |
| 204 | +| 服务 | 协议 | HTTP 端口 | gRPC 端口 | |
| 205 | +|------------------|-------------|------------|-----------| |
| 206 | +| eshop_gw | HTTP | 8080 | - | |
| 207 | +| user | HTTP, gRPC | 30080 | 30082 | |
| 208 | +| product | HTTP, gRPC | 30180 | 30182 | |
| 209 | +| order | HTTP, gRPC | 30280 | 30282 | |
| 210 | +| stock | HTTP, gRPC | 30380 | 30382 | |
| 211 | +| coupon | HTTP, gRPC | 30480 | 30482 | |
| 212 | +| pay | HTTP, gRPC | 30580 | 30582 | |
| 213 | +| flashSale | HTTP, gRPC | 30680 | 30682 | |
| 214 | + |
| 215 | +> 注:如果在容器或不同机器上运行,不需要修改默认的端口,只需修改映射端口。 |
| 216 | +
|
| 217 | +<br> |
| 218 | + |
| 219 | +### 测试与验证 |
| 220 | + |
| 221 | +#### 单个服务测试 |
| 222 | + |
| 223 | +各个服务启动成功之后,先验证单个服务是否正常运行,分别测试user、product、order、stock、coupon、pay、flashSale这7个服务的api。 |
| 224 | + |
| 225 | +在浏览器访问 `http://localhost:<服务端口>/apis/swagger/index.html`,验证各个服务的api是否正常,除了在swaggers页面测试api,也可以在各个服务下的internal/service/xxx_client_test.go 文件中填写参数后运行测试grpc api。 |
| 226 | + |
| 227 | +<br> |
| 228 | + |
| 229 | +#### 集成测试 |
| 230 | + |
| 231 | +各个服务测试正常后,通过eshop_gw的api网关服务测试整个系统是否正常运行。在浏览器访问api网关服务的swagger页面,[http://localhost:8080/apis/swagger/index.html](http://localhost:8080/apis/swagger/index.html),如下图所示: |
| 232 | + |
| 233 | + |
| 234 | + |
| 235 | +<br> |
| 236 | + |
| 237 | +**测试提交订单 API** |
| 238 | + |
| 239 | +- 提交订单使用了dtm的分布式事务模式saga,主要验证创建订单、扣减库存、创建支付订单、优惠券数据是否一致。 |
| 240 | + |
| 241 | +- 为了避免库存不足导致订单失败,测试前先设置库存,在swagger页面找到设置产品库存api,填写参数,例如产品id为1、库存为10 |
| 242 | + |
| 243 | + ```json |
| 244 | + { |
| 245 | + "productID": 1, |
| 246 | + "stock": 10 |
| 247 | + } |
| 248 | + ``` |
| 249 | + |
| 250 | +- 测试提交订单分别请求无缓冲队列和缓冲队列的api,在swagger页面找到对应的api,填写参数,例如用户id为1、产品id为1、产品数量为1、订单金额为100 |
| 251 | + |
| 252 | + ```json |
| 253 | + { |
| 254 | + "userID": 1, |
| 255 | + "productID": 1, |
| 256 | + "productCount": 1, |
| 257 | + "amount": 100, |
| 258 | + "couponID": 0 |
| 259 | + } |
| 260 | + ``` |
| 261 | + |
| 262 | +> 注:如果设置couponID不为0,表示使用优惠券,如果优惠券失效或过期,会导致订单失败。如果想使得订单成功的话,在swagger页面找到创建优惠券api创建新的优惠券,然后获取得到优惠券id,填写到提交订单api的couponID字段。 |
| 263 | +
|
| 264 | +<br> |
| 265 | + |
| 266 | +**测试秒杀抢购API** |
| 267 | + |
| 268 | +- 秒杀抢购使用了kafka的消息队列、dtm+redis的二阶段消息、dtm+mysql的saga两个分布式事务模式,主要验证秒杀抢购、扣减库存、创建订单、扣减库存、创建支付订单数据是否一致。 |
| 269 | + |
| 270 | +- 为了避免库存不足导致订单失败,测试前先设置库存,在swagger页面找到设置产品库存api,填写参数,例如产品id为1、库存为10 |
| 271 | + |
| 272 | + ```json |
| 273 | + { |
| 274 | + "productID": 1, |
| 275 | + "stock": 10 |
| 276 | + } |
| 277 | + ``` |
| 278 | + |
| 279 | +- 测试秒杀抢购api,验证数据是否一致,在swagger页面找到秒杀抢购api,填写参数,例如用户id为1、产品id为1 |
| 280 | + |
| 281 | + ```json |
| 282 | + { |
| 283 | + "userID": 1, |
| 284 | + "productID": 1, |
| 285 | + "amount": 100 |
| 286 | + } |
| 287 | + ``` |
| 288 | + |
| 289 | +<br> |
| 290 | + |
| 291 | +### 压力测试 |
| 292 | + |
| 293 | +这里使用压力测试工具[k6](https://github.com/grafana/k6)对eshop_gw的api网关服务进行压力测试,验证系统在高并发场景下的性能。压测前先设置足够的库存数量,否则会导致订单失败。 |
| 294 | + |
| 295 | +1. 压测提交订单 API的场景,使用k6的脚本 [submitOrder.js](https://github.com/zhufuyi/sponge_examples/blob/main/_14_eshop/test/submitOrder.js) ,运行命令如下: |
| 296 | + |
| 297 | + ```bash |
| 298 | + # 1000个虚拟用户,持续10秒 |
| 299 | + k6 run --vus 1000 --duration 10s test/k6/submitOrder.js |
| 300 | + |
| 301 | + # 或者,指定虚拟用户数和请求迭代次数,例如1000个虚拟用户共同完成迭代请求100000次 |
| 302 | + k6 run -u 1000 -i 100000 submit_order.js |
| 303 | + ``` |
| 304 | + |
| 305 | +2. 压测秒杀抢购 API的场景,使用k6的脚本 [flashSale.js](https://github.com/zhufuyi/sponge_examples/blob/main/_14_eshop/test/flashSale.js) ,运行命令如下: |
| 306 | + |
| 307 | + ```bash |
| 308 | + # 10000个虚拟用户,持续1秒 |
| 309 | + k6 run --vus 10000 --duration 1s test/k6/flashSale.js |
| 310 | + ``` |
| 311 | + |
| 312 | +> 注:压测结果与机器配置、网络环境、数据库配置等因素有关,请根据实际情况进行调整。 |
| 313 | + |
| 314 | +<br> |
0 commit comments