Oracle Database 23ai で追加された True Cache に JDBC Thin driver でアクセスしてみた

Oracle Database 23ai から True Cache という機能が追加されました。
トランザクション量が多く、応答時間にシビアなシステムにおいてはアプリとDBの間にキャッシュ製品を挟む構成がよくあります。
通常はRedisやMemcachedといった製品を使うケースが多いですが、同様の機能をOracle Databaseの機能として提供するのが True Cache です。
True Cache は内部で Active Data Guard (ADG) の仕組みを使ってキャッシュにデータ読み込みを行います。
ただし通常の ADG と異なるのは、データをディスクに保持せず、バッファ・キャッシュのみに格納する点です。
![truecache.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33182/b25a99e5-5dde-a5d3-0fed-47f6c6790b11.jpeg)
以下はキャッシュに True Cache を使う主なメリットです。
- アプリからの接続に使うサービスを1つに集約できる (JDBC Thin driver 23以上が必要)
    - アプリ実装時にDB/キャッシュどちらに接続するか考える必要なし
    - Write伴うならPrimary DB (マスターデータを持つ大元のDB)、Read Onlyなら True Cache へ自動振り分け
- キャッシュへのデータ読込はOracle Databaseが実施 (アプリ実装不要)
    - キャッシュされていないデータがあれば、Primary DBから自動でキャッシュに読込
    - キャッシュ済みデータは Data Guard の仕組みにより常に最新化

今回はTrue Cacheについて以下作業を試してみました。
- True Cacheの構成
- Primary DBとTrue Cache両方にアクセスできるサービスの構成
- JDBC Thin driverからサービス接続した際に、Read処理がTrue Cacheへ自動振り分けされる動作の確認

なお本検証では Oracle Database 23ai Free を使いました。
Free版でも1つの True Cache を構成できますので、お手軽に試して頂けます。
またサーバはOCI Compute (Oracle Linux 8) を活用し、Primary DB と True Cache は別々のサーバにインストールしました。
以降は Oracle Database 23ai Free のインストールから True Cache の構成、サンプルのJavaコードを使った JDBC Thin driver による True Cache アクセスの手順を記載していきます。
また Primary DB のノードを Primaryノード、True Cache のノードをキャッシュノードと呼称します。

## 23aiインストール

Primaryノードとキャッシュノードの双方に Oracle Database 23ai Free 本体をインストールします。

```bash
$ sudo su -

# プリインストールRPMを使ってOS設定
$ dnf -y install oracle-database-preinstall-23ai

# 本体RPMをダウンロード＆インストール
$ wget https://download.oracle.com/otn-pub/otn_software/db-free/oracle-database-free-23ai-1.0-1.el8.x86_64.rpm
$ dnf install -y oracle-database-free*
```

## DB作成

PrimaryノードだけDBを作成します。
キャッシュノードには作成しないようご注意ください。

```bash
$ /etc/init.d/oracle-free-23ai configure
```

## firewall設定

Primaryノードとキャッシュノードの双方でファイアウォールを設定します。
Oracle Net通信で使うTCP 1521ポートを開けておきます。

```bash
$ sudo su -
# 1521ポート許可
$ firewall-cmd --add-port=1521/tcp
# 永続保存
$ firewall-cmd --runtime-to-permanent
# 設定リロード
$ firewall-cmd --reload
# 設定内容確認
$ firewall-cmd --list-all
```

## 環境変数設定

Primaryノードとキャッシュノードの双方でOracleユーザの環境変数を設定します。
`.bashrc`などに追記して読み込ませます。

```bash
export ORACLE_SID=FREE 
export ORACLE_BASE=/opt/oracle 
export ORACLE_HOME=/opt/oracle/product/23ai/dbhomeFree 
export PATH=/home/oracle/.local/bin:/home/oracle/bin:/usr/share/Modules/bin:/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/opt/oracle/product/23ai/dbhomeFree/bin
```

