้…็ฝฎไธญๅฟƒ๏ผš่ฎฉๆ”นๅŠจ"็ง’็บง็”Ÿๆ•ˆ"

็ณปๅˆ—ไธƒ๏ผšๅŸบ็ก€่ฎพๆ–ฝ็ฏ‡ ยท ็ฌฌ3็ฏ‡

ไฝ ๆœ‰ๆฒกๆœ‰็ปๅކ่ฟ‡่ฟ™ๆ ท็š„ๅœบๆ™ฏ๏ผšไธบไบ†ๆ”นไธ€ไธช่ถ…ๆ—ถๆ—ถ้—ด๏ผŒ้œ€่ฆ้‡ๆ–ฐๆ‰“ๅŒ…ใ€้‡ๆ–ฐ้ƒจ็ฝฒใ€้‡ๆ–ฐไธŠ็บฟ๏ผŸไธ€ไธช็ฎ€ๅ•็š„ๅ‚ๆ•ฐ่ฐƒๆ•ด๏ผŒๅด่ฆๆŠ˜่…พๅŠๅฐๆ—ถ็”š่‡ณๆ›ดไน…๏ผŸ

้…็ฝฎไธญๅฟƒๅฐฑๆ˜ฏไธบไบ†่งฃๅ†ณ่ฟ™ไธช้—ฎ้ข˜่€Œ็”Ÿ็š„ใ€‚ๅฎƒ่ฎฉ้…็ฝฎๅ˜ๆ›ดไปŽ"ๅ‘ๅธƒ็บง"ๅ˜ๆˆ"็ง’็บง"๏ผŒ่ฎฉ่ฟ็ปดไปŽ"้‡ๅฏๅคงๆณ•"ๅ˜ๆˆ"ไธ€้”ฎ็”Ÿๆ•ˆ"ใ€‚

ไปŠๅคฉๆˆ‘ไปฌๆฅ่Š่Š้…็ฝฎไธญๅฟƒ็š„่ฎพ่ฎกๅŽŸ็†๏ผŒ็œ‹็œ‹ๅฎƒๆ˜ฏๅฆ‚ไฝ•่ฎฉๆ”นๅŠจ"็ง’็บง็”Ÿๆ•ˆ"็š„ใ€‚


ไธ€ใ€้…็ฝฎ็ฎก็†็š„ๆผ”่ฟ›

1.1 ็กฌ็ผ–็ ๆ—ถไปฃ

ๆœ€ๅŽŸๅง‹็š„ๆ–นๅผ๏ผŒ้…็ฝฎ็›ดๆŽฅๅ†™ๆญปๅœจไปฃ็ ้‡Œ๏ผš

const int MAX_RETRY = 3;
const int TIMEOUT_MS = 5000;

่ฟ™็งๆ–นๅผๆœ€็ฎ€ๅ•๏ผŒไฝ†้—ฎ้ข˜ไนŸๆœ€ๆ˜Žๆ˜พ๏ผš

๐ŸŽฏ ็œŸๅฎžๆกˆไพ‹๏ผšๆŸ็”ตๅ•†ๅ›ข้˜Ÿไธบไบ†ๆ”นไธ€ไธชๆ”ฏไป˜่ถ…ๆ—ถๆ—ถ้—ด๏ผŒ็ปๅކไบ†ไปฃ็ ๆไบค โ†’ Code Review โ†’ ๆ‰“ๅŒ… โ†’ ๆต‹่ฏ•็Žฏๅขƒ้ชŒ่ฏ โ†’ ้ข„ๅ‘ๅธƒ้ชŒ่ฏ โ†’ ็”Ÿไบงๅ‘ๅธƒ๏ผŒๆ•ดๆ•ดๆŠ˜่…พไบ† 2 ๅคฉใ€‚็ป“ๆžœ่ถ…ๆ—ถๆ—ถ้—ดไปŽ 5 ็ง’ๆ”นๆˆ 8 ็ง’๏ผŒๅฐฑ่ฟ™ไธ€่กŒๆ”นๅŠจใ€‚

1.2 ้…็ฝฎๆ–‡ไปถๆ—ถไปฃ

ๆŠŠ้…็ฝฎๆŠฝ็ฆปๅˆฐๅค–้ƒจๆ–‡ไปถ๏ผš

# config.properties
max.retry=3
timeout.ms=5000
db.url=jdbc:mysql://localhost:3306/mydb

่ฟ›ๆญฅไบ†ไธ€็‚น๏ผŒไฝ†่ฟ˜ๆ˜ฏๆœ‰้—ฎ้ข˜๏ผš

1.3 ็Žฏๅขƒๅ˜้‡ๆ—ถไปฃ

็”จ็Žฏๅขƒๅ˜้‡ๆณจๅ…ฅ้…็ฝฎ๏ผš

export MAX_RETRY=3
export TIMEOUT_MS=5000
export DB_URL=jdbc:mysql://prod-db:3306/mydb

้…ๅˆๅฎนๅ™จๅŒ–้ƒจ็ฝฒ๏ผŒ็ตๆดปไบ†ๅพˆๅคšใ€‚ไฝ†๏ผš

1.4 ้…็ฝฎไธญๅฟƒๆ—ถไปฃ

้…็ฝฎไธญๅฟƒ็š„ๆ ธๅฟƒ็†ๅฟต๏ผš้…็ฝฎไนŸๆ˜ฏไธ€็งๆœๅŠกใ€‚

่ฟ™ๅฐฑๅƒไปŽ"ๆฏไธชไบบ่‡ชๅทฑๅš้ฅญ"่ฟ›ๅŒ–ๅˆฐ"ไธญๅคฎๅŽจๆˆฟ็ปŸไธ€้…้€"โ€”โ€”ๆ ‡ๅ‡†ๅŒ–ใ€้ซ˜ๆ•ˆ็އใ€ๅฏ่ฟฝๆบฏใ€‚


ไบŒใ€้…็ฝฎไธญๅฟƒ็š„ๆ ธๅฟƒ่ฎพ่ฎกๅŽŸ็†

2.1 ๆ•ดไฝ“ๆžถๆž„

้…็ฝฎไธญๅฟƒ้‡‡็”จๅˆ†ๅฑ‚ๆžถๆž„่ฎพ่ฎก๏ผš

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                    ้…็ฝฎ็ฎก็†ๆŽงๅˆถๅฐ                        โ”‚
โ”‚              (Web UI / API)                             โ”‚
โ”‚    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”         โ”‚
โ”‚    โ”‚  ้…็ฝฎ็ผ–่พ‘    โ”‚  ็‰ˆๆœฌ็ฎก็†    โ”‚  ๆƒ้™็ฎก็†    โ”‚         โ”‚
โ”‚    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜         โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                         โ”‚
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                  ้…็ฝฎๆœๅŠกๅฑ‚                              โ”‚
โ”‚              (Config Server Cluster)                    โ”‚
โ”‚    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”         โ”‚
โ”‚    โ”‚  ้…็ฝฎ่ฏปๅ†™    โ”‚  ๅ˜ๆ›ดๆŽจ้€    โ”‚  SDK ๆŽฅๅ…ฅ    โ”‚         โ”‚
โ”‚    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜         โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                         โ”‚
        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
        โ–ผ                โ–ผ                โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  MySQL ้›†็พค    โ”‚ โ”‚  Redis ้›†็พค   โ”‚ โ”‚  ๅค‡ไปฝๅญ˜ๅ‚จ     โ”‚
โ”‚ (้…็ฝฎๅญ˜ๅ‚จ)     โ”‚ โ”‚ (ๅ˜ๆ›ด้€š็Ÿฅ)    โ”‚ โ”‚ (็‰ˆๆœฌๆŽงๅˆถ)    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
        โ”‚                โ”‚                โ”‚
        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                         โ”‚
        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
        โ–ผ                โ–ผ                โ–ผ
   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”      โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”      โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
   โ”‚ ๅบ”็”จ A  โ”‚      โ”‚ ๅบ”็”จ B  โ”‚      โ”‚ ๅบ”็”จ C  โ”‚
   โ”‚ SDK     โ”‚      โ”‚ SDK     โ”‚      โ”‚ SDK     โ”‚
   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜      โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜      โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

2.2 ๆ ธๅฟƒ็ป„ไปถ่ฏฆ่งฃ

้…็ฝฎๆœๅŠก็ซฏๆ˜ฏๆ•ดไธช็ณป็ปŸ็š„ๅคง่„‘๏ผŒ่ดŸ่ดฃ๏ผš

// ้…็ฝฎๆœๅŠก็ซฏๆ ธๅฟƒๆŽฅๅฃ็คบไพ‹
@RestController
@RequestMapping("/api/v1/configs")
public class ConfigController {
    
