Skip to content

Latest commit

ย 

History

History
408 lines (324 loc) ยท 18.1 KB

File metadata and controls

408 lines (324 loc) ยท 18.1 KB

TinyFlow - ้ซ˜ๆ€ง่ƒฝ็Ÿญ้“พๆŽฅๆœๅŠก

ไธ€ไธช้ขๅ‘้ซ˜ๅนถๅ‘ๅœบๆ™ฏ็š„็Ÿญ้“พๆŽฅ็”ŸๆˆไธŽ็ฎก็†ๅนณๅฐ๏ผŒๆ”ฏๆŒ้•ฟ้“พๆŽฅๅŽ‹็ผฉไธบ6ไฝ็Ÿญ็ ๆˆ–่‡ชๅฎšไน‰ๅˆซๅ๏ผŒๆไพ›้“พๆŽฅๆ”ถ่—ๅคนไธŽๆ•ฐๆฎ็ปŸ่ฎกๅˆ†ๆžๅŠŸ่ƒฝ๏ผŒไพฟไบŽ็”จๆˆท้›†ไธญ็ฎก็†ๅธธ็”จ็ฝ‘ๅ€ใ€‚

ๆŠ€ๆœฏๆ ˆ

ๅŽ็ซฏ๏ผšSpring Boot 3ใ€MySQL 8ใ€Redisใ€RabbitMQใ€Caffeineใ€Resilience4jใ€Micrometer + Prometheus

ๅ‰็ซฏ๏ผšVue 3ใ€Viteใ€Tailwind CSS

่ฟ็ปด๏ผšDocker Composeใ€Grafanaใ€K6 ๅŽ‹ๆต‹


็›ฎๅฝ•


ๆ ธๅฟƒ็‰นๆ€ง

็‰นๆ€ง ๆ่ฟฐ
็Ÿญ็ ็”Ÿๆˆ ๆ”ฏๆŒ6ไฝ็Ÿญ็ ่‡ชๅŠจ็”Ÿๆˆ๏ผˆHashids็ผ–็ ๏ผ‰ๆˆ–็”จๆˆท่‡ชๅฎšไน‰ๅˆซๅ
ไธ‰็บง็ผ“ๅญ˜ Caffeine๏ผˆL1๏ผ‰โ†’ Redis๏ผˆL2๏ผ‰โ†’ MySQL๏ผˆL3๏ผ‰๏ผŒ็ƒญ็‚น้“พๆŽฅๆฏซ็ง’็บงๅ“ๅบ”
็ผ“ๅญ˜้˜ฒๆŠค ๅธƒ้š†่ฟ‡ๆปคๅ™จๆ‹ฆๆˆชๆ— ๆ•ˆ็Ÿญ็ ่ฏทๆฑ‚๏ผŒ้˜ฒๆญข็ผ“ๅญ˜็ฉฟ้€
ๅˆ†ๅธƒๅผID ๅ‚่€ƒ็พŽๅ›ขLeafๅทๆฎตๆจกๅผ๏ผŒๅŒBufferๅผ‚ๆญฅ้ข„ๅŠ ่ฝฝ๏ผŒ้ฟๅ…IDๆฎตๅˆ‡ๆข้˜ปๅกž
ๅผ‚ๆญฅ็ปŸ่ฎก ่ทณ่ฝฌๆˆๅŠŸๅŽ็ซ‹ๅณ่ฟ”ๅ›ž302๏ผŒ็‚นๅ‡ปๆ—ฅๅฟ—้€š่ฟ‡RabbitMQๅผ‚ๆญฅๅ†™ๅ…ฅ๏ผŒๆ ธๅฟƒ้“พ่ทฏไธ้˜ปๅกž
ๆต้‡้˜ฒๆŠค Resilience4jๅฎž็Žฐ้™ๆต๏ผˆๆป‘ๅŠจ็ช—ๅฃ3500 QPS๏ผ‰+ Redis/MySQL็‹ฌ็ซ‹็†”ๆ–ญๅ™จ
ๅฏ่ง‚ๆต‹ๆ€ง PrometheusๆŒ‡ๆ ‡ๆšด้œฒ + Grafanaๅฏ่ง†ๅŒ– + ็ป“ๆž„ๅŒ–ๆ—ฅๅฟ—
ๆ•ฐๆฎ็ปŸ่ฎก ็‚นๅ‡ป่ถ‹ๅŠฟใ€ๆฅๆบๅˆ†ๅธƒใ€่ฎพๅค‡ๅ ๆฏ”ใ€ๅœฐๅŸŸๅˆ†ๆž๏ผŒๆ”ฏๆŒCSV/JSONๅฏผๅ‡บ