## True Cache 作成

### アーカイブ・ログモード設定

True Cache を構成するにはPrimary DBがアーカイブ・ログモードとなっている必要があるため、Primary DBをアーカイブ・ログモードに変更します。

```sql
-- 設定事前確認
SQL> select log_mode from V$DATABASE;

LOG_MODE
------------
NOARCHIVELOG

-- アーカイブ・ログモードに変更して起動
SQL> shutdown immediate
SQL> startup mount
SQL> alter database archivelog;
SQL> alter database open;

-- 設定事後確認
SQL> select log_mode from V$DATABASE;

LOG_MODE
------------
ARCHIVELOG
```

### パスワードファイルをコピー

Primaryノードにあるパスワードファイルをキャッシュノードへコピーします。
パスワードファイルはPrimaryノードの以下に配置されています。

```
/opt/oracle/product/23ai/dbhomeFree/dbs/orapwFREE
```

こちらをキャッシュノードの以下に配置する想定でこれ以降の手順を記載します。

```
/opt/oracle/tmp/
```

### True Cache 作成

DBCAを使ってキャッシュノード上に True Cache を作成します。
`your_easy_connect_string`にはご自分のPrimary DBの簡易接続識別子を記入します。

```bash
$ dbca -createTrueCache -gdbName FREE -sourceDBConnectionString your_easy_connect_string -passwordFileFromSourceDB /opt/oracle/tmp/orapwFREE -silent

Enter Remote DB SYS user password:

Session ID of the current execution is: 1
Log file location: /opt/oracle/cfgtoollogs/dbca/FREE/trace.log_2024-08-06_08-27-11AM_51824
-----------------
Running Extract_password_file_from_blob_file job
Completed Extract_password_file_from_blob_file job
25% complete
-----------------
Running Create_static_listener job
Completed Create_static_listener job
38% complete
-----------------
Running Register_listener job
Completed Register_listener job
50% complete
-----------------
Running Extract_tde_wallet_from_blob_file job
Skipping. Job is detected as not applicable.
54% complete
-----------------
Running Setup_required_directories job
Completed Setup_required_directories job
57% complete
-----------------
Running Create_pfile job
Completed Create_pfile job
61% complete
-----------------
Running Start_nomount_instance job
Completed Start_nomount_instance job
64% complete
-----------------
Running Create_TDE_wallet job
Skipping. Job is detected as not applicable.
68% complete
-----------------
Running Create_truecache_instance job
Completed Create_truecache_instance job
71% complete
-----------------
Running Add_oratab_entry job
Completed Add_oratab_entry job
75% complete
-----------------
Running Reopen_wallet job
Skipping. Job is detected as not applicable.
100% complete
---------- PLUGIN NOTES ----------
Successfully created True Cache.
In order to complete the operation,configure the True Cache database application services from the primary database.
---------- END OF PLUGIN NOTES ----------
Look at the log file "/opt/oracle/cfgtoollogs/dbca/FREE/FREE.log" for further details.
```

### 事後確認

キャッシュノード上に作成された True Cache インスタンスにログインし、問題なく構成されていることを確認します。

```sql
SQL> select name, open_mode, database_role from v$database;

NAME	  OPEN_MODE	           DATABASE_ROLE
--------- -------------------- ----------------
FREE	  READ ONLY WITH APPLY TRUE CACHE

SQL> select controlfile_type from v$database;

CONTROLFIL
----------
TRUE CACHE

SQL> col TRUE_CACHE_NAME format a15
SQL> col status format a10 
SQL> col primary_name format a15
SQL> select TRUE_CACHE_NAME, PRIMARY_NAME, STATUS, REMOTE_VERSION, CON_ID from v$true_cache;

TRUE_CACHE_NAME PRIMARY_NAME	STATUS	   REMOTE_VERSION	  CON_ID
--------------- --------------- ---------- ------------------ ----------
FREE		    FREE		    HEALTHY    23.0.0.0.0	      0
```