    // ่Žทๅ–้…็ฝฎ
    @GetMapping("/{appId}/{env}/{namespace}")
    public ConfigResponse getConfig(
        @PathVariable String appId,
        @PathVariable String env,
        @PathVariable String namespace,
        @RequestHeader("Authorization") String token) {
        
        // 1. ้ชŒ่ฏๆƒ้™
        if (!authService.hasPermission(token, appId, env)) {
            throw new ForbiddenException("No permission");
        }
        
        // 2. ่Žทๅ–้…็ฝฎ๏ผˆๅธฆ็ผ“ๅญ˜๏ผ‰
        Config config = configService.getConfig(appId, env, namespace);
        
        // 3. ่ฟ”ๅ›ž้…็ฝฎๅ†…ๅฎนๅ’Œ็‰ˆๆœฌๅท
        return ConfigResponse.builder()
            .config(config.getContent())
            .version(config.getVersion())
            .releaseKey(config.getReleaseKey())
            .build();
    }
    
    // ๅ‘ๅธƒ้…็ฝฎ
    @PostMapping("/{appId}/{env}/{namespace}/release")
    public ReleaseResponse releaseConfig(
        @PathVariable String appId,
        @PathVariable String env,
        @PathVariable String namespace,
        @RequestBody ReleaseRequest request) {
        
        // 1. ็”Ÿๆˆๆ–ฐ็‰ˆๆœฌๅท
        long newVersion = versionGenerator.next();
        
        // 2. ๆŒไน…ๅŒ–้…็ฝฎ
        configService.release(appId, env, namespace, 
            request.getConfigs(), newVersion, request.getOperator());
        
        // 3. ้€š็Ÿฅๆ‰€ๆœ‰่ฎข้˜…็š„ๅฎขๆˆท็ซฏ
        notificationService.notify(appId, env, namespace, newVersion);
        
        return ReleaseResponse.success(newVersion);
    }
}

ๅฎขๆˆท็ซฏ SDK ๅตŒๅ…ฅๅˆฐไธšๅŠกๅบ”็”จไธญ๏ผŒๆ˜ฏ้…็ฝฎไธญๅฟƒ็š„่งฆ่ง’๏ผš

// ๅฎขๆˆท็ซฏ SDK ไฝฟ็”จ็คบไพ‹
public class ConfigClientExample {
    
    public static void main(String[] args) {
        // ๅˆๅง‹ๅŒ–้…็ฝฎๅฎขๆˆท็ซฏ
        ConfigService configService = ConfigService.getAppConfig(
            "my-app",                    // ๅบ”็”จ ID
            "prod",                      // ็Žฏๅขƒ
            new ConfigChangeListener() { // ๅ˜ๆ›ด็›‘ๅฌๅ™จ
                @Override
                public void onChange(ConfigChangeEvent event) {
                    // ้…็ฝฎๅ˜ๆ›ดๅ›ž่ฐƒ
                    for (String key : event.changedKeys()) {
                        ConfigChange change = event.getChange(key);
                        System.out.println(String.format(
                            "้…็ฝฎ %s ไปŽ %s ๅ˜ๆ›ดไธบ %s",
                            key, change.getOldValue(), change.getNewValue()));
                        
                        // ๅŠจๆ€่ฐƒๆ•ด่ฟžๆŽฅๆฑ ๅคงๅฐ
                        if ("db.pool.size".equals(key)) {
                            int newSize = Integer.parseInt(change.getNewValue());
                            connectionPool.resize(newSize);
                        }
                    }
                }
            });
        
        // ่Žทๅ–้…็ฝฎๅ€ผ
        int timeout = configService.getIntProperty("request.timeout", 5000);
        String dbUrl = configService.getProperty("db.url", "localhost:3306");
    }
}

ๅญ˜ๅ‚จๅฑ‚ๆ˜ฏ้…็ฝฎไธญๅฟƒ็š„ๅŸบ็Ÿณ๏ผš

ๅญ˜ๅ‚จๆ–นๆกˆ ไผ˜ๅŠฟ ๅŠฃๅŠฟ ้€‚็”จๅœบๆ™ฏ
MySQL ๆˆ็†Ÿ็จณๅฎšใ€ๆ˜“่ฟ็ปด ๆ— ๅŽŸ็”ŸๆŽจ้€่ƒฝๅŠ› ไธญๅฐ่ง„ๆจกใ€ไผ ็ปŸๆžถๆž„
Redis ้ซ˜ๆ€ง่ƒฝใ€Pub/Sub ๆœบๅˆถ ๅ†…ๅญ˜ๆˆๆœฌ่พƒ้ซ˜ ้ซ˜้ข‘ๅ˜ๆ›ดๅœบๆ™ฏ
ๆททๅˆๅญ˜ๅ‚จ ๅ…ผ้กพๆ€ง่ƒฝๅ’Œๅฏ้ ๆ€ง ๆžถๆž„ๅคๆ‚ๅบฆ้ซ˜ ๅคง่ง„ๆจก็”Ÿไบง็Žฏๅขƒ

2.3 ้…็ฝฎๆจกๅž‹่ฎพ่ฎก

ไธ€ไธช็ตๆดป็š„้…็ฝฎๆจกๅž‹้œ€่ฆ่€ƒ่™‘ๅคšไธช็ปดๅบฆ๏ผš

้…็ฝฎไธญๅฟƒ
โ”œโ”€โ”€ ๅ‘ฝๅ็ฉบ้—ด๏ผˆNamespace๏ผ‰
โ”‚   โ”œโ”€โ”€ dev๏ผˆๅผ€ๅ‘็Žฏๅขƒ๏ผ‰
โ”‚   โ”‚   โ”œโ”€โ”€ ๅบ”็”จ A
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ application.yml๏ผˆไธป้…็ฝฎ๏ผ‰
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ db.yml๏ผˆๆ•ฐๆฎๅบ“้…็ฝฎ๏ผ‰
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ redis.yml๏ผˆ็ผ“ๅญ˜้…็ฝฎ๏ผ‰
โ”‚   โ”‚   โ””โ”€โ”€ ๅบ”็”จ B
โ”‚   โ”œโ”€โ”€ test๏ผˆๆต‹่ฏ•็Žฏๅขƒ๏ผ‰
โ”‚   โ”œโ”€โ”€ uat๏ผˆ้ข„ๅ‘ๅธƒ็Žฏๅขƒ๏ผ‰
โ”‚   โ””โ”€โ”€ prod๏ผˆ็”Ÿไบง็Žฏๅขƒ๏ผ‰
# ้…็ฝฎ้กน็คบไพ‹
db:
  url: jdbc:mysql://prod-db:3306/mydb
  username: app_user
  password: ${DB_PASSWORD}  # ๆ”ฏๆŒๅ ไฝ็ฌฆ
  pool:
    max_size: 100
    min_idle: 10
    
cache:
  type: redis
  hosts:
    - redis-1:6379
    - redis-2:6379
    - redis-3:6379
  timeout_ms: 3000

ไธ‰ใ€้…็ฝฎ็š„็ƒญๆ›ดๆ–ฐๆœบๅˆถ

้…็ฝฎไธญๅฟƒๆœ€ๆ ธๅฟƒ็š„่ƒฝๅŠ›๏ผš้…็ฝฎๅ˜ๆ›ดๅŽ๏ผŒๅบ”็”จๆ— ้œ€้‡ๅฏๅณๅฏ็”Ÿๆ•ˆใ€‚ๆˆ‘ไปฌๅฎž็Žฐไบ† 3 ็ง’ๅ†…้…็ฝฎ็”Ÿๆ•ˆ ็š„่ƒฝๅŠ›ใ€‚่ฟ™ๆ˜ฏๅฆ‚ไฝ•ๅฎž็Žฐ็š„๏ผŸ

3.1 ๆŽจๆ‹‰็ป“ๅˆๆจกๅผ

