• Chào bạn, hãy đăng ký hoặc đăng nhập để tham gia cùng bọn mình và sử dụng được đầy đủ chức năng của diễn đàn :).
anhcraft

Bukkit [Optimization] Các cách tổ chức, load, save dữ liệu với file

anhcraft

Thành viên BQT
DEVELOPER
THÀNH VIÊN
Tham gia
18/09/2016
Bài viết
3,165
Topic tiếp theo về chuyên mục Optimization :3 chúng ta sẽ bàn tới các cách tổ chức, load & save dữ liệu với file.

Đối với các sv vừa và nhỏ (cỡ vn chẳng hạn) thì vấn đề này có vẻ ko quan trọng lắm :)) Nhưng mà khi sv càng phát triển thì cần phải optimize triệt để (cả server lẫn plugin) nếu ko muốn bị giật lag... Chuyện tổ chức, load & save dữ liệu rất quan trọng vì giúp tiết kiệm khá nhiều tài nguyên server (cpu, disk, vvv), dễ scale khi playerbase tăng lên, vvv.

Các kiểu dữ liệu thường gặp:
- Dạng text như JSON, XML, YAML: dữ liệu dạng key-value như thế này có thể đọc được dễ dàng, chỉnh sửa dc, nhưng lại ko tối ưu lắm:
  • Tốn dung lượng lưu trữ (như JSON thì có "{...}", XML thì "<....>", YAML thì lưu từng cặp key-value trên 1 dòng :'')
  • Read mất thời gian: lý do là phải tuân theo 1 format nhất định. Khi read là chắc chắn nó sẽ check này check nọ, k phải đọc từ đầu đến cuối mà có thể đọc ngược nữa :v
- Dạng binary (ko biết gọi tên sao nữa?): vd bạn đọc file này bằng một cái text editor sẽ thấy các chữ cái + số mix với các kí tự đặc biệt chưa bao giờ gặp, do nó k có 1 format rõ ràng, đúng chuẩn. Chẳng hạn như JSON đi là tổ chức dạng cây, mục mẹ - con, trong mục là có các cặp khoá-giá trị.... nghe là thấy có quy tắc rõ ràng rồi; còn mấy file dạng binary này nó ko có đâu
  • Đỡ tốn dung lượng: vd để lưu kiểu boolean đi thì thông thường sẽ ghi là true/false (là 4-5 byte); nhưng ta có thể tối ưu bằng cách lấy 8 giá trị lưu vào 1 byte, bit 1 là true, bit 0 là false. vậy là tối ưu hơn rất nhiều
  • Đỡ bị lỗi? í t là ở dạng text thì mn thường có sở thích tò mò, edit vớ vẩn... còn dạng binary thì đọc còn k dc ai dám edit =))
Nói sơ sơ v dc rồi... Do t cũng k phải là chuyên gia về 3 cái này... :-"

Tổ chức dữ liệu sao cho hợp lí?
Dữ liệu nên lưu theo 1 quy tắc để dễ đọc, dễ viết. Ko phải đến lúc cần cái này là phải đọc hàng loạt dữ liệu, tối ưu là chỉ cần đọc những cái cần thiết.
Ví dụ:
- Để lưu file log, thì nên lưu vào 1 file .txt là đủ, mỗi cái log là 1 dòng..
- Để làm 1 plugin đua top thì đơn giản nhất là lưu mỗi player 1 file dữ liệu (tránh gộp chung); còn nâng cao hơn thì có thêm 1 file để lưu top gần nhất.
- Để làm 1 plugin dạng claim chunk như towny, faction,vv thì lưu mỗi file là 1 chunk (16x16x256 block), giống Minecraft vậy...

