2026/01/19

CRDT

CDRT conflict-free replicated data type 無衝突複製資料類型是一種可以在網路中的多台電腦上複製的資料結構,每一個副本可獨立各自更新,不需要在副本之間進行協調,已透過數學確認可解決可能出現的不一致問題。

對於不同節點上的共用資料,如果在某一個節點上更新了資料,會產生不一致性,如果節點之間沒有協調更新的權限而產生資料衝突,就可能需要放棄一部分更新,以達到最終的一致性,分散式計算都集中在如何防止複製資料的並行更新問題上。

另一種方式 Optimistic Replication,是讓各節點都能做更新,允許所有更新都能同時執行,然後再來考慮如何合併解決衝突問題,CRDT 是一種資料結構,可在不同的副本上進行更新,最後透過合併解決衝突問題。

CRDT 種類

基於狀態的 CRDT 比較容易實作,但需要每個 CRDT 都要將狀態傳給其他副本,傳輸資料耗費較大,基於操作的 CRDT 只需要傳送更新的動作記錄。

基於操作的 CRDT

也稱為 交換性複製資料類型(commutative replicated data types,或CmRDT)

只傳輸更新操作來傳播狀態,必須確保操作過程都有傳遞給每一個資料副本,可不依照順序,但不能重複。

基於狀態的 CRDT

被稱為收斂複製資料類型(convergent replicated data types,或CvRDT)

必須將完整的本地端狀態,重送給其他副本。

State-Based LWW-Element-Graph CRDT

GitHub - juliuskrah/crdt-java: A minimal CRDT implementation 這是一個簡易的 java library,實作了 CvRDT,用 graph 方式,連結不同的節點。

當兩個節點沒有連線時,就是 split-brain 狀態,CRDT 可在節點連接後,讓不同的副本資料合併達到一致性。

這邊測試四種演算法

  • Grow-Only Set:會一直不斷增加的 Set,最終不同節點的 Set 會合併再一起

  • Increment-Only Counter: 結果會是所有節點的加總總和

  • PN Counter:結果是所有節點的 加總總和 - 減法總和

  • Last-Writer-Wins Register:在合併時,只會保留最新的異動內容。同時發生的操作會被丟棄

<!-- Experimental CRDT-implementations for the JVM -->
        <dependency>
            <groupId>com.netopyr.wurmloch</groupId>
            <artifactId>wurmloch-crdt</artifactId>
            <version>0.1.0</version>
        </dependency>
import com.netopyr.wurmloch.crdt.GCounter;
import com.netopyr.wurmloch.crdt.GSet;
import com.netopyr.wurmloch.crdt.LWWRegister;
import com.netopyr.wurmloch.crdt.PNCounter;
import com.netopyr.wurmloch.store.LocalCrdtStore;
import org.junit.Test;

import static org.junit.Assert.*;

public class CRDTTest {
    @Test
    public void gset() {
        LocalCrdtStore crdtStore1 = new LocalCrdtStore();
        LocalCrdtStore crdtStore2 = new LocalCrdtStore();
        crdtStore1.connect(crdtStore2);

        GSet<String> replica1 = crdtStore1.createGSet("fruit");
        GSet<String> replica2 = crdtStore2.<String>findGSet("fruit").get();

        replica1.add("apple");
        replica2.add("banana");

        // 確認replica1, replica2 都有 "apple" 及  “”banana
        assertTrue( replica1.contains("apple") );
        assertTrue( replica1.contains("banana") );
        assertTrue( replica2.contains("apple") );
        assertTrue( replica2.contains("banana") );

        // 刻意斷線
        crdtStore1.disconnect(crdtStore2);
        // 異動 replica1, replica2
        replica1.add("strawberry");
        replica2.add("pear");

        assertTrue( replica1.contains("strawberry") );
        assertFalse( replica2.contains("strawberry") );
        assertFalse( replica1.contains("pear") );
        assertTrue( replica2.contains("pear") );

        // 連線
        crdtStore1.connect(crdtStore2);

        assertTrue( replica1.contains("strawberry") );
        assertTrue( replica2.contains("strawberry") );
        assertTrue( replica1.contains("pear") );
        assertTrue( replica2.contains("pear") );
    }