้…็ฝฎๅŒๆญฅ้‡‡็”จๆŽจๆ‹‰็ป“ๅˆ็š„ๆœ€ไฝณๅฎž่ทต๏ผš

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Client โ”‚                        โ”‚  Server โ”‚
โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”˜                        โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”˜
     โ”‚                                  โ”‚
     โ”‚  1. ๅปบ็ซ‹้•ฟ่ฟžๆŽฅ                    โ”‚
     โ”‚ โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•> โ”‚
     โ”‚                                  โ”‚
     โ”‚  2. ๆ”ถๅˆฐๅ˜ๆ›ด้€š็Ÿฅ๏ผˆๅช้€š็Ÿฅ็‰ˆๆœฌๅท๏ผ‰    โ”‚
     โ”‚ <โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• โ”‚
     โ”‚                                  โ”‚
     โ”‚  3. ไธปๅŠจๆ‹‰ๅ–ๆœ€ๆ–ฐ้…็ฝฎ              โ”‚
     โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€> โ”‚
     โ”‚                                  โ”‚
     โ”‚        4. ่ฟ”ๅ›žๅฎŒๆ•ด้…็ฝฎๅ†…ๅฎน        โ”‚
     โ”‚ <โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚
     โ”‚                                  โ”‚
  1. ้€š็Ÿฅ่ฝป้‡ๅŒ–๏ผšๆŽจ้€ๅชๅ‘้€็‰ˆๆœฌๅท๏ผŒๆ•ฐๆฎ้‡ๆžๅฐ
  2. ๅฏ้ ๆ€งไฟ้šœ๏ผšๅณไฝฟ้•ฟ่ฟžๆŽฅๆ–ญๅผ€๏ผŒๅฎขๆˆท็ซฏๅฏไปฅๅฎšๆ—ถ่ฝฎ่ฏขๅ…œๅบ•
  3. ่ดŸ่ฝฝๅฏๆŽง๏ผšๅฎขๆˆท็ซฏๆ”ถๅˆฐ้€š็ŸฅๅŽไธปๅŠจๆ‹‰ๅ–๏ผŒๆœๅŠก็ซฏๆ— ้œ€็ปดๆŠคๆŽจ้€้˜Ÿๅˆ—
  4. 3็ง’็”Ÿๆ•ˆ๏ผšไปŽ้…็ฝฎๅ‘ๅธƒๅˆฐๅฎขๆˆท็ซฏ็”Ÿๆ•ˆ๏ผŒๅ…จ็จ‹ไธ่ถ…่ฟ‡ 3 ็ง’

3.2 ้•ฟ่ฟžๆŽฅ็š„ๅฎž็Žฐๆ–นๆกˆ

// ้•ฟ่ฝฎ่ฏขๅฎž็Žฐ็คบไพ‹
@GetMapping("/notifications")
public DeferredResult<ConfigNotification> longPollNotification(
    @RequestParam String appId,
    @RequestParam String env,
    @RequestParam long currentVersion) {
    
    // ่ฎพ็ฝฎ่ถ…ๆ—ถๆ—ถ้—ด 30 ็ง’
    DeferredResult<ConfigNotification> result = 
        new DeferredResult<>(30000L, "No changes");
    
    // ๆณจๅ†Œ็›‘ๅฌๅ™จ
    notificationRegistry.register(appId, env, currentVersion, notification -> {
        // ๆœ‰ๅ˜ๆ›ดๆ—ถ็ซ‹ๅณ่ฟ”ๅ›ž
        result.setResult(notification);
    });
    
    // ่ถ…ๆ—ถๆˆ–ๅฎŒๆˆๆ—ถ็งป้™ค็›‘ๅฌๅ™จ
    result.onCompletion(() -> {
        notificationRegistry.unregister(appId, env);
    });
    
    return result;
}

3.3 ้…็ฝฎๆ›ดๆ–ฐ็š„ๅŽŸๅญๆ€งไฟ่ฏ

้…็ฝฎๆ›ดๆ–ฐๅฟ…้กปไฟ่ฏๅŽŸๅญๆ€ง๏ผš่ฆไนˆๅ…จ้ƒจ็”Ÿๆ•ˆ๏ผŒ่ฆไนˆๅ…จ้ƒจไธ็”Ÿๆ•ˆใ€‚

ๅ‡่ฎพ้œ€่ฆๅŒๆ—ถๆ›ดๆ–ฐๆ•ฐๆฎๅบ“่ฟžๆŽฅๅœฐๅ€ๅ’Œๅฏ†็ ๏ผš

# ๅŽŸ้…็ฝฎ
db:
  url: jdbc:mysql://old-db:3306/mydb
  password: old_password

# ็›ฎๆ ‡้…็ฝฎ
db:
  url: jdbc:mysql://new-db:3306/mydb
  password: new_password

ๅฆ‚ๆžœๅชๆ›ดๆ–ฐไบ† url๏ผŒๅฏ†็ ่ฟ˜ๆ˜ฏๆ—ง็š„ โ†’ ่ฟžๆŽฅๅคฑ่ดฅ๏ผ ๅฆ‚ๆžœๅชๆ›ดๆ–ฐไบ†ๅฏ†็ ๏ผŒurl ่ฟ˜ๆ˜ฏๆ—ง็š„ โ†’ ๅŒๆ ทๅคฑ่ดฅ๏ผ

public class ConfigManager {
    
    private volatile ConfigSnapshot currentConfig;
    private final AtomicLong configVersion = new AtomicLong(0);
    
    // ้…็ฝฎๆ›ดๆ–ฐ๏ผˆๅŽŸๅญๆ“ไฝœ๏ผ‰
    public void updateConfig(ConfigSnapshot newSnapshot) {
        // 1. ๆ ก้ชŒ็‰ˆๆœฌๅท๏ผˆ้˜ฒๆญขไนฑๅบ๏ผ‰
        if (newSnapshot.getVersion() <= configVersion.get()) {
            return; // ๅฟฝ็•ฅๆ—ง็‰ˆๆœฌ
        }
        
        // 2. ๅŠ ่ฝฝๆ–ฐ้…็ฝฎๅˆฐไธดๆ—ถๅ˜้‡
        Map<String, String> newConfigs = newSnapshot.getConfigs();
        
        // 3. ้ชŒ่ฏ้…็ฝฎๅฎŒๆ•ดๆ€ง
        if (!validateConfig(newConfigs)) {
            throw new ConfigValidationException("Invalid config");
        }
        
        // 4. ๅŽŸๅญๆ›ฟๆข๏ผˆvolatile ไฟ่ฏๅฏ่งๆ€ง๏ผ‰
        currentConfig = newSnapshot;
        configVersion.set(newSnapshot.getVersion());
        
        // 5. ่งฆๅ‘ๅ›ž่ฐƒ้€š็Ÿฅ
        notifyConfigChange(newSnapshot);
    }
    
    // ่Žทๅ–้…็ฝฎ๏ผˆ็บฟ็จ‹ๅฎ‰ๅ…จ๏ผ‰
    public String getConfig(String key) {
        return currentConfig.get(key);
    }
}
  1. ็‰ˆๆœฌๅทๆœบๅˆถ๏ผšๆฏๆฌกๅ‘ๅธƒ็”Ÿๆˆๆ–ฐ็‰ˆๆœฌ๏ผŒๅฎขๆˆท็ซฏๆŒ‰็‰ˆๆœฌๅทๆ•ดไฝ“ๆ›ฟๆข
  2. ๅŒ็ผ“ๅ†ฒๅˆ‡ๆข๏ผšๅ…ˆๅŠ ่ฝฝๅˆฐๆ–ฐ buffer๏ผŒ้ชŒ่ฏ้€š่ฟ‡ๅŽไธ€ๆฌกๆ€งๅˆ‡ๆขๆŒ‡้’ˆ
  3. volatile ไฟ่ฏๅฏ่งๆ€ง๏ผš็กฎไฟๆ‰€ๆœ‰็บฟ็จ‹็ซ‹ๅณ็œ‹ๅˆฐๆ–ฐ้…็ฝฎ

3.4 ็ฐๅบฆๅ‘ๅธƒไธŽๅ›žๆปš

้…็ฝฎๅ˜ๆ›ดไนŸๆœ‰้ฃŽ้™ฉ๏ผŒ้œ€่ฆๆ”ฏๆŒ็ฐๅบฆๅ’Œๅ›žๆปšใ€‚

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                    ็ฐๅบฆๅ‘ๅธƒๆต็จ‹                         โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                         โ”‚
                         โ–ผ
        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
        โ”‚  1. ้€‰ๆ‹ฉ็ฐๅบฆๅฎžไพ‹๏ผˆๆŒ‰ IP/ๆ ‡็ญพ๏ผ‰   โ”‚
        โ”‚     ไพ‹ๅฆ‚๏ผšweb-server-[01-03]   โ”‚
        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                         โ–ผ
        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
        โ”‚  2. ๅ‘ๅธƒๆ–ฐ้…็ฝฎๅˆฐ็ฐๅบฆๅฎžไพ‹         โ”‚
        โ”‚     ่ง‚ๅฏŸ็›‘ๆŽงๆŒ‡ๆ ‡ 5-10 ๅˆ†้’Ÿ      โ”‚
        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                         โ–ผ
              โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
              โ”‚                     โ”‚
              โ–ผ                     โ–ผ
        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”          โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
        โ”‚  ๅผ‚ๅธธ๏ผŸ   โ”‚          โ”‚  ๆญฃๅธธ๏ผŸ   โ”‚
        โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜          โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜
             โ”‚                     โ”‚
             โ–ผ                     โ–ผ
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    โ”‚  ็ซ‹ๅณๅ›žๆปš       โ”‚    โ”‚  ๆ‰ฉๅคง็ฐๅบฆ่Œƒๅ›ด   โ”‚
    โ”‚  ๅˆ†ๆžๅŽŸๅ›        โ”‚    โ”‚  ๆˆ–ๅ…จ้‡ๅ‘ๅธƒ     โ”‚
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
@Service
public class GrayReleaseService {
    