็ณป็ปŸๆžถๆž„

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                              Client Request                                  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                      โ”‚
                                      โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                         Spring Boot Application                              โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚  โ”‚ Rate Limiterโ”‚โ”€โ”€โ”€โ–ถโ”‚                   Controller Layer                   โ”‚ โ”‚
โ”‚  โ”‚  (3500 QPS) โ”‚    โ”‚         ShortUrlController / StatsController         โ”‚ โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ”‚                                      โ”‚                                       โ”‚
โ”‚                                      โ–ผ                                       โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚  โ”‚                           Service Layer                                  โ”‚ โ”‚
โ”‚  โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚ โ”‚
โ”‚  โ”‚  โ”‚  ShortUrlService  โ”‚  โ”‚ SegmentIdGeneratorโ”‚  โ”‚ ClickRecorderServiceโ”‚  โ”‚ โ”‚
โ”‚  โ”‚  โ”‚  (Core Business)  โ”‚  โ”‚   (Leaf-Style ID) โ”‚  โ”‚  (Async Statistics) โ”‚  โ”‚ โ”‚
โ”‚  โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚ โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ”‚                                      โ”‚                                       โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                       โ”‚
          โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
          โ”‚                            โ”‚                            โ”‚
          โ–ผ                            โ–ผ                            โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”          โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚   L1: Caffeine  โ”‚        โ”‚   L2: Redis     โ”‚          โ”‚   L3: MySQL     โ”‚
โ”‚   (Local Cache) โ”‚โ—€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถโ”‚ (Distributed)   โ”‚โ—€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถโ”‚  (Persistence)  โ”‚
โ”‚   50,000 entriesโ”‚        โ”‚ + BloomFilter   โ”‚          โ”‚                 โ”‚
โ”‚   TTL: 30min    โ”‚        โ”‚ + CircuitBreakerโ”‚          โ”‚ + CircuitBreakerโ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜          โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                   โ”‚
                                   โ–ผ
                          โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                          โ”‚    RabbitMQ     โ”‚
                          โ”‚ (Click Events)  โ”‚
                          โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                   โ”‚
                                   โ–ผ
                          โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                          โ”‚ Click Analytics โ”‚
                          โ”‚  (Batch Write)  โ”‚
                          โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

ๆŠ€ๆœฏไบฎ็‚น่ฏฆ่งฃ

1. ไธ‰็บง็ผ“ๅญ˜ๆžถๆž„ไธŽ้˜ฒ็ฉฟ้€็ญ–็•ฅ

้—ฎ้ข˜่ƒŒๆ™ฏ๏ผš็Ÿญ้“พ่ทณ่ฝฌๆ˜ฏๅ…ธๅž‹็š„่ฏปๅคšๅ†™ๅฐ‘ๅœบๆ™ฏ๏ผŒ็ƒญ็‚น้“พๆŽฅๅฏ่ƒฝๆ‰ฟๅ—ๆž้ซ˜QPSใ€‚ๅŒๆ—ถ้œ€่ฆ้˜ฒๆญขๆถๆ„่ฏทๆฑ‚ไธๅญ˜ๅœจ็š„็Ÿญ็ ๅฏผ่‡ด็ผ“ๅญ˜็ฉฟ้€ๆ‰“็ฉฟๆ•ฐๆฎๅบ“ใ€‚