    @Test
    public void gcounter() {
        // 結果會是所有節點的 sum
        LocalCrdtStore crdtStore1 = new LocalCrdtStore();
        LocalCrdtStore crdtStore2 = new LocalCrdtStore();
        crdtStore1.connect(crdtStore2);

        GCounter replica1 = crdtStore1.createGCounter("counter");
        GCounter replica2 = crdtStore2.findGCounter("counter").get();

        replica1.increment();
        replica2.increment(2L);

        assertEquals(3L, replica1.get());
        assertEquals(3L, replica2.get());

        // 斷線
        crdtStore1.disconnect(crdtStore2);

        replica1.increment(3L);
        replica2.increment(5L);

        assertEquals(6L, replica1.get());
        assertEquals(8L, replica2.get());

        // connect
        crdtStore1.connect(crdtStore2);
        assertEquals(11L, replica1.get());
        assertEquals(11L, replica2.get());
    }

    @Test
    public void pncounter() {
        // 結果是所有節點的 加總總和 - 減法總和
        LocalCrdtStore crdtStore1 = new LocalCrdtStore();
        LocalCrdtStore crdtStore2 = new LocalCrdtStore();
        crdtStore1.connect(crdtStore2);

        PNCounter replica1 = crdtStore1.createPNCounter("pncounter");
        PNCounter replica2 = crdtStore2.findPNCounter("pncounter").get();

        replica1.increment();
        replica2.decrement(2L);

        assertEquals(-1L, replica1.get());
        assertEquals(-1L, replica2.get());

        // disconnect
        crdtStore1.disconnect(crdtStore2);

        replica1.decrement(3L);
        replica2.increment(5L);

        assertEquals(-4L, replica1.get());
        assertEquals(4L, replica2.get());

        // connect
        crdtStore1.connect(crdtStore2);

        assertEquals(1L, replica1.get());
        assertEquals(1L, replica2.get());
    }

    @Test
    public void lwwregister() {
        // 在合併時,只會保留最新的異動內容。同時發生的操作會被丟棄
        LocalCrdtStore crdtStore1 = new LocalCrdtStore("crdt");
        LocalCrdtStore crdtStore2 = new LocalCrdtStore("crdt");
        crdtStore1.connect(crdtStore2);

        LWWRegister<String> replica1 = crdtStore1.createLWWRegister("lwwregister");
        LWWRegister<String> replica2 = crdtStore2.<String>findLWWRegister("lwwregister").get();

        replica1.set("apple");
        replica2.set("banana");

        assertEquals("banana", replica1.get());
        assertEquals("banana", replica2.get());

        //disconnect
        crdtStore1.disconnect(crdtStore2);

        replica1.set("strawberry");
        replica2.set("pear");

        assertEquals("strawberry", replica1.get());
        assertEquals("pear", replica2.get());

        // connect
        crdtStore1.connect(crdtStore2);

        // buggy:應該要是 pear
        assertEquals("strawberry", replica1.get());
        assertEquals("strawberry", replica2.get());
    }
}

References

無衝突複製資料類型 - 維基百科,自由的百科全書

# CRDT — 將非同步資料整合

CRDT - HackMD

Introduction to Conflict-Free Replicated Data Types | Baeldung

GitHub - juliuskrah/crdt-java: A minimal CRDT implementation

2026/01/12

JavaFX 08 webview

WebView 是一個可以嵌入網頁瀏覽功能的元件,使用內建的 WebKit 引擎來顯示 HTML 內容(包括 JavaScript、CSS 等)。這對於桌面應用程式需要嵌入網頁內容(如:顯示文件、使用 Web UI 元件)時非常有用。

但該 WebKit engine 不是最新版本,雖然支援 html5/css/javascript,但不支援新的標準,不支援多個分頁,沒有 js console 開發者工具,無法使用 react/vue 這些框架,不能跟 javafx node 直接互動(無法將 node 插入 html DOM)。

webview 裡面的頁面跟 js,可以跟 java code 互動

以下是一個雙向互動的 sample

js 可可

package javafx.webview;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import netscape.javascript.JSObject;

public class WebViewDemo extends Application {

    @Override
    public void start(Stage primaryStage) {
        WebView webView = new WebView();
        WebEngine webEngine = webView.getEngine();

        // 載入 HTML 字串
        String html = """
            <html>
            <head>
                <script>
                    function callJava() {
                        javaApp.showMessage("Hello from JavaScript!");
                    }

                    function fromJava(data) {
                        document.getElementById("result").innerText = "Java says: " + data;
                    }
                </script>
            </head>
            <body>
                <h2>JavaFX WebView 雙向互動</h2>
                <button onclick="callJava()">呼叫 Java 方法</button>
                <p id="result"></p>
            </body>
            </html>
            """;

        webEngine.loadContent(html);

        // 頁面載入完成後建立 bridge
        webEngine.getLoadWorker().stateProperty().addListener((obs, oldState, newState) -> {
            if (newState == javafx.concurrent.Worker.State.SUCCEEDED) {
                JSObject window = (JSObject) webEngine.executeScript("window");
                window.setMember("javaApp", new JavaBridge(webEngine));
            }
        });

        BorderPane root = new BorderPane(webView);
        Scene scene = new Scene(root, 800, 600);
        primaryStage.setScene(scene);
        primaryStage.setTitle("JavaFX WebView 雙向互動範例");
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }

    // Java Bridge 類別
    public static class JavaBridge {
        private final WebEngine webEngine;

        public JavaBridge(WebEngine webEngine) {
            this.webEngine = webEngine;
        }

        // JS 呼叫這個方法
        public void showMessage(String msg) {
            System.out.println("JavaScript 傳來訊息: " + msg);
            // Java 反呼叫 JS 的 function
            webEngine.executeScript("fromJava('收到: " + msg + "')");
        }
    }
}

2026/01/05

JavaFX 07 chart

用 javafx 繪製圖表的工具

  • 圓餅圖 PieChart 無需軸
  • 折線圖 LineChart<X,Y> 通常 CategoryAxis/NumberAxis
  • 面積圖 AreaChart<X,Y> 同折線圖
  • 長條圖 BarChart<X,Y> 通常 CategoryAxis/NumberAxis
  • 氣泡圖 BubbleChart<Number,Number> 用第三值表示半徑
  • 散點圖 ScatterChart<Number,Number> 點陣圖
  • 累積面積圖 StackedAreaChart<X,Y> 多組堆疊數據區域
  • 累積長條圖 StackedBarChart<X,Y> 長條堆疊比較
package javafx.chart;

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.chart.*;
import javafx.scene.control.ComboBox;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

//圖表類型    JavaFX 類別名稱    X/Y 軸類型
//圓餅圖    PieChart    無需軸
//折線圖    LineChart<X,Y>    通常 CategoryAxis/NumberAxis
//面積圖    AreaChart<X,Y>    同折線圖
//長條圖    BarChart<X,Y>    通常 CategoryAxis/NumberAxis
//氣泡圖    BubbleChart<Number,Number>    用第三值表示半徑
//散點圖    ScatterChart<Number,Number>    點陣圖
//累積面積圖    StackedAreaChart<X,Y>    多組堆疊數據區域
//累積長條圖    StackedBarChart<X,Y>    長條堆疊比較

public class AllChartsDemo extends Application {

    private BorderPane root;
    private ComboBox<String> chartSelector;