    // ็ฐๅบฆๅ‘ๅธƒ
    public ReleaseResult grayRelease(GrayReleaseRequest request) {
        String appId = request.getAppId();
        String env = request.getEnv();
        List<String> grayInstances = request.getGrayInstances();
        long newVersion = request.getNewVersion();
        
        // 1. ๆ ‡่ฎฐ็ฐๅบฆๅฎžไพ‹
        for (String instance : grayInstances) {
            instanceRegistry.markGray(appId, env, instance, newVersion);
        }
        
        // 2. ๆŽจ้€ๆ–ฐ้…็ฝฎๅˆฐ็ฐๅบฆๅฎžไพ‹
        notificationService.notifyInstances(appId, env, grayInstances, newVersion);
        
        // 3. ๅˆ›ๅปบ็ฐๅบฆ็›‘ๆŽงไปปๅŠก
        monitorService.createGrayMonitor(appId, env, newVersion, 
            grayInstances, request.getMonitorDuration());
        
        return ReleaseResult.grayStarted(newVersion, grayInstances);
    }
    
    // ๅ…จ้‡ๅ‘ๅธƒ๏ผˆ็ฐๅบฆ้ชŒ่ฏ้€š่ฟ‡ๅŽ๏ผ‰
    public ReleaseResult fullRelease(String appId, String env, long version) {
        // 1. ๆธ…้™ค็ฐๅบฆๆ ‡่ฎฐ
        instanceRegistry.clearGray(appId, env);
        
        // 2. ๅ…จ้‡ๆŽจ้€
        notificationService.notifyAll(appId, env, version);
        
        return ReleaseResult.fullReleased(version);
    }
    
    // ไธ€้”ฎๅ›žๆปš
    public ReleaseResult rollback(String appId, String env, long targetVersion) {
        // 1. ่Žทๅ–็›ฎๆ ‡็‰ˆๆœฌ้…็ฝฎ
        ConfigSnapshot targetConfig = configRepository.getVersion(appId, env, targetVersion);
        
        // 2. ๅ‘ๅธƒไธบๆœ€ๆ–ฐ็‰ˆๆœฌ
        long newVersion = versionGenerator.next();
        configRepository.release(appId, env, targetConfig.getConfigs(), newVersion);
        
        // 3. ๅ…จ้‡ๆŽจ้€
        notificationService.notifyAll(appId, env, newVersion);
        
        // 4. ่ฎฐๅฝ•ๅ›žๆปšๆ“ไฝœ
        auditService.log(AuditAction.ROLLBACK, appId, env, newVersion, 
            "Rollback to version " + targetVersion);
        
        return ReleaseResult.rolledBack(newVersion, targetVersion);
    }
}

ๅ››ใ€ๅคš็Žฏๅขƒ็ฎก็†ๆ–นๆกˆ

ไธ€ไธชๅบ”็”จ้€šๅธธ่ฆ็ปๅކๅคšไธช็Žฏๅขƒ๏ผšๅผ€ๅ‘ใ€ๆต‹่ฏ•ใ€้ข„ๅ‘ๅธƒใ€็”Ÿไบงใ€‚ๆฏไธช็Žฏๅขƒ็š„้…็ฝฎ้ƒฝไธๅฐฝ็›ธๅŒใ€‚

4.1 ็Žฏๅขƒ้š”็ฆป็ญ–็•ฅ

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Dev Config     โ”‚   โ”‚  Test Config    โ”‚   โ”‚  Prod Config    โ”‚
โ”‚  Cluster        โ”‚   โ”‚  Cluster        โ”‚   โ”‚  Cluster        โ”‚
โ”‚                 โ”‚   โ”‚                 โ”‚   โ”‚                 โ”‚
โ”‚  configcenter   โ”‚   โ”‚  configcenter   โ”‚   โ”‚  configcenter   โ”‚
โ”‚  -dev           โ”‚   โ”‚  -test          โ”‚   โ”‚  -prod          โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
        โ”‚                     โ”‚                     โ”‚
        โ–ผ                     โ–ผ                     โ–ผ
   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”          โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”          โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
   โ”‚ Dev App โ”‚          โ”‚Test App โ”‚          โ”‚Prod App โ”‚
   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜          โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜          โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                 ็ปŸไธ€้…็ฝฎไธญๅฟƒ้›†็พค                        โ”‚
โ”‚                                                        โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”            โ”‚
โ”‚  โ”‚  dev     โ”‚  โ”‚  test    โ”‚  โ”‚  prod    โ”‚            โ”‚
โ”‚  โ”‚ namespaceโ”‚  โ”‚ namespaceโ”‚  โ”‚ namespaceโ”‚            โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜            โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Non-Prod Config Cluster        โ”‚   โ”‚  Prod Config    โ”‚
โ”‚  (Dev + Test + UAT)             โ”‚   โ”‚  Cluster        โ”‚
โ”‚                                 โ”‚   โ”‚                 โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚   โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚  โ”‚  dev     โ”‚  โ”‚  test    โ”‚    โ”‚   โ”‚  โ”‚  prod    โ”‚  โ”‚
โ”‚  โ”‚  ns      โ”‚  โ”‚  ns      โ”‚    โ”‚   โ”‚  โ”‚  ns      โ”‚  โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚   โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

4.2 ้…็ฝฎ็ปงๆ‰ฟไธŽ่ฆ†็›–

ไธๅŒ็Žฏๅขƒ็š„้…็ฝฎๆœ‰ๅคง้‡็›ธๅŒ้ƒจๅˆ†๏ผŒๅฆ‚ไฝ•้ฟๅ…้‡ๅค๏ผŸ

# base.yml - ๅŸบ็ก€้…็ฝฎ๏ผˆๆ‰€ๆœ‰็Žฏๅขƒๅ…ฑไบซ๏ผ‰
app:
  name: my-service
  version: 1.0.0

logging:
  level: INFO
  format: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"

features:
  enable_cache: true
  enable_metrics: true

---
# dev.yml - ๅผ€ๅ‘็Žฏๅขƒ้…็ฝฎ๏ผˆ็ปงๆ‰ฟ base.yml๏ผ‰
extends: base.yml

overrides:
  logging:
    level: DEBUG  # ๅผ€ๅ‘็Žฏๅขƒๅผ€ๅฏ DEBUG
  
  db:
    url: jdbc:mysql://localhost:3306/mydb_dev
    username: dev_user
    password: dev_password

---
# prod.yml - ็”Ÿไบง็Žฏๅขƒ้…็ฝฎ๏ผˆ็ปงๆ‰ฟ base.yml๏ผ‰
extends: base.yml

overrides:
  logging:
    level: WARN  # ็”Ÿไบง็Žฏๅขƒๅช่ฎฐๅฝ• WARN ไปฅไธŠ
  
  db:
    url: jdbc:mysql://prod-db-master:3306/mydb
    username: prod_user
    password: ${DB_PASSWORD}  # ไปŽ็Žฏๅขƒๅ˜้‡่ฏปๅ–
    
  db:
    slaves:
      - url: jdbc:mysql://prod-db-slave-1:3306/mydb
      - url: jdbc:mysql://prod-db-slave-2:3306/mydb
็Žฏๅขƒ็‰นๅฎš้…็ฝฎ > ็ŽฏๅขƒๅŸบ็ก€้…็ฝฎ > ๅ…จๅฑ€้ป˜่ฎค้…็ฝฎ

่ฟ™ๆ ท๏ผŒๅ…ฌๅ…ฑ้…็ฝฎๅช้œ€็ปดๆŠคไธ€ไปฝ๏ผŒ็Žฏๅขƒๅทฎๅผ‚้…็ฝฎๅ•็‹ฌ่ฆ†็›–ใ€‚

4.3 ้…็ฝฎๅŒๆญฅ็ญ–็•ฅ

ๅฝ“ๅ…ฌๅ…ฑ้…็ฝฎๅ˜ๆ›ดๆ—ถ๏ผŒๅฆ‚ไฝ•ๅŒๆญฅๅˆฐๆ‰€ๆœ‰็Žฏๅขƒ๏ผŸ

็ฎ€ๅ•ไฝ†ๅฎนๆ˜“้—ๆผ๏ผŒ้€‚ๅˆๅฐๅž‹ๅ›ข้˜Ÿใ€‚