Tối ưu việc load dữ liệu từ file vào bộ nhớ:
Thông thường thì mình thấy các plugin sẽ có 1 file chứa dữ liệu, khi plugin bật là load toàn bộ vào plugin. Cách này thì đơn giản rồi, nhưng mà sẽ gây nhiều vấn đề nếu lượng dữ liệu lớn, hoặc load liên tục khi server đã có người chơi....
Điều cần thiết là phải chia nhỏ lượng dữ liệu này lại (đã nói ở mục trên). Ví dụ chia theo player, theo chunk, theo world, vvv
Tránh load toàn bộ mà chỉ load khi cần thiết, ví dụ:
- Khi ai đó join server thì load dữ liệu của người đó
- Khi 1 chunk dc load thì load dữ liệu chunk đó
Ngoài ra, quy tắc nữa là đã load rồi thì đừng load lại, ý t là:
- Đừng nhét cái dòng load vào 1 cái loop hay 1 cái task.
- Lưu config đã load vào 1 biến, hoặc có thể parse thành objects

Ví dụ với code này, nên lưu config đã load vào 1 biến....:
Java:
player.sendMessage(loadConfig().getString("message"));
player.getInventory().setItem(loadConfig().getInt("slot"), null);

Và đừng load cái gì ko cần phải load, như log chẳng hạn (load log làm gì :3)

Tối ưu việc save dữ liệu từ file vào bộ nhớ:
Giống như load, chỉ nên save dữ liệu chỉ khi cần.
- Khi ai đó quit server thì save dữ liệu
- Khi 1 chunk dc unload thì save dữ liệu
Đặc biệt tránh save liên tục.... T đã từng gặp rất nhiều plugin có vấn đề này, đó là save liên tục dữ liệu trong 1 cái loop hay 1 cái timer task.
Java:
for(Player player: players) {
   getConfig().set(player.getName(), 69);
   saveConfig(); // why u dont put this shiet outside the loop?
}

Giải pháp là delay cái save lại :3 Có rất nhiều cách như tạo task delay tầm 3 - 5s, hoặc làm cái queue, vvv nhiều kiểu

Tối ưu chung cho cả 2:
Dùng async khi thực hiện các tác vụ I/O (khá quan trọng nhé :v)

Một vài ví dụ thực tế:

Plugin Report - tố cáo hack, cheat:
Về plugin dạng này thì ko có gì đặc biệt lắm :D Dữ liệu cũng ko phải là lớn, có thể lưu dưới dạng file yaml cho nhanh gọn.
Code:
Java:
public class Report {
    private UUID owner;
    private UUID player;
    private String reason;

    public Report(UUID owner, UUID player, String reason) {
        this.owner = owner;
        this.player = player;
        this.reason = reason;
    }

    public UUID getOwner() {
        return owner;
    }

    public UUID getPlayer() {
        return player;
    }

    public String getReason() {
        return reason;
    }
}
Java:
public final class LearnPlugins extends JavaPlugin {
    private final List<Report> reports = new ArrayList<>();
    private FileConfiguration conf;

    public void loadData() {
        reports.clear(); // reset
        conf.getKeys(false).forEach(s -> {
            ConfigurationSection sec = getConfig().getConfigurationSection(s);
            reports.add(new Report(
                    UUID.fromString(sec.getString("owner")),
                    UUID.fromString(sec.getString("player")),
                    sec.getString("reason")
            ));
        });
    }