    @Override
    public void start(Stage primaryStage) {
        root = new BorderPane();
        root.setPadding(new Insets(10));

        chartSelector = new ComboBox<>();
        chartSelector.getItems().addAll(
                "PieChart", "LineChart", "AreaChart", "BarChart",
                "BubbleChart", "ScatterChart", "StackedAreaChart", "StackedBarChart"
        );
        chartSelector.setValue("PieChart");
        chartSelector.setOnAction(e -> updateChart(chartSelector.getValue()));

        root.setTop(chartSelector);
        updateChart(chartSelector.getValue());

        Scene scene = new Scene(root, 800, 600);
        primaryStage.setTitle("JavaFX Chart Selector Demo");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private void updateChart(String type) {
        switch (type) {
            case "PieChart":
                PieChart pieChart = new PieChart();
                pieChart.setTitle("Pie Chart");
                pieChart.getData().addAll(
                        new PieChart.Data("Java", 30),
                        new PieChart.Data("Python", 25),
                        new PieChart.Data("C++", 20),
                        new PieChart.Data("Kotlin", 15),
                        new PieChart.Data("Other", 10)
                );
                root.setCenter(pieChart);
                break;

            case "LineChart":
                CategoryAxis x1 = new CategoryAxis();
                NumberAxis y1 = new NumberAxis();
                LineChart<String, Number> lineChart = new LineChart<>(x1, y1);
                lineChart.setTitle("Line Chart");
                XYChart.Series<String, Number> lineSeries = new XYChart.Series<>();
                lineSeries.setName("Sales");
                lineSeries.getData().add(new XYChart.Data<>("Mon", 10));
                lineSeries.getData().add(new XYChart.Data<>("Tue", 20));
                lineSeries.getData().add(new XYChart.Data<>("Wed", 15));
                lineSeries.getData().add(new XYChart.Data<>("Thu", 25));
                lineSeries.getData().add(new XYChart.Data<>("Fri", 18));
                lineChart.getData().add(lineSeries);
                root.setCenter(lineChart);
                break;

            case "AreaChart":
                AreaChart<String, Number> areaChart = new AreaChart<>(new CategoryAxis(), new NumberAxis());
                areaChart.setTitle("Area Chart");
                XYChart.Series<String, Number> areaSeries = new XYChart.Series<>();
                areaSeries.setName("Traffic");
                areaSeries.getData().add(new XYChart.Data<>("Jan", 200));
                areaSeries.getData().add(new XYChart.Data<>("Feb", 300));
                areaSeries.getData().add(new XYChart.Data<>("Mar", 150));
                areaSeries.getData().add(new XYChart.Data<>("Apr", 250));
                areaChart.getData().add(areaSeries);
                root.setCenter(areaChart);
                break;

            case "BarChart":
                BarChart<String, Number> barChart = new BarChart<>(new CategoryAxis(), new NumberAxis());
                barChart.setTitle("Bar Chart");
                XYChart.Series<String, Number> barSeries = new XYChart.Series<>();
                barSeries.setName("Downloads");
                barSeries.getData().add(new XYChart.Data<>("Chrome", 60));
                barSeries.getData().add(new XYChart.Data<>("Firefox", 40));
                barSeries.getData().add(new XYChart.Data<>("Edge", 30));
                barChart.getData().add(barSeries);
                root.setCenter(barChart);
                break;

            case "BubbleChart":
                BubbleChart<Number, Number> bubbleChart = new BubbleChart<>(new NumberAxis(), new NumberAxis());
                bubbleChart.setTitle("Bubble Chart");
                XYChart.Series<Number, Number> bubbleSeries = new XYChart.Series<>();
                bubbleSeries.setName("Bubbles");
                bubbleSeries.getData().add(new XYChart.Data<>(10, 20, 5));
                bubbleSeries.getData().add(new XYChart.Data<>(15, 30, 10));
                bubbleSeries.getData().add(new XYChart.Data<>(25, 10, 7));
                bubbleChart.getData().add(bubbleSeries);
                root.setCenter(bubbleChart);
                break;

            case "ScatterChart":
                ScatterChart<Number, Number> scatterChart = new ScatterChart<>(new NumberAxis(), new NumberAxis());
                scatterChart.setTitle("Scatter Chart");
                XYChart.Series<Number, Number> scatterSeries = new XYChart.Series<>();
                scatterSeries.setName("Points");
                scatterSeries.getData().add(new XYChart.Data<>(5, 20));
                scatterSeries.getData().add(new XYChart.Data<>(10, 40));
                scatterSeries.getData().add(new XYChart.Data<>(15, 25));
                scatterChart.getData().add(scatterSeries);
                root.setCenter(scatterChart);
                break;

            case "StackedAreaChart":
                StackedAreaChart<String, Number> stackedAreaChart = new StackedAreaChart<>(new CategoryAxis(), new NumberAxis());
                stackedAreaChart.setTitle("Stacked Area Chart");
                XYChart.Series<String, Number> sa1 = new XYChart.Series<>();
                sa1.setName("Team A");
                sa1.getData().add(new XYChart.Data<>("Q1", 100));
                sa1.getData().add(new XYChart.Data<>("Q2", 120));
                sa1.getData().add(new XYChart.Data<>("Q3", 90));
                sa1.getData().add(new XYChart.Data<>("Q4", 110));
                XYChart.Series<String, Number> sa2 = new XYChart.Series<>();
                sa2.setName("Team B");
                sa2.getData().add(new XYChart.Data<>("Q1", 80));
                sa2.getData().add(new XYChart.Data<>("Q2", 95));
                sa2.getData().add(new XYChart.Data<>("Q3", 70));
                sa2.getData().add(new XYChart.Data<>("Q4", 85));
                stackedAreaChart.getData().addAll(sa1, sa2);
                root.setCenter(stackedAreaChart);
                break;

            case "StackedBarChart":
                StackedBarChart<String, Number> stackedBarChart = new StackedBarChart<>(new CategoryAxis(), new NumberAxis());
                stackedBarChart.setTitle("Stacked Bar Chart");
                XYChart.Series<String, Number> sb1 = new XYChart.Series<>();
                sb1.setName("Male");
                sb1.getData().add(new XYChart.Data<>("2021", 60));
                sb1.getData().add(new XYChart.Data<>("2022", 70));
                XYChart.Series<String, Number> sb2 = new XYChart.Series<>();
                sb2.setName("Female");
                sb2.getData().add(new XYChart.Data<>("2021", 40));
                sb2.getData().add(new XYChart.Data<>("2022", 55));
                stackedBarChart.getData().addAll(sb1, sb2);
                root.setCenter(stackedBarChart);
                break;
        }
    }

    public static void main(String[] args) {
        launch(args);
    }
}