่งฃๅ†ณๆ–นๆกˆ๏ผš

่ฏทๆฑ‚ โ†’ BloomFilterๅˆคๆ–ญ โ†’ L1 Caffeine โ†’ L2 Redis โ†’ L3 MySQL
         โ†“ ไธๅญ˜ๅœจ                โ†“ ๆœชๅ‘ฝไธญ      โ†“ ๆœชๅ‘ฝไธญ     โ†“ ๆŸฅ่ฏข
      ๅฟซ้€Ÿ่ฟ”ๅ›ž404           ๆŸฅL2ๅนถๅ›žๅกซL1   ๆŸฅL3ๅนถๅ›žๅกซL1/L2  ่ฟ”ๅ›ž็ป“ๆžœ
  • L1 ๆœฌๅœฐ็ผ“ๅญ˜๏ผˆCaffeine๏ผ‰๏ผšๅ•ๆœบ50,000ๆก็ƒญ็‚น็Ÿญ้“พ๏ผŒ30ๅˆ†้’Ÿ่ฟ‡ๆœŸ๏ผŒๅ‘ฝไธญ็އ>95%ๆ—ถ่ทณ่ฟ‡็ฝ‘็ปœๅผ€้”€
  • L2 ๅˆ†ๅธƒๅผ็ผ“ๅญ˜๏ผˆRedis๏ผ‰๏ผšๅ…จ้‡็Ÿญ้“พ็ผ“ๅญ˜๏ผŒๆ”ฏๆŒ้›†็พค้ƒจ็ฝฒๆ•ฐๆฎๅ…ฑไบซ๏ผŒ24ๅฐๆ—ถTTL
  • L3 ๆŒไน…ๅญ˜ๅ‚จ๏ผˆMySQL๏ผ‰๏ผšๅ…œๅบ•ๆ•ฐๆฎๆบ๏ผŒๅ›žๆบๆ—ถ่‡ชๅŠจๅ›žๅกซL1/L2
  • ๅธƒ้š†่ฟ‡ๆปคๅ™จ๏ผšๅŸบไบŽRedisson็š„ๅˆ†ๅธƒๅผๅธƒ้š†่ฟ‡ๆปคๅ™จ๏ผŒ้ข„ๆœŸ1000ไธ‡็Ÿญ็ ๏ผŒ1%่ฏฏๅˆค็އ๏ผŒๆ‹ฆๆˆชๆ— ๆ•ˆ่ฏทๆฑ‚

็ผ“ๅญ˜ไธ€่‡ดๆ€ง็ญ–็•ฅ๏ผš

  • ๅ†™ๆ“ไฝœ๏ผšๅ…ˆๆ›ดๆ–ฐDB โ†’ ๅˆ ้™คRedis็ผ“ๅญ˜ โ†’ ๅคฑๆ•ˆๆœฌๅœฐ็ผ“ๅญ˜๏ผˆCache Asideๆจกๅผ๏ผ‰
  • ็ƒญ็‚น้ข„็ƒญ๏ผšๅบ”็”จๅฏๅŠจๆ—ถๅŠ ่ฝฝTop 5000็ƒญ็‚น็Ÿญ้“พๅˆฐL1๏ผŒๅ‡ๅฐ‘ๅ†ทๅฏๅŠจๅปถ่ฟŸ

2. ้ซ˜ๅนถๅ‘ๅˆ†ๅธƒๅผID็”Ÿๆˆ

้—ฎ้ข˜่ƒŒๆ™ฏ๏ผšไผ ็ปŸ่‡ชๅขžIDๅœจๅˆ†ๅธƒๅผ้ƒจ็ฝฒไธ‹ๅญ˜ๅœจ้”็ซžไบ‰ๅ’Œ็Ÿญ็ ๅฏ้ๅކ้—ฎ้ข˜๏ผ›UUIDๅคช้•ฟไธ”ๆ— ๅบใ€‚