# .github/workflows/sync-config.yml
name: Sync Base Config

on:
  push:
    paths:
      - 'configs/base.yml'

jobs:
  sync:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      
      - name: Sync to all environments
        run: |
          # ่ฏปๅ– base.yml
          BASE_CONFIG=$(cat configs/base.yml)
          
          # ๅŒๆญฅๅˆฐๅผ€ๅ‘็Žฏๅขƒ
          curl -X PUT "https://configcenter.dev.internal/api/v1/configs/my-app/base" \
            -H "Authorization: ${{ secrets.CONFIG_CENTER_TOKEN_DEV }}" \
            -d "$BASE_CONFIG"
          
          # ๅŒๆญฅๅˆฐๆต‹่ฏ•็Žฏๅขƒ
          curl -X PUT "https://configcenter.test.internal/api/v1/configs/my-app/base" \
            -H "Authorization: ${{ secrets.CONFIG_CENTER_TOKEN_TEST }}" \
            -d "$BASE_CONFIG"
          
          # ๅŒๆญฅๅˆฐ็”Ÿไบง็Žฏๅขƒ๏ผˆ้œ€่ฆๅฎกๆ‰น๏ผ‰
          # ็”Ÿไบง็ŽฏๅขƒๅŒๆญฅๅœจๅ•็‹ฌ็š„ workflow ไธญ

configcenter ๆ”ฏๆŒ้…็ฝฎๅ…‹้š†ๅ’ŒๅŒๆญฅๅŠŸ่ƒฝ๏ผš

// ้…็ฝฎๅ…‹้š†็คบไพ‹
public void cloneConfig(String sourceEnv, String targetEnv, String appId) {
    // 1. ่ฏปๅ–ๆบ็Žฏๅขƒ้…็ฝฎ
    Config sourceConfig = configService.getConfig(appId, sourceEnv, "application");
    
    // 2. ๅ…‹้š†ๅˆฐ็›ฎๆ ‡็Žฏๅขƒ
    configService.createConfig(appId, targetEnv, "application", sourceConfig);
    
    // 3. ่ฎฐๅฝ•ๅฎก่ฎกๆ—ฅๅฟ—
    auditService.log(AuditAction.CLONE, appId, targetEnv, 
        "Cloned from " + sourceEnv);
}

ไบ”ใ€้ซ˜ๅฏ็”จ่ฎพ่ฎก

้…็ฝฎไธญๅฟƒๆ˜ฏๅพฎๆœๅŠกๆžถๆž„็š„ๅ…ณ้”ฎไพ่ต–๏ผŒๅฟ…้กปไฟ่ฏ้ซ˜ๅฏ็”จใ€‚ไธ€ๆ—ฆ้…็ฝฎไธญๅฟƒๅฎ•ๆœบ๏ผŒๅฏ่ƒฝๅฏผ่‡ดๆ‰€ๆœ‰ๆœๅŠกๆ— ๆณ•ๅฏๅŠจๆˆ–้…็ฝฎๆ— ๆณ•ๆ›ดๆ–ฐใ€‚

5.1 ๅคšๅฑ‚ๆฌกๅฎน้”™ๆœบๅˆถ

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                    ้ซ˜ๅฏ็”จๆžถๆž„                          โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                         โ”‚
        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
        โ–ผ                โ–ผ                โ–ผ
   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”      โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”      โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
   โ”‚Server-1 โ”‚      โ”‚Server-2 โ”‚      โ”‚Server-3 โ”‚
   โ”‚(Active) โ”‚      โ”‚(Active) โ”‚      โ”‚(Standby)โ”‚
   โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”˜      โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”˜      โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”˜
        โ”‚                โ”‚                โ”‚
        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                         โ”‚
                         โ–ผ
        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
        โ”‚      ่ดŸ่ฝฝๅ‡่กก๏ผˆNginx/SLB๏ผ‰      โ”‚
        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                         โ”‚
        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
        โ–ผ                โ–ผ                โ–ผ
   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”      โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”      โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
   โ”‚ Client  โ”‚      โ”‚ Client  โ”‚      โ”‚ Client  โ”‚
   โ”‚ Local   โ”‚      โ”‚ Local   โ”‚      โ”‚ Local   โ”‚
   โ”‚ Cache   โ”‚      โ”‚ Cache   โ”‚      โ”‚ Cache   โ”‚
   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜      โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜      โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
ๅฑ‚ๆฌก ๆœบๅˆถ ไฝœ็”จ
ๅฎขๆˆท็ซฏ ๆœฌๅœฐ็ผ“ๅญ˜ ้…็ฝฎไธญๅฟƒๅฎ•ๆœบๆ—ถ๏ผŒไฝฟ็”จๆœฌๅœฐ็ผ“ๅญ˜้…็ฝฎ
็ฝ‘็ปœๅฑ‚ ่ดŸ่ฝฝๅ‡่กก ่‡ชๅŠจๅ‰”้™คๆ•…้šœ่Š‚็‚น
ๆœๅŠกๅฑ‚ ้›†็พค้ƒจ็ฝฒ ๅคš่Š‚็‚นไบ’ๅค‡
ๆ•ฐๆฎๅฑ‚ ไธปไปŽๅคๅˆถ ๆ•ฐๆฎๆŒไน…ๅŒ–ไฟ่ฏ

5.2 ๅฎขๆˆท็ซฏๅฎน้”™่ฎพ่ฎก

public class ConfigClient {
    
    private final ConfigServerProxy serverProxy;
    private final LocalConfigCache localCache;
    private final AtomicReference<ConfigSnapshot> currentConfig;
    
    // ่Žทๅ–้…็ฝฎ๏ผˆๅธฆๅฎน้”™๏ผ‰
    public String getConfig(String key) {
        // 1. ไผ˜ๅ…ˆไฝฟ็”จๅ†…ๅญ˜็ผ“ๅญ˜
        ConfigSnapshot snapshot = currentConfig.get();
        if (snapshot != null && snapshot.contains(key)) {
            return snapshot.get(key);
        }
        
        // 2. ๅฐ่ฏ•ไปŽๆœๅŠก็ซฏ่Žทๅ–
        try {
            ConfigSnapshot newSnapshot = serverProxy.fetchConfig();
            currentConfig.set(newSnapshot);
            localCache.persist(newSnapshot); // ๆŒไน…ๅŒ–ๅˆฐๆœฌๅœฐ
            return newSnapshot.get(key);
        } catch (Exception e) {
            log.warn("Failed to fetch config from server, use local cache", e);
            
            // 3. ๆœๅŠก็ซฏๅคฑ่ดฅ๏ผŒไฝฟ็”จๆœฌๅœฐๆ–‡ไปถ็ผ“ๅญ˜
            ConfigSnapshot cachedSnapshot = localCache.load();
            if (cachedSnapshot != null) {
                currentConfig.set(cachedSnapshot);
                return cachedSnapshot.get(key);
            }
            
            // 4. ๅฎŒๅ…จๅคฑ่ดฅ๏ผŒ่ฟ”ๅ›ž้ป˜่ฎคๅ€ผ
            return getDefaultConfig(key);
        }
    }
}

// ๆœฌๅœฐ็ผ“ๅญ˜ๅฎž็Žฐ
public class LocalConfigCache {
    
    private final String cacheDir = System.getProperty("user.home") + "/config-cache";
    
    // ๆŒไน…ๅŒ–้…็ฝฎๅˆฐๆœฌๅœฐ
    public void persist(ConfigSnapshot snapshot) {
        String cacheFile = String.format("%s/%s-%s-%d.cache",
            cacheDir, snapshot.getAppId(), snapshot.getEnv(), snapshot.getVersion());
        
        try (ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream(cacheFile))) {
            oos.writeObject(snapshot);
        } catch (IOException e) {
            log.error("Failed to persist config cache", e);
        }
    }
    
    // ไปŽๆœฌๅœฐๅŠ ่ฝฝ้…็ฝฎ
    public ConfigSnapshot load() {
        File[] cacheFiles = new File(cacheDir).listFiles((dir, name) -> 
            name.startsWith(appId + "-" + env) && name.endsWith(".cache"));
        
        if (cacheFiles == null || cacheFiles.length == 0) {
            return null;
        }
        
        // ๅŠ ่ฝฝๆœ€ๆ–ฐ็š„็ผ“ๅญ˜ๆ–‡ไปถ
        Arrays.sort(cacheFiles, (a, b) -> extractVersion(b) - extractVersion(a));
        
        try (ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream(cacheFiles[0]))) {
            return (ConfigSnapshot) ois.readObject();
        } catch (Exception e) {
            log.error("Failed to load config cache", e);
            return null;
        }
    }
}
public class ConfigClientWithFallback {
    