Primaryノードとキャッシュノード双方の`tnsnames.ora`にサービス名を登録しておきます。
`your_primary_node_host`、`your_cache_node_host`にはご自分のPrimaryノード、キャッシュノードのホスト名を記入します。

```
freepdb1 =
  (DESCRIPTION =
    (ADDRESS = (PROTOCOL = TCP)(HOST = your_primary_node_host)(PORT = 1521))
    (CONNECT_DATA =
      (SERVER = DEDICATED)
      (SERVICE_NAME = freepdb1)
    )
  )

free_tc =
  (DESCRIPTION =
    (ADDRESS = (PROTOCOL = TCP)(HOST = your_cache_node_host)(PORT = 1521))
    (CONNECT_DATA =
      (SERVER = DEDICATED)
      (SERVICE_NAME = freepdb1)
    )
  )
```

## True Cache 向けサービスの作成

ここから Primary DB と True Cache の接続を集約するためのアプリケーション用サービスを作成していきます。
まずはPrimaryノードで Valid Node Checking for Registration (VNCR) を有効化します。
VNCRは別サーバ上で稼働するDBインスタンス (今回で言う True Cache) をリスナーに登録許可するための設定です。

:::note info
VNCRの詳細はMy Oracle Supportの下記ドキュメントが参考になります。
登録のための有効ノードのチェック (VNCR) (Doc ID 1900587.1)
:::

以下をPrimaryノードの`listener.ora`に追記します。
`your_host_name`にはご自分のPrimaryノードのホスト名を記載します。

```
VALID_NODE_CHECKING_REGISTRATION_LISTENER = SUBNET
REGISTRATION_INVITED_NODES_LISTENER=(your_host_name)
```

設定を有効化するため、listenerをリロードします。

```bash
$ lsnrctl reload
```

Primary DB上のPDBにアプリケーション接続用のサービスを作成します。
今回はサンプル・スキーマの`HR`を使うため、`HR`というサービス名にしています。

```sql
BEGIN
   DBMS_SERVICE.CREATE_SERVICE('HR', 'HR');
   DBMS_SERVICE.START_SERVICE('HR');
END;
/
```

作成したサービスの設定を確認します。
まだ True Cache 用サービスは紐づいていないため、`true_cache_service`は空欄です。

```sql
SQL> SELECT service_id, name, true_cache_service FROM v$active_services WHERE name='HR';

SERVICE_ID NAME
---------- ----------------------------------------------------------------
TRUE_CACHE_SERVICE
----------------------------------------------------------------
	 1     HR

```

DBCAを使い、True Cache用サービスをPrimary DBに登録します。
Primaryノード上で以下コマンドを実行します。
`your_tc_easy_connect_string`には True Cache インスタンスに対する簡易接続識別子を記入します。
また True Cache 接続用のサービス名は`hr_tc`としています。

```bash
$ dbca -configureDatabase -configureTrueCacheInstanceService -sourceDB FREE -trueCacheConnectString your_tc_easy_connect_string -trueCacheServiceName hr_tc -serviceName hr -pdbName freepdb1 -silent
```

Primaryノード上でリモートリスナーの設定を表示し、正しく構成されたことを確認します。

```bash
$ lsnrctl services
...(略)...
Service "hr_tc" has 1 instance(s).
  Instance "FREE", status READY, has 1 handler(s) for this service...
    Handler(s):
      "DEDICATED" established:0 refused:0 state:ready
         REMOTE SERVER
         (ADDRESS=(PROTOCOL=TCP)(HOST=tc_host_name)(PORT=1521))
...(略)...
```

なおTrue Cache用サービス`hr_tc`ですが、DBCA実行後にTrue Cacheインスタンス上で自動起動されていました。