    public void saveData() {
        conf = new YamlConfiguration(); // reset
        for(int i = 0; i < reports.size(); i++){
            Report report = reports.get(i);
            ConfigurationSection sec = new YamlConfiguration();
            sec.set("owner", report.getOwner().toString());
            sec.set("player", report.getPlayer().toString());
            sec.set("reason", report.getReason());
            conf.set(String.valueOf(i), sec);
        }
        try {
            conf.save(new File(getDataFolder(), "config.yml"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onEnable() {
        saveDefaultConfig();
        reloadConfig();

        conf = getConfig();
        loadData();
    }

    @Override
    public void onDisable() {
        saveData();
    }
}
Ở đây, mình lưu toàn bộ tố cáo vào 1 file duy nhất :3 load khi server bật & save khi server tắt.

Xong rồi tự nhiên mình nghĩ: "Lỡ server crash hoặc bị mất điện đột ngột thì sao mà save dc? :v" thế là phải thêm cái task để save :3
Java:
//....................

    @Override
    public void onEnable() {
        //....................

        new BukkitRunnable() {
            @Override
            public void run() {
                saveData();
            }
        }.runTaskTimerAsynchronously(this, 0, 100); // task async chay moi 5s (100 ticks)
    }
  
//....................
Nhớ task async nhé :D
Nhưng mà code này chưa hoàn toàn tối ưu, vì dữ liệu dc lưu vào 1 file...

Giả sử plugin chỉ cho phép xem, sửa các tố cáo của 1 player nhất định, thì mình có thể chia dữ liệu theo từng player. Vậy code có thể sửa lại như thế này:
Java:
public final class LearnPlugins extends JavaPlugin implements Listener {
    // HashMultimap<K, V> = HashMap<K, Collection<V>>
    private final Multimap<UUID, Report> reports = HashMultimap.create();

    public void loadData(UUID player) {
        reports.removeAll(player); // reset

        File file = new File(getDataFolder(), player.toString() + ".yml");
        // chỉ load nếu đã có file
        if(file.exists()) {
            YamlConfiguration conf = YamlConfiguration.loadConfiguration(file);
            conf.getKeys(false).forEach(s -> {
                ConfigurationSection sec = getConfig().getConfigurationSection(s);
                reports.put(player, new Report(
                        UUID.fromString(sec.getString("owner")),
                        UUID.fromString(sec.getString("player")),
                        sec.getString("reason")
                ));
            });
        }
    }

    public void saveData(UUID player) {
        File file = new File(getDataFolder(), player.toString() + ".yml");
        try {
            //noinspection ResultOfMethodCallIgnored
            file.createNewFile(); // ko cần phải check exist() !!!
        } catch (IOException e) {
            e.printStackTrace();
        }
        YamlConfiguration conf = new YamlConfiguration(); // reset
        List<Report> list = new ArrayList<>(reports.get(player));
        for(int i = 0; i < list.size(); i++){
            Report report = list.get(i);
            ConfigurationSection sec = new YamlConfiguration();
            sec.set("owner", report.getOwner().toString());
            sec.set("player", report.getPlayer().toString());
            sec.set("reason", report.getReason());
            conf.set(String.valueOf(i), sec);
        }
        try {
            conf.save(file);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
  
    @EventHandler
    public void join(PlayerJoinEvent event) {
        // load khi member join
        loadData(event.getPlayer().getUniqueId());
    }

    @EventHandler
    public void quit(PlayerQuitEvent event) {
        // save khi quit
        saveData(event.getPlayer().getUniqueId());
        // remove khoi memory
        reports.removeAll(event.getPlayer().getUniqueId());
    }

    @Override
    public void onEnable() {
        saveDefaultConfig();
        reloadConfig();

        getServer().getPluginManager().registerEvents(this, this);
      
        new BukkitRunnable() {
            @Override
            public void run() {
                reports.keys().forEach(u -> saveData(u));
            }
        }.runTaskTimerAsynchronously(this, 0, 100); // task async chay moi 5s (100 ticks)
    }

    @Override
    public void onDisable() {
        reports.keys().forEach(this::saveData);
    }
}
Với code này, dữ liệu chỉ dc load & save khi cần :v đó là load khi 1 player vào & save khi nó quit.

Tuy nhiên nếu bạn ko muốn lưu theo người chơi thì có thể lưu theo ngày tháng năm :D Tất nhiên là sẽ ko load toàn bộ mà sẽ ưu tiên load của ngày hôm nay trước, nếu cần thì mới load từ các ngày khác.
Nếu cần thêm 1 cái GUI hiển thị các ngày có report đi, thì mình có thể làm như sau:
- Mở menu, đọc thư mục "data" để lấy tên các thư mục (tương ứng các năm)
- Click vào 1 năm thì đọc thư mục "data/<năm>/" để lấy tên các thư mục tương ứng các tháng
- Tương tự như trên khi click vào 1 tháng, mình sẽ dc danh sách các ngày có report
- Click vào 1 ngày, lúc này file nào đó sẽ dc load :v Vậy là mình dc danh sách report có trong ngày đó.
Cách load này còn gọi là lazy-loading ấy <(") ai làm website chắc biết.

Plugin log block dc phá/đặt:
Phân tích plugin này thì.. dữ liệu sẽ rất nặng :D do player liên tục đặt/phá block mà... Thế nên mình ưu tiên dùng file .txt cho nó nhẹ, chỉ save thôi chứ ko load
Ngoài ra do mình chỉ có nhiệm vụ save (lưu vào cuối file thôi), nên chọn mode là append.
Nếu muốn tối ưu, dễ dàng xem lại, thì có thể phân chia log theo ngày, tháng, năm, theo world, vvv
Code:
Java:
public final class LearnPlugins extends JavaPlugin implements Listener {
    private Path filePath;
  
    private void log(String text) {
        try (BufferedWriter writer = Files.newBufferedWriter(filePath, StandardCharsets.UTF_8, StandardOpenOption.APPEND, StandardOpenOption.CREATE)) {
            writer.write(text + "\n");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @EventHandler
    public void placeBlock(BlockPlaceEvent event) {
        Block b = event.getBlock();
        log(String.format("%s đã đặt block %s tại vị trí %s %s %s %s", event.getPlayer().getName(), b.getType().name().toLowerCase(), b.getWorld().getName(), b.getX(), b.getY(), b.getZ()));
    }

    @EventHandler
    public void breakBlock(BlockBreakEvent event) {
        Block b = event.getBlock();
        log(String.format("%s đã phá block %s tại vị trí %s %s %s %s", event.getPlayer().getName(), b.getType().name().toLowerCase(), b.getWorld().getName(), b.getX(), b.getY(), b.getZ()));
    }

    @Override
    public void onEnable() {
        getServer().getPluginManager().registerEvents(this, this);
      
        filePath = new File(getDataFolder(), "logs.txt").toPath();
    }
}
Code này có vấn đề rất lớn đó là phải liên tục save dữ liệu vào disk :3 Mà member thì lại có sở thích spam, thế nên là chạy code trên chỉ có banh server.

Thế nên code này cần fix lại là tránh lưu liên tục mà lưu theo từng cụm 10 log (tức là làm cái cache chứa 10 log, đủ 10 cái thì save 1 lần :D)
Để chứng minh hiệu quả của cách fix trên mình đã làm 1 test so sánh: lưu 1 chuỗi 1000 lần, so với lưu 1 lần nhưng chuỗi dài 1000 lần thì khác nhau thế nào...
Java:
public class a {
    private static final String STR = "tichcucquaytayvanmayseden";

    private static void log(String str) {
        try (BufferedWriter writer = Files.newBufferedWriter(new File("logs.txt").toPath(), StandardCharsets.UTF_8, StandardOpenOption.APPEND, StandardOpenOption.CREATE)) {
            writer.write(str);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        long time = System.currentTimeMillis();
        for(int i = 0; i < 1000; i++){
            log(STR);
        }
        System.out.println(System.currentTimeMillis() - time); // ~50-70 s

        time = System.currentTimeMillis();
        StringBuilder stringBuilder = new StringBuilder();
        for(int i = 0; i < 1000; i++){
            stringBuilder.append(STR);
        }
        log(stringBuilder.toString());
        System.out.println(System.currentTimeMillis() - time); // ~3-5s
    }
}
Cả 3 lần test đều cho thấy việc viết 1 chuỗi dài 1000 lần sẽ nhanh hơn x10.

Giờ áp dụng cách này với plugin thôi :D
Java:
    private List<String> cache = new ArrayList<>(10);
    private void log(String text) {
        cache.add(text);
        if(cache.size() == 10) { // đủ 10 cái thì lưu
            StringBuilder str = new StringBuilder();
            for(String s : cache) {
                str.append(s).append('\n'); // ghép thành 1 chuỗi
            }
            try (BufferedWriter writer = Files.newBufferedWriter(filePath, StandardCharsets.UTF_8, StandardOpenOption.APPEND, StandardOpenOption.CREATE)) {
                writer.write(str.toString());
            } catch (IOException e) {
                e.printStackTrace();
            }
            cache.clear();
        }
    }

    // ..............................................


..............................................................................................................................................
Nâng level lên 1 chút :))
VD một plugin claim chunk cho 1 server lớn chẳng hạn
Có vài yếu tố phải nhắc tới:
- Dữ liệu sẽ lưu theo chunk
- Read dữ liệu khi 1 chunk dc load, để vào cái list (hoặc map)
- Save + xoá khỏi list/map khi chunk bị unload
Tuy nhiên do ở server lớn nên sẽ bị các vấn đề đó là:
- Lượng chunk dc load sẽ rất lớn (do có nhiều member)
- Member sẽ di chuyển liên tục -> load & save liên tục
(Giả sử lượng dữ liệu về chunk đó rất lớn đi, ví dụ bao gồm các thành viên trong chunk, log các hoạt động, bank, jail dành riêng cho chunk đó, bla bla)

Thì chúng ta phải thiết kế code sao cho việc load & save chậm lại, giả sử memory trong server là đủ để chứa (tầm 500MB cho mớ dữ liệu đó)
Một vài ý tưởng:
1. Khi 1 chunk dc load, thay vì chỉ lấy dữ liệu của chunk đó, ta có thể lấy thêm 4 hoặc 8 chunk xung quanh (4 cái ở trên, dưới, trái phải, thêm 4 cái ở 4 góc)
2. Có thể load trước một vài chunk quan trọng, chẳng hạn dựa trên tần suất online của chunk đó (giả sử chunk X có 10 member thường xuyên online thì ưu tiên load chunk này trước)
3. Khi 1 chunk bị unload, thay vì save rồi xoá liền dữ liệu thì chúng ta có thể làm 1 cái countdown, nếu sau X giây không có ai vào lại chunk thì dữ liệu của nó sẽ dc save + xoá
4. Có thể thêm điều kiện để xoá dữ liệu đó là chunk hiện tại và các chunk xung quanh ko có người chơi nào :v vd trong khoảng 4 - 8 chunk xung quanh
Code: <tự code đi :| >

OK viết vậy đủ rồi <(") ... ae tự tìm hiểu thêm. Ai thấy hay thì like, ai ko thấy hay thì thả tim.
 
Các kiểu dữ liệu thường gặp:
- Dạng text như JSON, XML, YAML: dữ liệu dạng key-value như thế này có thể đọc được dễ dàng, chỉnh sửa dc, nhưng lại ko tối ưu lắm:
  • Tốn dung lượng lưu trữ (như JSON thì có "{...}", XML thì "<....>", YAML thì lưu từng cặp key-value trên 1 dòng :'')
  • Read mất thời gian: lý do là phải tuân theo 1 format nhất định. Khi read là chắc chắn nó sẽ check này check nọ, k phải đọc từ đầu đến cuối mà có thể đọc ngược nữa :v
bạn mỗi lần execute bạn đọc 1 lần à ? ngta parse vứt trước vô ram rồi

còn về bản chất các file chunk thì nó là file bth đc nén gzip lại để đỡ tốn disk thôi <(")

muốn nhanh thì vứt lên database database nó indexing thông minh hơn <(")
 
bạn mỗi lần execute bạn đọc 1 lần à ? ngta parse vứt trước vô ram rồi

còn về bản chất các file chunk thì nó là file bth đc nén gzip lại để đỡ tốn disk thôi <(")

muốn nhanh thì vứt lên database database nó indexing thông minh hơn <(")
parse là đương nhiên phải làm rồi, nhưng mà còn tuỳ trường hợp nữa, vd cái file "chunk" đâu thể parse toàn bộ cùng 1 lúc dc
nén gzip là chuyện khác r, còn nó lm ra file chunk để chia nhỏ dữ liệu lại, tránh load quá nhiều vào ram
 
parse là đương nhiên phải làm rồi, nhưng mà còn tuỳ trường hợp nữa, vd cái file "chunk" đâu thể parse toàn bộ cùng 1 lúc dc
nén gzip là chuyện khác r, còn nó lm ra file chunk để chia nhỏ dữ liệu lại, tránh load quá nhiều vào ram
có ai load hết toàn bộ chunk vào bao h <(")
Mojang chỉ load chunk trong phạm vi n x n trong đó tâm là vị trí hiện tại của player
 
gotta use binary format
 
Similar content Most view Xem thêm
Back
Top Bottom