    // ่Žทๅ–้…็ฝฎ๏ผˆๅคš็บง้™็บง๏ผ‰
    public String getConfigWithFallback(String key, String defaultValue) {
        // Level 1: ๅ†…ๅญ˜็ผ“ๅญ˜๏ผˆๆœ€ๅฟซ๏ผ‰
        String value = memoryCache.get(key);
        if (value != null) {
            return value;
        }
        
        // Level 2: ๆœๅŠก็ซฏ๏ผˆๅฎžๆ—ถ๏ผ‰
        try {
            value = fetchFromServer(key);
            if (value != null) {
                memoryCache.put(key, value);
                return value;
            }
        } catch (Exception e) {
            log.warn("Server unavailable", e);
        }
        
        // Level 3: ๆœฌๅœฐๆ–‡ไปถ็ผ“ๅญ˜๏ผˆๅฎน้”™๏ผ‰
        value = localCache.get(key);
        if (value != null) {
            return value;
        }
        
        // Level 4: ้ป˜่ฎคๅ€ผ๏ผˆๅ…œๅบ•๏ผ‰
        return defaultValue;
    }
}

5.3 ๆœๅŠก็ซฏ้ซ˜ๅฏ็”จ่ฎพ่ฎก

# docker-compose.yml - ้…็ฝฎไธญๅฟƒ้›†็พค้ƒจ็ฝฒ
version: '3.8'

services:
  config-server-1:
    image: configcenter:latest
    environment:
      - SERVER_ID=1
      - CLUSTER_NODES=config-server-1:8080,config-server-2:8080,config-server-3:8080
      - DB_URL=jdbc:mysql://mysql-master:3306/config_center
      - REDIS_URL=redis://redis:6379
    ports:
      - "8081:8080"
    depends_on:
      - mysql-master
      - redis
      
  config-server-2:
    image: configcenter:latest
    environment:
      - SERVER_ID=2
      - CLUSTER_NODES=config-server-1:8080,config-server-2:8080,config-server-3:8080
      - DB_URL=jdbc:mysql://mysql-master:3306/config_center
      - REDIS_URL=redis://redis:6379
    ports:
      - "8082:8080"
    depends_on:
      - mysql-master
      - redis
      
  config-server-3:
    image: configcenter:latest
    environment:
      - SERVER_ID=3
      - CLUSTER_NODES=config-server-1:8080,config-server-2:8080,config-server-3:8080
      - DB_URL=jdbc:mysql://mysql-master:3306/config_center
      - REDIS_URL=redis://redis:6379
    ports:
      - "8083:8080"
    depends_on:
      - mysql-master
      - redis
      
  nginx:
    image: nginx:latest
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
    ports:
      - "80:80"
    depends_on:
      - config-server-1
      - config-server-2
      - config-server-3
@RestController
public class HealthController {
    
    @GetMapping("/health")
    public HealthStatus health() {
        return HealthStatus.builder()
            .status("UP")
            .dbConnection(checkDbConnection())
            .cacheConnection(checkCacheConnection())
            .activeConnections(getActiveConnectionCount())
            .build();
    }
}

// Nginx ๅฅๅบทๆฃ€ๆŸฅ้…็ฝฎ
upstream config_servers {
    server config-server-1:8080 max_fails=3 fail_timeout=30s;
    server config-server-2:8080 max_fails=3 fail_timeout=30s;
    server config-server-3:8080 max_fails=3 fail_timeout=30s;
}

server {
    location / {
        proxy_pass http://config_servers;
        proxy_next_upstream error timeout http_502 http_503 http_504;
    }
}

5.4 ๆ•ฐๆฎๅฑ‚้ซ˜ๅฏ็”จ

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ MySQL Master โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ MySQL Slave  โ”‚
โ”‚ (่ฏปๅ†™)        โ”‚        โ”‚ (ๅช่ฏป)        โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
       โ–ฒ                       โ”‚
       โ”‚                       โ”‚
       โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
         ๆ•…้šœๅˆ‡ๆข๏ผˆVIP ๆˆ– Proxy๏ผ‰
// ๆ•ฐๆฎๅบ“่ฏปๅ†™ๅˆ†็ฆป้…็ฝฎ
public class DataSourceConfig {
    
    @Bean
    @Primary
    public DataSource masterDataSource() {
        return DataSourceBuilder.create()
            .url("jdbc:mysql://mysql-master:3306/config_center")
            .username("root")
            .password("${DB_PASSWORD}")
            .build();
    }
    
    @Bean
    public DataSource slaveDataSource() {
        return DataSourceBuilder.create()
            .url("jdbc:mysql://mysql-slave:3306/config_center")
            .username("readonly")
            .password("${DB_PASSWORD}")
            .build();
    }
    
    @Bean
    public RoutingDataSource routingDataSource(
            @Qualifier("masterDataSource") DataSource master,
            @Qualifier("slaveDataSource") DataSource slave) {
        
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("master", master);
        targetDataSources.put("slave", slave);
        
        RoutingDataSource routingDataSource = new RoutingDataSource();
        routingDataSource.setDefaultTargetDataSource(master);
        routingDataSource.setTargetDataSources(targetDataSources);
        
        return routingDataSource;
    }
}
# redis-sentinel.conf
sentinel monitor configcenter-redis redis-master 6379 2
sentinel down-after-milliseconds configcenter-redis 5000
sentinel failover-timeout configcenter-redis 60000
sentinel parallel-syncs configcenter-redis 1

ๅ…ญใ€้…็ฝฎๅฎ‰ๅ…จ

้…็ฝฎไธญๅพ€ๅพ€ๅŒ…ๅซๆ•ๆ„Ÿไฟกๆฏ๏ผšๆ•ฐๆฎๅบ“ๅฏ†็ ใ€API ๅฏ†้’ฅใ€่ฏไนฆ็ญ‰ใ€‚ๅฎ‰ๅ…จๆ˜ฏ้…็ฝฎไธญๅฟƒ็š„้‡ไธญไน‹้‡ใ€‚

6.1 ไผ ่พ“ๅฎ‰ๅ…จ

ๆ‰€ๆœ‰้…็ฝฎไผ ่พ“ๅฟ…้กปๅŠ ๅฏ†๏ผŒ้˜ฒๆญขไธญ้—ดไบบๆ”ปๅ‡ป๏ผš

# application.yml - ๆœๅŠก็ซฏ TLS ้…็ฝฎ
server:
  port: 8443
  ssl:
    enabled: true
    key-store: classpath:keystore.p12
    key-store-password: ${KEYSTORE_PASSWORD}
    key-store-type: PKCS12
    key-alias: configcenter
// ๅฎขๆˆท็ซฏ่ฏไนฆ้ชŒ่ฏ
@Configuration
public class MutualTLSConfig {
    
    @Bean
    public ServletWebServerFactory servletWebServerFactory() {
        TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
        tomcat.addAdditionalTomcatConnectors(createSslConnector());
        return tomcat;
    }
    
    private Connector createSslConnector() {
        Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
        Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler();
        
        connector.setScheme("https");
        connector.setSecure(true);
        connector.setPort(8443);
        
        // ๆœๅŠก็ซฏ่ฏไนฆ
        protocol.setSSLEnabled(true);
        protocol.setKeystoreFile("/path/to/server-keystore.jks");
        protocol.setKeystorePass("password");
        
        // ๅฎขๆˆท็ซฏ่ฏไนฆ้ชŒ่ฏ
        protocol.setTruststoreFile("/path/to/truststore.jks");
        protocol.setTruststorePass("password");
        protocol.setClientAuth("want"); // ๅฏ้€‰้ชŒ่ฏ
        // protocol.setClientAuth("true"); // ๅผบๅˆถ้ชŒ่ฏ
        
        return connector;
    }
}

6.2 ๅญ˜ๅ‚จๅฎ‰ๅ…จ

// ้…็ฝฎๅŠ ๅฏ†ๆœๅŠก
@Service
public class ConfigEncryptionService {
    
    private final EncryptionProvider encryptionProvider;
    
    // ๅŠ ๅฏ†้…็ฝฎๅ€ผ
    public String encrypt(String plainText) {
        return encryptionProvider.encrypt(plainText);
    }
    
    // ่งฃๅฏ†้…็ฝฎๅ€ผ
    public String decrypt(String cipherText) {
        return encryptionProvider.decrypt(cipherText);
    }
    