Primaryノード上で再度`HR`サービスの設定を確認します。
今回は`true_cache_service`に`hr_tc`と表示されます。

```sql
SELECT service_id, name, true_cache_service FROM v$active_services WHERE name='HR';

SERVICE_ID NAME
---------- ----------------------------------------------------------------
TRUE_CACHE_SERVICE
----------------------------------------------------------------
	 1     HR
hr_tc
```

## サンプル・スキーマのインストール

Javaプログラムからアクセスするサンプル・スキーマをインストールします。
まずはPrimaryノード上でサンプル・スキーマをダウンロードします。

```bash
$ wget https://github.com/oracle-samples/db-sample-schemas/archive/refs/tags/v23.2.zip
$ unzip v23.2.zip
```

Primary DBにサンプル・スキーマをインストールします。
今回は`HR`スキーマを使いました。

```sql
@hr_install.sql 
```

## Javaインストール

Javaプログラムを使うため、コンパイラ`javac`をインストールします。
今回はPrimaryノード上にインストールし、Primaryノードからプログラム実行する構成としました。

```bash
$ dnf -y install java-11-openjdk-devel
```

## JDBC Thin driverを使ったTrue Cacheへの接続テスト

JavaプログラムからHRスキーマへアクセスする前に、True Cacheへの接続テスト用プログラムを実行してみます。
以下はマニュアルに掲載されていたサンプルコードです。
ポイントは 46 行目の `setReadOnly(true)` です。
接続時に使うサービス名は`hr`のみですが、`setReadOnly(true)`の指定有無で接続先がPrimary DB/True Cacheに自動で振り分けられます。
`setReadOnly(true)`の設定により Trun Cache へ接続され、なければ Primary DB に接続します。
なおコード中の以下の記述はご自分の環境に合わせて修正してください。
- your_primary_db_host: Primaryノードのホスト名
- your_system_pw: sys/systemのパスワード

:::note alert
本サンプル・コードを本番環境で利用しないでください。
:::

```java
import java.sql.*;
import java.io.*;
import java.util.*;
import java.util.logging.FileHandler;
import java.util.logging.Level;
import java.util.logging.Logger;

import oracle.jdbc.driver.OracleLog;
import oracle.jdbc.pool.OracleDataSource;

public class TrueCache {
    static String url_primary = "jdbc:oracle:thin:@your_primary_db_host:1521/hr";
    static String user = "SYSTEM";
    static String password = "your_system_pw";

    public static void main(String args[]) {
        try {
            TrueCache t = new TrueCache();
            if(args != null && args.length >0 ) {
                url_primary = args[0];
                user = args[1];
                password = args[2];
            }
            t.test1();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void test1() throws SQLException {
        show("Basic test to connect to primary and True Cache");
        try {
            show("Get Connection from True Cache instance using primary db and properties");
            OracleDataSource ods1 = new OracleDataSource();
            show("URL = " + url_primary);
            ods1.setURL(url_primary);
            ods1.setUser(user);
            ods1.setPassword(password);
            ods1.setConnectionProperty("oracle.jdbc.useTrueCacheDriverConnection", "true");

            Connection conn1 = ods1.getConnection();

            show("isReadonly " + conn1.isReadOnly());
            verifyConnection(conn1);  // This is connected to Primary

            conn1.setReadOnly(true);
            show("isReadonly " + conn1.isReadOnly());
            verifyConnection(conn1); // This is connected to True Cache

            conn1.close();
        } catch (SQLException sqex) {
            show("test1 -- failed" + sqex.getMessage()+":"+sqex.getCause());
            sqex.printStackTrace();
        }
        show("The end");
    }

    public void verifyConnection(Connection conn) {
        try {
            Statement statement = conn.createStatement();
            ResultSet rs = statement.executeQuery("SELECT database_role from v$database");
            ResultSetMetaData rsmd = rs.getMetaData();
            int columnsNumber = rsmd.getColumnCount();
            rs.next();
            show("Database role : " + rs.getString(1));
            rs.close();
            ResultSet resultSet = statement.executeQuery("SELECT SYS_CONTEXT('userenv', 'instance_name') as instance_name"
                    + ", SYS_CONTEXT('userenv', 'server_host')" + " as server_host" + ", SYS_CONTEXT('userenv', 'service_name')"
                    + " as service_name" + ", SYS_CONTEXT('USERENV','db_unique_name')" + " as db_unique_name" + " from sys.dual");
            resultSet.next();
            show("instance_name : " + resultSet.getString("instance_name"));
            show("server_host : " + resultSet.getString("server_host"));
            show("service_name : " + resultSet.getString("service_name"));
            show("db_unique_name : " + resultSet.getString("db_unique_name"));
            resultSet.close();
            statement.close();
        } catch (SQLException sqex) {
            show("verifyConnection failed " + sqex.getMessage());
        }
    }

    public void show(String msg) {
        System.out.println(msg);
    }
}
```