่งฃๅ†ณๆ–นๆกˆ๏ผšๅ‚่€ƒ็พŽๅ›ขLeafๅทๆฎตๆจกๅผ่ฎพ่ฎก

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                    SegmentIdGenerator                          โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚
โ”‚  โ”‚              Buffer (Per bizTag)                        โ”‚   โ”‚
โ”‚  โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”      โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”              โ”‚   โ”‚
โ”‚  โ”‚   โ”‚ currentRange  โ”‚      โ”‚  nextRange    โ”‚              โ”‚   โ”‚
โ”‚  โ”‚   โ”‚ [10001-110000]โ”‚      โ”‚[110001-210000]โ”‚ โ† ๅผ‚ๆญฅ้ข„ๅŠ ่ฝฝ โ”‚   โ”‚
โ”‚  โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜      โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜              โ”‚   โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                            โ”‚
                            โ–ผ
                    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                    โ”‚   id_segment  โ”‚  (MySQL)
                    โ”‚ biz_tag | max_id | step
                    โ”‚ shorturl| 210000 | 100000
                    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

ๆ ธๅฟƒๆœบๅˆถ๏ผš

  • ๅทๆฎต้ข„ๅ–๏ผšๆฏๆฌกไปŽDB็”ณ่ฏท10ไธ‡ไธชID๏ผŒๅ‡ๅฐ‘DB่ฎฟ้—ฎ้ข‘็އ
  • ๅŒBufferๅผ‚ๆญฅๅŠ ่ฝฝ๏ผšๅฝ“currentRangeไฝฟ็”จ็އ>70%ๆ—ถ๏ผŒๅผ‚ๆญฅ็บฟ็จ‹ๆๅ‰ๅŠ ่ฝฝnextRange
  • ๆ•ฐๆฎๅบ“่กŒ้”๏ผšUPDATE id_segment SET max_id = max_id + step WHERE biz_tag = ?๏ผŒๅŽŸๅญๆ“ไฝœไฟ่ฏๅคšๅฎžไพ‹ไธๅ†ฒ็ช
  • Hashids็ผ–็ ๏ผšๅฐ†้•ฟๆ•ดๅž‹ID็ผ–็ ไธบ6ไฝ็Ÿญๅญ—็ฌฆไธฒ๏ผˆa-zA-Z0-9๏ผ‰๏ผŒๆ”ฏๆŒ่‡ชๅฎšไน‰็›ๅ€ผ้˜ฒๆญขๆžšไธพ

ๆ€ง่ƒฝ๏ผšๆœฌๅœฐๅทๆฎตๆจกๅผ๏ผŒID่Žทๅ–O(1)ๆ—ถ้—ดๅคๆ‚ๅบฆ๏ผŒๅ•ๆœบๆ”ฏๆ’‘็™พไธ‡็บงQPSๆ— ๅŽ‹ๅŠ›

3. ๅผ‚ๆญฅ็ปŸ่ฎกไธŽๆถˆๆฏ่งฃ่€ฆ

้—ฎ้ข˜่ƒŒๆ™ฏ๏ผš่ทณ่ฝฌๆŽฅๅฃ้œ€่ฆ่ฎฐๅฝ•็‚นๅ‡ปๆ—ฅๅฟ—๏ผˆIPใ€UAใ€ๆฅๆบใ€่ฎพๅค‡็ฑปๅž‹็ญ‰๏ผ‰๏ผŒๅŒๆญฅๅ†™ๅ…ฅไผšไธฅ้‡ๆ‹–ๆ…ขๅ“ๅบ”ๆ—ถ้—ดใ€‚

่งฃๅ†ณๆ–นๆกˆ๏ผš่ทณ่ฝฌไธŽ็ปŸ่ฎก้“พ่ทฏๅˆ†็ฆป

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     302่ทณ่ฝฌ      โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚   ็”จๆˆท่ฏทๆฑ‚   โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถ  โ”‚    ๅฎขๆˆท็ซฏ    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
       โ”‚
       โ”‚  ๅผ‚ๆญฅๅ‘้€MQๆถˆๆฏ
       โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    ๆถˆ่ดน&ๆ‰น้‡ๅ†™ๅ…ฅ   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚   RabbitMQ   โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถ โ”‚ ClickEvent่กจ โ”‚