    // ๅŠ ๅฏ†ๆ•ดไธช้…็ฝฎๆ–‡ไปถ
    public ConfigSnapshot encryptSensitiveConfigs(ConfigSnapshot config, 
            List<String> sensitiveKeys) {
        
        Map<String, String> encryptedConfigs = new HashMap<>();
        
        for (Map.Entry<String, String> entry : config.getConfigs().entrySet()) {
            if (isSensitive(entry.getKey(), sensitiveKeys)) {
                encryptedConfigs.put(entry.getKey(), encrypt(entry.getValue()));
            } else {
                encryptedConfigs.put(entry.getKey(), entry.getValue());
            }
        }
        
        return new ConfigSnapshot(config.getVersion(), encryptedConfigs);
    }
    
    private boolean isSensitive(String key, List<String> sensitiveKeys) {
        return sensitiveKeys.stream()
            .anyMatch(pattern -> key.matches(pattern));
    }
}

// ๆ•ๆ„Ÿ้…็ฝฎ Key ็š„ๅŒน้…่ง„ๅˆ™
List<String> sensitivePatterns = Arrays.asList(
    ".*password.*",
    ".*secret.*",
    ".*token.*",
    ".*key.*",
    ".*credential.*"
);
// KMS ้›†ๆˆ็คบไพ‹
@Service
public class KMSConfigEncryption implements ConfigEncryptionService {
    
    private final KmsClient kmsClient;
    private final String keyId;
    
    @Override
    public String encrypt(String plainText) {
        EncryptRequest request = EncryptRequest.builder()
            .keyId(keyId)
            .plaintext(ByteBuffer.wrap(plainText.getBytes()))
            .build();
        
        EncryptResult result = kmsClient.encrypt(request);
        return Base64.getEncoder().encodeToString(
            result.ciphertextBlob().array());
    }
    
    @Override
    public String decrypt(String cipherText) {
        ByteBuffer ciphertextBlob = ByteBuffer.wrap(
            Base64.getDecoder().decode(cipherText));
        
        DecryptRequest request = DecryptRequest.builder()
            .ciphertextBlob(ciphertextBlob)
            .build();
        
        DecryptResult result = kmsClient.decrypt(request);
        return new String(result.plaintext().array());
    }
}

6.3 ่ฎฟ้—ฎๆŽงๅˆถ

// ๆƒ้™ๆจกๅž‹่ฎพ่ฎก
public enum Permission {
    CONFIG_READ,        // ่ฏปๅ–้…็ฝฎ
    CONFIG_WRITE,       // ไฟฎๆ”น้…็ฝฎ
    CONFIG_PUBLISH,     // ๅ‘ๅธƒ้…็ฝฎ
    CONFIG_ROLLBACK,    // ๅ›žๆปš้…็ฝฎ
    NAMESPACE_CREATE,   // ๅˆ›ๅปบๅ‘ฝๅ็ฉบ้—ด
    NAMESPACE_DELETE,   // ๅˆ ้™คๅ‘ฝๅ็ฉบ้—ด
}

public enum Role {
    VIEWER(Permission.CONFIG_READ),
    DEVELOPER(Permission.CONFIG_READ, Permission.CONFIG_WRITE),
    ADMIN(Permission.values());
    
    private final Set<Permission> permissions;
    
    Role(Permission... permissions) {
        this.permissions = EnumSet.copyOf(Arrays.asList(permissions));
    }
    
    public boolean hasPermission(Permission permission) {
        return permissions.contains(permission);
    }
}

// ๆƒ้™ๆฃ€ๆŸฅๆ‹ฆๆˆชๅ™จ
@Aspect
@Component
public class PermissionAspect {
    
    @Autowired
    private UserService userService;
    
    @Before("@annotation(requirePermission)")
    public void checkPermission(JoinPoint joinPoint, RequirePermission requirePermission) {
        // ่Žทๅ–ๅฝ“ๅ‰็”จๆˆท
        String userId = SecurityContextHolder.getCurrentUserId();
        User user = userService.getUser(userId);
        
        // ๆฃ€ๆŸฅๆƒ้™
        Permission required = requirePermission.value();
        if (!user.getRole().hasPermission(required)) {
            throw new ForbiddenException(
                "User " + userId + " does not have permission: " + required);
        }
        
        // ่ฎฐๅฝ•ๅฎก่ฎกๆ—ฅๅฟ—
        auditService.log(userId, required, joinPoint.getSignature().toShortString());
    }
}

// ไฝฟ็”จ็คบไพ‹
@RestController
@RequestMapping("/api/v1/configs")
public class ConfigController {
    
    @RequirePermission(Permission.CONFIG_READ)
    @GetMapping("/{namespace}/{app}")
    public ConfigResponse getConfig(@PathVariable String namespace, 
                                    @PathVariable String app) {
        return configService.getConfig(namespace, app);
    }
    
    @RequirePermission(Permission.CONFIG_PUBLISH)
    @PostMapping("/{namespace}/{app}/release")
    public ReleaseResponse releaseConfig(@PathVariable String namespace,
                                         @PathVariable String app,
                                         @RequestBody ReleaseRequest request) {
        return configService.release(namespace, app, request);
    }
}

6.4 ๅฎก่ฎกไธŽ็›‘ๆŽง

// ๅฎก่ฎกๆ—ฅๅฟ—ๅฎžไฝ“
@Entity
public class AuditLog {
    @Id
    @GeneratedValue
    private Long id;
    
    private String userId;          // ๆ“ไฝœไบบ
    private String action;          // ๆ“ไฝœ็ฑปๅž‹
    private String resourceType;    // ่ต„ๆบ็ฑปๅž‹๏ผˆ้…็ฝฎ/ๅ‘ฝๅ็ฉบ้—ด/ๅบ”็”จ๏ผ‰
    private String resourceId;      // ่ต„ๆบ ID
    private String detail;          // ๆ“ไฝœ่ฏฆๆƒ…
    private String clientIp;        // ๅฎขๆˆท็ซฏ IP
    private Instant timestamp;      // ๆ“ไฝœๆ—ถ้—ด
    
    // ๆ•ๆ„Ÿๅญ—ๆฎต่‡ชๅŠจๆ‰“็ 
    public String getDetail() {
        return maskSensitiveInfo(detail);
    }
    
    private String maskSensitiveInfo(String text) {
        return text.replaceAll("(password|secret|token)=[^&\\s]+", "$1=***");
    }
}

// ๅฎก่ฎกๆœๅŠก
@Service
public class AuditService {
    
    @Autowired
    private AuditLogRepository auditLogRepository;
    
    public void log(String userId, String action, String resourceType, 
                   String resourceId, String detail) {
        
        AuditLog log = new AuditLog();
        log.setUserId(userId);
        log.setAction(action);
        log.setResourceType(resourceType);
        log.setResourceId(resourceId);
        log.setDetail(detail);
        log.setClientIp(getCurrentClientIp());
        log.setTimestamp(Instant.now());
        
        auditLogRepository.save(log);
        
        // ๆ•ๆ„Ÿๆ“ไฝœๅฎžๆ—ถๅ‘Š่ญฆ
        if (isSensitiveOperation(action)) {
            alertService.sendAlert(log);
        }
    }
    
    private boolean isSensitiveOperation(String action) {
        return Arrays.asList("CONFIG_PUBLISH", "CONFIG_DELETE", "ROLLBACK")
            .contains(action);
    }
}
# ๅ‘Š่ญฆ่ง„ๅˆ™้…็ฝฎ
alert:
  rules:
    - name: ็”Ÿไบง็Žฏๅขƒ้…็ฝฎๅ˜ๆ›ด
      condition:
        env: prod
        action: CONFIG_PUBLISH
      channels:
        - type: feishu
          webhook: ${FEISHU_WEBHOOK_URL}
          at_users: ["ou_xxx", "ou_yyy"]
      
    - name: ๆ•ๆ„Ÿ้…็ฝฎไฟฎๆ”น
      condition:
        config_key_pattern: ".*(password|secret|token).*"
        action: CONFIG_WRITE
      channels:
        - type: email
          recipients: ["security@company.com"]
        - type: sms
          phones: ["13800138000"]
    
    - name: ้žๅทฅไฝœๆ—ถ้—ดๅ˜ๆ›ด
      condition:
        time_range: "22:00-08:00"
        action: CONFIG_PUBLISH
      severity: HIGH
      channels:
        - type: phone_call
          phones: ["13800138000"]

ไธƒใ€ๅฎžๆˆ˜ๆกˆไพ‹๏ผš็”ตๅ•†ๅนณๅฐ้…็ฝฎไธญๅฟƒ่ฝๅœฐ

7.1 ไธšๅŠก่ƒŒๆ™ฏ

ๆŸ็”ตๅ•†ๅนณๅฐๆœ‰ 50+ ๅพฎๆœๅŠก๏ผŒ้…็ฝฎ็ฎก็†้ขไธดไปฅไธ‹ๆŒ‘ๆˆ˜๏ผš

7.2 ่งฃๅ†ณๆ–นๆกˆ