上記を`TrueCache.java`というファイル名で保存し、以下のようにコンパイルします。

```bash
$ javac -classpath /opt/oracle/product/23ai/dbhomeFree/jdbc/lib/ojdbc11.jar TrueCache.java
```

実行してTrue Cacheインスタンスへ接続できることを確認します。
出力の`primary_host`、`cache_host`はご自分のPrimaryノード、キャッシュノードのホスト名が表示されます。

```bash
$ java -classpath /opt/oracle/product/23ai/dbhomeFree/jdbc/lib/ojdbc11.jar TrueCache.java
Basic test to connect to primary and True Cache
Get Connection from True Cache instance using primary db and properties
URL = jdbc:oracle:thin:@your_primary_node_host:1521/hr
isReadonly false
Database role : PRIMARY
instance_name : FREE
server_host : primary_host
service_name : HR
db_unique_name : FREE
isReadonly true
Database role : TRUE CACHE
instance_name : FREE
server_host : cache_host
service_name : hr_tc
db_unique_name : FREE
The end
```

## JDBC Thin driverを使ったHRスキーマへのアクセス

JDBC Thin driverを使って先ほど作成したサービス`HR`に接続し、HRスキーマのデータにアクセスしてみます。
`setReadOnly(true)`を設定してアクセスすることで、True Cache インスタンスのバッファ・キャッシュに該当データがキャッシュされます。
まずは事前のキャッシュ状況をTrue Cacheインスタンス上のPDBで確認します。

```sql
SQL> set trimspool on
SQL> set linesize 180
SQL> col object_name format a30
SQL> col object_type format a20
SQL> col owner format a25
SQL> col number_of_blocks format 9999999999999

SQL> SELECT o.OBJECT_NAME, o.OBJECT_TYPE, o.OWNER, COUNT(*) NUMBER_OF_BLOCKS,
substr(d.database_role,1,10) role
FROM DBA_OBJECTS o, V$BH bh,  v$database d
WHERE o.DATA_OBJECT_ID = bh.OBJD
AND o.OWNER in ('HR') 
GROUP BY o.OBJECT_NAME, o.OWNER, o.OBJECT_TYPE,d.database_role;

no rows selected
```

まだ何もキャッシュされていないことが分かります。

次にHRスキーマのデータへアクセスするJavaプログラムを作成します。
今回は以下のコードを利用しました。
なおコード中の以下の記述はご自分の環境に合わせて修正してください。
- your_primary_db_host: Primaryノードのホスト名
- your_hr_pw: HRユーザのパスワード

:::note alert
本サンプル・コードを本番環境で利用しないでください。
:::

