ไธไธช้ขๅ้ซๅนถๅๅบๆฏ็็ญ้พๆฅ็ๆไธ็ฎก็ๅนณๅฐ๏ผๆฏๆ้ฟ้พๆฅๅ็ผฉไธบ6ไฝ็ญ็ ๆ่ชๅฎไนๅซๅ๏ผๆไพ้พๆฅๆถ่ๅคนไธๆฐๆฎ็ป่ฎกๅๆๅ่ฝ๏ผไพฟไบ็จๆท้ไธญ็ฎก็ๅธธ็จ็ฝๅใ
ๅ็ซฏ๏ผSpring Boot 3ใMySQL 8ใRedisใRabbitMQใCaffeineใResilience4jใMicrometer + Prometheus
ๅ็ซฏ๏ผVue 3ใViteใTailwind CSS
่ฟ็ปด๏ผDocker ComposeใGrafanaใK6 ๅๆต
- ๆ ธๅฟ็นๆง
- ็ณป็ปๆถๆ
- ๆๆฏไบฎ็น่ฏฆ่งฃ
- ๆง่ฝๆๆ
- ้กน็ฎ็ปๆ
- ๅฟซ้ๅผๅง
- ้ ็ฝฎ่ฏดๆ
- ๅๆตไธ็ๆง
- API ๆๆกฃ
- ่ฎธๅฏ่ฏ
| ็นๆง | ๆ่ฟฐ |
|---|---|
| ็ญ็ ็ๆ | ๆฏๆ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) โ
โโโโโโโโโโโโโโโโโโโ
้ฎ้ข่ๆฏ๏ผ็ญ้พ่ทณ่ฝฌๆฏๅ ธๅ็่ฏปๅคๅๅฐๅบๆฏ๏ผ็ญ็น้พๆฅๅฏ่ฝๆฟๅๆ้ซ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๏ผๅๅฐๅทๅฏๅจๅปถ่ฟ
้ฎ้ข่ๆฏ๏ผไผ ็ป่ชๅข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ๆ ๅๅ
้ฎ้ข่ๆฏ๏ผ่ทณ่ฝฌๆฅๅฃ้่ฆ่ฎฐๅฝ็นๅปๆฅๅฟ๏ผIPใUAใๆฅๆบใ่ฎพๅค็ฑปๅ็ญ๏ผ๏ผๅๆญฅๅๅ ฅไผไธฅ้ๆๆ ขๅๅบๆถ้ดใ
่งฃๅณๆนๆก๏ผ่ทณ่ฝฌไธ็ป่ฎก้พ่ทฏๅ็ฆป
โโโโโโโโโโโโโโโโ 302่ทณ่ฝฌ โโโโโโโโโโโโโโโโ
โ ็จๆท่ฏทๆฑ โ โโโโโโโโโโโโโโโถ โ ๅฎขๆท็ซฏ โ
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ
โ
โ ๅผๆญฅๅ้MQๆถๆฏ
โผ
โโโโโโโโโโโโโโโโ ๆถ่ดน&ๆน้ๅๅ
ฅ โโโโโโโโโโโโโโโโ
โ RabbitMQ โ โโโโโโโโโโโโโโโโโถ โ ClickEvent่กจ โ
โ (click.queue)โ โ (MySQL) โ
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ
ๆๆฏๅฎ็ฐ๏ผ
- ๅณๆถๅๅบ๏ผ่ทณ่ฝฌๆฅๅฃ่ฟๅ302ๅ๏ผๅผๆญฅๅ้็นๅปไบไปถๅฐRabbitMQ
- ๆถๆฏๆ ผๅผ๏ผ
{shortCode, ip, ua, referer, deviceType, timestamp} - ๆน้ๆถ่ดน๏ผๆถ่ดน่ ๆฏ2็งๆน้ๆๅๆถๆฏ๏ผ่ๅๅๆน้INSERT๏ผๅๅฐDBๅๅ๏ผ
- ้ๆ ทๆงๅถ๏ผ้ซๆต้ๅบๆฏๅฏ้ ็ฝฎ้ๆ ท็๏ผๅฆ10%๏ผ๏ผๅนณ่กก็ป่ฎก็ฒพๅบฆไธๅญๅจๆๆฌ
- ๆญปไฟก้ๅ๏ผๆถ่ดนๅคฑ่ดฅ็ๆถๆฏ่ฟๅ ฅDLQ๏ผๆฏๆไบบๅทฅๆๆฅไธ้่ฏ
ๆๆ๏ผ่ทณ่ฝฌๆฅๅฃP99ๅปถ่ฟ้ไฝ60%+๏ผ็ป่ฎกไธๆ ธๅฟ้พ่ทฏๅฎๅ จ่งฃ่ฆ
้ฎ้ข่ๆฏ๏ผ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% |
่ฐไผๅ ณ้ฎ็น๏ผ
- HikariCP่ฟๆฅๆฑ ๏ผ
maximumPoolSize=100๏ผconnectionTimeout=3s - Caffeineๆฌๅฐ็ผๅญ50Kๆก็ฎ๏ผๅฝไธญ็>92%
- Redis Lettuce่ฟๆฅๆฑ ๏ผ
maxActive=600๏ผmaxIdle=200 - 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+๏ผๅ็ซฏ๏ผ
docker compose up -d mysql redis rabbitmqCREATE 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);cd TinyFlow
mvn spring-boot:runcd 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ๅฏผๅ
ฅ web/infra/observability/dashboards/shortener-overview.json๏ผๅฏ่งๅ๏ผ
- ่ทณ่ฝฌๆฅๅฃ QPS / P95 / P99
- ็ผๅญๅฝไธญ็๏ผL1/L2๏ผ
- ็ๆญๅจ็ถๆ
- RabbitMQ้ๅๆทฑๅบฆ
| 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