โ”‚ (click.queue)โ”‚                   โ”‚   (MySQL)    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

ๆŠ€ๆœฏๅฎž็Žฐ๏ผš

  • ๅณๆ—ถๅ“ๅบ”๏ผš่ทณ่ฝฌๆŽฅๅฃ่ฟ”ๅ›ž302ๅŽ๏ผŒๅผ‚ๆญฅๅ‘้€็‚นๅ‡ปไบ‹ไปถๅˆฐRabbitMQ
  • ๆถˆๆฏๆ ผๅผ๏ผš{shortCode, ip, ua, referer, deviceType, timestamp}
  • ๆ‰น้‡ๆถˆ่ดน๏ผšๆถˆ่ดน่€…ๆฏ2็ง’ๆ‰น้‡ๆ‹‰ๅ–ๆถˆๆฏ๏ผŒ่šๅˆๅŽๆ‰น้‡INSERT๏ผˆๅ‡ๅฐ‘DBๅŽ‹ๅŠ›๏ผ‰
  • ้‡‡ๆ ทๆŽงๅˆถ๏ผš้ซ˜ๆต้‡ๅœบๆ™ฏๅฏ้…็ฝฎ้‡‡ๆ ท็އ๏ผˆๅฆ‚10%๏ผ‰๏ผŒๅนณ่กก็ปŸ่ฎก็ฒพๅบฆไธŽๅญ˜ๅ‚จๆˆๆœฌ
  • ๆญปไฟก้˜Ÿๅˆ—๏ผšๆถˆ่ดนๅคฑ่ดฅ็š„ๆถˆๆฏ่ฟ›ๅ…ฅDLQ๏ผŒๆ”ฏๆŒไบบๅทฅๆŽ’ๆŸฅไธŽ้‡่ฏ•

ๆ•ˆๆžœ๏ผš่ทณ่ฝฌๆŽฅๅฃP99ๅปถ่ฟŸ้™ไฝŽ60%+๏ผŒ็ปŸ่ฎกไธŽๆ ธๅฟƒ้“พ่ทฏๅฎŒๅ…จ่งฃ่€ฆ

4. ็†”ๆ–ญ้™ๆตไธŽๆœๅŠก้™็บง

้—ฎ้ข˜่ƒŒๆ™ฏ๏ผšRedis/MySQLๆ•…้šœๆ—ถ้œ€่ฆๅฟซ้€Ÿๅคฑ่ดฅ๏ผŒ้ฟๅ…้›ชๅดฉ๏ผ›็ชๅ‘ๆต้‡้œ€่ฆ้™ๆตไฟๆŠคใ€‚

่งฃๅ†ณๆ–นๆกˆ๏ผšๅŸบไบŽResilience4j็š„ๅคš็ปดๅบฆ้˜ฒๆŠค

resilience4j:
  ratelimiter:
    instances:
      redirectLimit:
        limitForPeriod: 3500      # ๆฏๅ‘จๆœŸๅ…่ฎธ่ฏทๆฑ‚ๆ•ฐ
        limitRefreshPeriod: 1s    # ๆป‘ๅŠจ็ช—ๅฃๅ‘จๆœŸ
        timeoutDuration: 0        # ่ถ…้™็ซ‹ๅณๆ‹’็ป

  circuitbreaker:
    instances:
      redisBreaker:
        slidingWindowSize: 10          # ๆป‘ๅŠจ็ช—ๅฃๅคงๅฐ
        failureRateThreshold: 50       # ๅคฑ่ดฅ็އ้˜ˆๅ€ผ50%
        waitDurationInOpenState: 5s    # ็†”ๆ–ญๅŽ็ญ‰ๅพ…ๆ—ถ้—ด
        permittedNumberOfCallsInHalfOpenState: 3  # ๅŠๅผ€็Šถๆ€ๆŽขๆต‹่ฏทๆฑ‚ๆ•ฐ
        
      mysqlBreaker:
        slidingWindowSize: 10
        failureRateThreshold: 50
        waitDurationInOpenState: 10s