```java
import java.sql.*;
import java.io.*;
import java.util.*;
import java.util.logging.FileHandler;
import java.util.logging.Level;
import java.util.logging.Logger;

import oracle.jdbc.driver.OracleLog;
import oracle.jdbc.pool.OracleDataSource;

public class SelectHR {
    static String url_primary = "jdbc:oracle:thin:@your_primary_node_host:1521/hr";
    static String user = "HR";
    static String password = "your_hr_pw";

    public static void main(String args[]) {
        try {
            OracleDataSource ods1 = new OracleDataSource();
            ods1.setURL(url_primary);
            ods1.setUser(user);
            ods1.setPassword(password);
            ods1.setConnectionProperty("oracle.jdbc.useTrueCacheDriverConnection", "true");

            Connection conn = ods1.getConnection();
            conn.setReadOnly(true);
            
            Statement statement = conn.createStatement();
            ResultSet resultSet = statement.executeQuery("select /*+FULL(c)*/ count(*) emp_num from hr.employees e");
            resultSet.next();
            System.out.println("emp_num : " + resultSet.getString("emp_num"));
            resultSet.close();
            statement.close();
            conn.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
```

本コードはHRスキーマの`EMPLOYEES`表の行数をカウントする内容となっています。
接続テストのコードと同様、25 行目にある`setReadOnly(true)`の設定によりPrimary DB/True Cacheどちらに接続するかが制御されます。
こちらのコードを`SelectHR.java`というファイル名で保存した場合、以下のようにコンパイルします。

```bash
$ javac -classpath /opt/oracle/product/23ai/dbhomeFree/jdbc/lib/ojdbc11.jar SelectHR.java
```

プログラムを実行します。

```bash
$ java -classpath /opt/oracle/product/23ai/dbhomeFree/jdbc/lib/ojdbc11.jar SelectHR.java
emp_num : 107
```

最後にTrue Cache インスタンスのPDBにログインし、キャッシュされているデータを再度確認します。

```sql
SQL> set trimspool on
SQL> set linesize 180
SQL> col object_name format a30
SQL> col object_type format a20
SQL> col owner format a25
SQL> col number_of_blocks format 9999999999999

SQL> SELECT o.OBJECT_NAME, o.OBJECT_TYPE, o.OWNER, COUNT(*) NUMBER_OF_BLOCKS,
substr(d.database_role,1,10) role
FROM DBA_OBJECTS o, V$BH bh,  v$database d
WHERE o.DATA_OBJECT_ID = bh.OBJD
AND o.OWNER in ('HR') 
GROUP BY o.OBJECT_NAME, o.OWNER, o.OBJECT_TYPE,d.database_role;

OBJECT_NAME                    OBJECT_TYPE          OWNER                     NUMBER_OF_BLOCKS ROLE
------------------------------ -------------------- ------------------------- ---------------- ----------------------------------------
EMP_MANAGER_IX                 INDEX                HR                                       1 TRUE CACHE
EMPLOYEES                      TABLE                HR                                       6 TRUE CACHE
EMP_NAME_IX                    INDEX                HR                                       1 TRUE CACHE
EMP_EMP_ID_PK                  INDEX                HR                                       2 TRUE CACHE
EMP_EMAIL_UK                   INDEX                HR                                       1 TRUE CACHE
EMP_DEPARTMENT_IX              INDEX                HR                                       1 TRUE CACHE
EMP_JOB_IX                     INDEX                HR                                       1 TRUE CACHE

7 rows selected.
```

`setReadOnly(true)`を設定してTrue Cacheに接続した状態で`EMPLOYEES`表をフルスキャンしましたので、`EMPLOYEES`表および依存関係にあるオブジェクトがTrue Cacheインスタンスのバッファ・キャッシュに読み込まれています。

以上、JDBC Thin driverを使ったTrue Cacheへの接続例でした。
接続時に指定するサービスはそのまま、`setReadOnly(true)`の追記だけでTrue Cacheへアクセスできますので、お手軽にキャッシュ層を導入できるのではないでしょうか。
Oracle Database 23ai Freeを使えば無償で構成できますので、是非お試しください。