ๅŸบไบŽ่‡ช็ ”็š„ configcenter ๆœๅŠกๆญๅปบ้…็ฝฎไธญๅฟƒ๏ผŒ็†็”ฑ๏ผš

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                    ็”Ÿไบง็Žฏๅขƒ                            โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚
โ”‚  โ”‚          configcenter ๆœๅŠก                      โ”‚   โ”‚
โ”‚  โ”‚    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚   โ”‚
โ”‚  โ”‚    โ”‚ Portal   โ”‚  โ”‚ Admin    โ”‚  โ”‚ Config   โ”‚  โ”‚   โ”‚
โ”‚  โ”‚    โ”‚ Server   โ”‚  โ”‚ Service  โ”‚  โ”‚ Service  โ”‚  โ”‚   โ”‚
โ”‚  โ”‚    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚   โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚
โ”‚                        โ”‚                              โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚  โ”‚            MySQL Master-Slave                โ”‚    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚

โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                  ้ž็”Ÿไบง็Žฏๅขƒ๏ผˆๅ…ฑไบซ๏ผ‰                     โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚
โ”‚  โ”‚          configcenter ๆœๅŠก                      โ”‚   โ”‚
โ”‚  โ”‚    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚   โ”‚
โ”‚  โ”‚    โ”‚ Portal   โ”‚  โ”‚ Admin    โ”‚  โ”‚ Config   โ”‚  โ”‚   โ”‚
โ”‚  โ”‚    โ”‚ Server   โ”‚  โ”‚ Service  โ”‚  โ”‚ Service  โ”‚  โ”‚   โ”‚
โ”‚  โ”‚    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚   โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚
โ”‚         ๅ‘ฝๅ็ฉบ้—ด๏ผšdev / test / uat                    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
# ๅŽŸๆฅ็š„ๆ•ฃไนฑ้…็ฝฎ
# order-service/application.properties
db.url=jdbc:mysql://localhost:3306/order_db
db.username=root
db.password=123456
redis.host=localhost
timeout.ms=5000
feature.new_ui=true
logging.level=DEBUG

# ่ฟ็งปๅŽ็š„็ป“ๆž„ๅŒ–้…็ฝฎ
# order-service/application.yml๏ผˆๅ…ฌๅ…ฑ้…็ฝฎ๏ผ‰
spring:
  datasource:
    url: ${db.url}
    username: ${db.username}
    password: ${db.password}

# order-service/db.yml๏ผˆๆ•ฐๆฎๅบ“้…็ฝฎ๏ผ‰
db:
  url: jdbc:mysql://${DB_HOST}:3306/order_db
  username: order_user
  password: cipher:encrypted_password_here  # ๅŠ ๅฏ†ๅญ˜ๅ‚จ
  pool:
    max_size: 100
    min_idle: 10

# order-service/feature-flags.yml๏ผˆๅŠŸ่ƒฝๅผ€ๅ…ณ๏ผ‰
features:
  new_ui: true
  new_payment: false
  recommendation_v2: true

# order-service/timeout.yml๏ผˆ่ถ…ๆ—ถ้…็ฝฎ๏ผ‰
timeout:
  db_query_ms: 1000
  external_api_ms: 5000
  cache_ms: 100
// ๅŽŸๆฅ็š„้…็ฝฎ่ฏปๅ–ๆ–นๅผ
@Configuration
public class OldConfig {
    @Value("${timeout.ms}")
    private int timeout;
    
    @Value("${db.url}")
    private String dbUrl;
}

// ๆ”น้€ ๅŽ๏ผšไฝฟ็”จ้…็ฝฎไธญๅฟƒ SDK
@Configuration
@EnableConfigCenterConfig({"application", "db", "feature-flags", "timeout"})
public class NewConfig {
    
    @Autowired
    private Config dbConfig;
    
    @Autowired
    private Config featureConfig;
    
    @ConfigChangeListener("db")
    private void onDbConfigChange(ConfigChangeEvent event) {
        // ๆ•ฐๆฎๅบ“้…็ฝฎๅ˜ๆ›ด๏ผŒๅŠจๆ€่ฐƒๆ•ด่ฟžๆŽฅๆฑ 
        if (event.isChanged("db.pool.max_size")) {
            int newSize = dbConfig.getIntProperty("db.pool.max_size", 100);
            dataSourcePool.resize(newSize);
            log.info("Database pool size changed to: {}", newSize);
        }
    }
    
    @Bean
    @RefreshScope  // ๆ”ฏๆŒ้…็ฝฎ็ƒญๆ›ดๆ–ฐ
    public DataSource dataSource() {
        return DataSourceBuilder.create()
            .url(dbConfig.getProperty("db.url", ""))
            .username(dbConfig.getProperty("db.username", ""))
            .password(dbConfig.getProperty("db.password", ""))
            .build();
    }
}
# 1. ๅœจ้…็ฝฎไธญๅฟƒไฟฎๆ”น้…็ฝฎ
# 2. ้€‰ๆ‹ฉ็ฐๅบฆๅฎžไพ‹๏ผˆ3 ๅฐไธญ็š„ 1 ๅฐ๏ผ‰
# 3. ๅ‘ๅธƒๅˆฐ็ฐๅบฆๅฎžไพ‹
# 4. ่ง‚ๅฏŸ็›‘ๆŽงๆŒ‡ๆ ‡ 10 ๅˆ†้’Ÿ
# 5. ๅฆ‚ๆžœๆญฃๅธธ๏ผŒๅ…จ้‡ๅ‘ๅธƒ
# 6. ๅฆ‚ๆžœๅผ‚ๅธธ๏ผŒไธ€้”ฎๅ›žๆปš

7.3 ่ฝๅœฐๆ•ˆๆžœ

7.4 ๆœ€ไฝณๅฎž่ทตๆ€ป็ป“

  1. ้…็ฝฎๅˆ†็ฑป็ฎก็†๏ผšๅฐ†้…็ฝฎๆŒ‰ๅŠŸ่ƒฝๆจกๅ—ๆ‹†ๅˆ†๏ผˆๆ•ฐๆฎๅบ“ใ€็ผ“ๅญ˜ใ€ไธšๅŠก่ง„ๅˆ™็ญ‰๏ผ‰
  2. ๆ•ๆ„Ÿ้…็ฝฎๅŠ ๅฏ†๏ผšๆ‰€ๆœ‰ๅฏ†็ ใ€ๅฏ†้’ฅๅฟ…้กปๅŠ ๅฏ†ๅญ˜ๅ‚จ
  3. ็ฐๅบฆๅ‘ๅธƒ๏ผš้…็ฝฎๅ˜ๆ›ดๅฟ…้กป็ฐๅบฆ้ชŒ่ฏ
  4. ๆœฌๅœฐ็ผ“ๅญ˜๏ผšๅฎขๆˆท็ซฏๅฟ…้กปๅฎž็Žฐๆœฌๅœฐ็ผ“ๅญ˜๏ผŒไฟ่ฏ้ซ˜ๅฏ็”จ
  5. ๆƒ้™ๆœ€ๅฐๅŒ–๏ผšๅบ”็”จๅช่ฏปๅ–่‡ชๅทฑ้œ€่ฆ็š„้…็ฝฎ
  6. ๅฎก่ฎกๅฎŒๆ•ด๏ผšๆ‰€ๆœ‰้…็ฝฎๅ˜ๆ›ดๅฟ…้กป่ฎฐๅฝ•ๅฎก่ฎกๆ—ฅๅฟ—

ๅ…ซใ€ๆ€ป็ป“

้…็ฝฎไธญๅฟƒๆ˜ฏ็ŽฐไปฃๅพฎๆœๅŠกๆžถๆž„็š„ๅŸบ็ก€่ฎพๆ–ฝไน‹ไธ€๏ผŒๅฎƒ็š„ๆ ธๅฟƒไปทๅ€ผๅœจไบŽ๏ผš

้…็ฝฎไธญๅฟƒไธไป…ไป…ๆ˜ฏไธ€ไธชๅทฅๅ…ท๏ผŒๆ›ดๆ˜ฏไธ€็ง้…็ฝฎ็ฎก็†็š„็†ๅฟตใ€‚ๅฎƒ่ฎฉ้…็ฝฎๆˆไธบไธ€็ญ‰ๅ…ฌๆฐ‘๏ผŒ่ฎฉ้…็ฝฎ็ฎก็†ไปŽ"ๆ‰‹ๅทฅๆ—ถไปฃ"่ฟ›ๅŒ–ๅˆฐ"ๅทฅไธšๆ—ถไปฃ"ใ€‚




๐Ÿ’ฌ ่ฏ„่ฎบ (0)

0/500
ๆŽ’ๅบ๏ผš