้™็บง็ญ–็•ฅ๏ผš

ๆ•…้šœๅœบๆ™ฏ ้™็บง่กŒไธบ
Redis็†”ๆ–ญ ่ทณ่ฟ‡L2๏ผŒ็›ดๆŽฅๆŸฅ่ฏขL3 MySQL
MySQL็†”ๆ–ญ ่ฟ”ๅ›žL1/L2็ผ“ๅญ˜ๆ•ฐๆฎ๏ผŒๆ— ็ผ“ๅญ˜ๅˆ™่ฟ”ๅ›ž503
้™ๆต่งฆๅ‘ ่ฟ”ๅ›ž429 Too Many Requests
RabbitMQๆ•…้šœ ๆœฌๅœฐๅ†…ๅญ˜้˜Ÿๅˆ—ๆš‚ๅญ˜๏ผŒๅฎšๆ—ถ้‡่ฏ•

ๆ€ง่ƒฝๆŒ‡ๆ ‡

ๅŸบไบŽK6ๅŽ‹ๆต‹๏ผŒๅŒๅฎžไพ‹้ƒจ็ฝฒ๏ผˆ4C8G ร— 2๏ผ‰๏ผŒMySQL 8.0๏ผŒRedis 7.0

ๆŽฅๅฃ QPS P50 P95 P99 ้”™่ฏฏ็އ
่ทณ่ฝฌ๏ผˆ็ผ“ๅญ˜ๅ‘ฝไธญ๏ผ‰ 5,200+ 12ms 28ms 45ms <0.01%
่ทณ่ฝฌ๏ผˆ็ผ“ๅญ˜็ฉฟ้€๏ผ‰ 2,800+ 35ms 85ms 120ms <0.1%
็Ÿญ้“พๅˆ›ๅปบ 1,500+ 25ms 65ms 95ms <0.05%

่ฐƒไผ˜ๅ…ณ้”ฎ็‚น๏ผš

  1. HikariCP่ฟžๆŽฅๆฑ ๏ผšmaximumPoolSize=100๏ผŒconnectionTimeout=3s
  2. Caffeineๆœฌๅœฐ็ผ“ๅญ˜50Kๆก็›ฎ๏ผŒๅ‘ฝไธญ็އ>92%
  3. Redis Lettuce่ฟžๆŽฅๆฑ ๏ผšmaxActive=600๏ผŒmaxIdle=200
  4. Tomcat็บฟ็จ‹ๆฑ ๏ผšmaxThreads=600๏ผŒacceptCount=2000

้กน็›ฎ็ป“ๆž„

TinyFlow/
โ”œโ”€โ”€ src/main/java/com/layor/tinyflow/
โ”‚   โ”œโ”€โ”€ config/                    # ้…็ฝฎ็ฑป
โ”‚   โ”‚   โ”œโ”€โ”€ CacheConfig.java       # Caffeine + Redis็ผ“ๅญ˜้…็ฝฎ
โ”‚   โ”‚   โ”œโ”€โ”€ RabbitConfig.java      # RabbitMQ้˜Ÿๅˆ—้…็ฝฎ
โ”‚   โ”‚   โ”œโ”€โ”€ BloomFilterConfig.java # ๅธƒ้š†่ฟ‡ๆปคๅ™จ้…็ฝฎ
โ”‚   โ”‚   โ””โ”€โ”€ HashidsConfig.java     # ็Ÿญ็ ็ผ–็ ้…็ฝฎ
โ”‚   โ”œโ”€โ”€ controller/
โ”‚   โ”‚   โ”œโ”€โ”€ ShortUrlController.java    # ็Ÿญ้“พCRUD + ่ทณ่ฝฌ
โ”‚   โ”‚   โ””โ”€โ”€ StatsController.java       # ็ปŸ่ฎกๆŸฅ่ฏข
โ”‚   โ”œโ”€โ”€ service/
โ”‚   โ”‚   โ”œโ”€โ”€ ShortUrlService.java       # ๆ ธๅฟƒไธšๅŠก้€ป่พ‘
โ”‚   โ”‚   โ”œโ”€โ”€ SegmentIdGenerator.java    # ๅทๆฎตๆจกๅผID็”Ÿๆˆๅ™จ
โ”‚   โ”‚   โ”œโ”€โ”€ ClickRecorderService.java  # ็‚นๅ‡ป็ปŸ่ฎกๆœๅŠก
โ”‚   โ”‚   โ””โ”€โ”€ ClickEventConsumer.java    # MQๆถˆ่ดน่€…
โ”‚   โ”œโ”€โ”€ strategy/
โ”‚   โ”‚   โ””โ”€โ”€ HashidsStrategy.java       # ็Ÿญ็ ็ผ–็ ็ญ–็•ฅ
โ”‚   โ”œโ”€โ”€ entity/                    # JPAๅฎžไฝ“
โ”‚   โ”œโ”€โ”€ dto/                       # ๆ•ฐๆฎไผ ่พ“ๅฏน่ฑก
โ”‚   โ””โ”€โ”€ repository/                # ๆ•ฐๆฎ่ฎฟ้—ฎๅฑ‚
โ”œโ”€โ”€ src/main/resources/
โ”‚   โ””โ”€โ”€ application.yml            # ๅบ”็”จ้…็ฝฎ
โ”œโ”€โ”€ web/                           # Vue 3 ๅ‰็ซฏ
โ”‚   โ”œโ”€โ”€ src/
โ”‚   โ”‚   โ”œโ”€โ”€ components/            # ้€š็”จ็ป„ไปถ
โ”‚   โ”‚   โ”œโ”€โ”€ pages/                 # ้กต้ข
โ”‚   โ”‚   โ””โ”€โ”€ composables/           # ็ป„ๅˆๅผAPI
โ”‚   โ””โ”€โ”€ infra/
โ”‚       โ”œโ”€โ”€ load/k6/               # K6ๅŽ‹ๆต‹่„šๆœฌ
โ”‚       โ””โ”€โ”€ observability/         # Prometheus + Grafana้…็ฝฎ
โ”œโ”€โ”€ docker-compose.yml             # ๆœฌๅœฐๅผ€ๅ‘็Žฏๅขƒ
โ””โ”€โ”€ pom.xml

ๅฟซ้€Ÿๅผ€ๅง‹

็Žฏๅขƒ่ฆๆฑ‚

  • JDK 17+
  • MySQL 8.0+
  • Redis 7.0+
  • RabbitMQ 3.12+๏ผˆๅฏ้€‰๏ผŒไธ้…็ฝฎๅˆ™ไฝฟ็”จๆœฌๅœฐๅผ‚ๆญฅๆจกๅผ๏ผ‰
  • Node.js 18+๏ผˆๅ‰็ซฏ๏ผ‰

1. ๅฏๅŠจๅŸบ็ก€่ฎพๆ–ฝ

docker compose up -d mysql redis rabbitmq

2. ๅˆๅง‹ๅŒ–ๆ•ฐๆฎๅบ“

CREATE DATABASE `tiny_flow` DEFAULT CHARACTER SET utf8mb4;

-- ๅทๆฎต่กจ
CREATE TABLE `id_segment` (
  `biz_tag` VARCHAR(64) PRIMARY KEY,
  `max_id` BIGINT NOT NULL DEFAULT 1,
  `step` INT NOT NULL DEFAULT 100000,
  `version` INT NOT NULL DEFAULT 0,
  `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
  `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

INSERT INTO `id_segment` (`biz_tag`, `max_id`, `step`) VALUES ('shorturl', 1, 100000);

3. ๅฏๅŠจๅŽ็ซฏ

cd TinyFlow
mvn spring-boot:run

4. ๅฏๅŠจๅ‰็ซฏ

cd web
npm install
npm run dev

่ฎฟ้—ฎ http://localhost:5173


้…็ฝฎ่ฏดๆ˜Ž

ๆ ธๅฟƒ้…็ฝฎ้กน๏ผˆapplication.yml๏ผ‰๏ผš

# ็ผ“ๅญ˜้…็ฝฎ
cache:
  caffeine:
    spec: maximumSize=50000,expireAfterWrite=30m,recordStats
  warmup:
    enabled: true
    size: 5000  # ๅฏๅŠจๆ—ถ้ข„็ƒญTop N็ƒญ็‚น้“พๆŽฅ

# ๅธƒ้š†่ฟ‡ๆปคๅ™จ
bloom:
  expected-insertions: 10000000  # ้ข„ๆœŸๆ•ฐๆฎ้‡
  false-positive-rate: 0.01     # ่ฏฏๅˆค็އ

# ็‚นๅ‡ป็ปŸ่ฎก
clicks:
  mode: rabbitmq  # ๅฏ้€‰: local๏ผˆๆœฌๅœฐๅผ‚ๆญฅ๏ผ‰, rabbitmq๏ผˆๆถˆๆฏ้˜Ÿๅˆ—๏ผ‰
events:
  sampleRate: 1.0  # ้‡‡ๆ ท็އ๏ผŒ1.0=100%่ฎฐๅฝ•

# ็†”ๆ–ญ้™ๆต
resilience4j:
  ratelimiter:
    instances:
      redirectLimit:
        limitForPeriod: 3500
        limitRefreshPeriod: 1s

ๅŽ‹ๆต‹ไธŽ็›‘ๆŽง

่ฟ่กŒๅŽ‹ๆต‹

# ๅฏๅŠจ่ง‚ๆต‹ๆ ˆ
docker compose -f web/infra/observability/docker-compose.yml up -d

# ่ฟ่กŒK6ๅŽ‹ๆต‹
k6 run web/infra/load/k6/shortener.js

Grafana Dashboard

ๅฏผๅ…ฅ web/infra/observability/dashboards/shortener-overview.json๏ผŒๅฏ่ง†ๅŒ–๏ผš

  • ่ทณ่ฝฌๆŽฅๅฃ QPS / P95 / P99
  • ็ผ“ๅญ˜ๅ‘ฝไธญ็އ๏ผˆL1/L2๏ผ‰
  • ็†”ๆ–ญๅ™จ็Šถๆ€
  • RabbitMQ้˜Ÿๅˆ—ๆทฑๅบฆ

API ๆ–‡ๆกฃ

Method Endpoint ๆ่ฟฐ
POST /api/shorten ๅˆ›ๅปบ็Ÿญ้“พ๏ผˆๆ”ฏๆŒ่‡ชๅฎšไน‰ๅˆซๅ๏ผ‰
GET /{shortCode} ็Ÿญ้“พ่ทณ่ฝฌ๏ผˆ302้‡ๅฎšๅ‘๏ผ‰
GET /api/urls ๅˆ†้กตๆŸฅ่ฏข็Ÿญ้“พๅˆ—่กจ
PUT /api/{shortCode} ๆ›ดๆ–ฐ็Ÿญ้“พๅˆซๅ
DELETE /api/{shortCode} ๅˆ ้™ค็Ÿญ้“พ
GET /api/stats/{shortCode}/overview ็ปŸ่ฎกๆฆ‚่งˆ
GET /api/stats/{shortCode}/trend ็‚นๅ‡ป่ถ‹ๅŠฟ
GET /api/stats/{shortCode}/distribution ๆฅๆบ/่ฎพๅค‡ๅˆ†ๅธƒ

่ฎธๅฏ่ฏ

MIT License