環境說明
本實驗環境基於Oracle 12C和JDK1.8,其中Oracle 12C支持多租戶特性,相較於之前的Oracle版本,使用‘C##用戶名‘表示用戶,例如如果數據庫用戶叫kevin,則登陸時使用C##kevin進行登陸。
一、Oracle高級消息隊列AQ
Oracle AQ是Oracle中的消息隊列,是Oracle中的一種高級應用,每個版本都在不斷的加強,使用DBMS_AQ系統包進行相應的操作,是Oracle的默認組件,只要安裝了Oracle數據庫就可以使用。使用AQ可以在多個Oracle數據庫、Oracle與Java、C等系統中進行數據傳輸。
下面分步驟說明如何創建Oracle AQ
1. 創建消息負荷payload
Oracle AQ中傳遞的消息被稱爲有效負荷(payloads),格式可以是用戶自定義對象或XMLType或ANYDATA。本例中我們創建一個簡單的對象類型用於傳遞消息。
create type demo_queue_payload_type as object (message varchar2(4000));
2. 創建隊列表
隊列表用於存儲消息,在入隊時自動存入表中,出隊時自動刪除。使用DBMS_AQADM包進行數據表的創建,只需要寫表名,同時設置相應的屬性。對於隊列需要設置multiple_consumers爲false,如果使用發佈/訂閱模式需要設置爲true。
begin
dbms_aqadm.create_queue_table(
queue_table => 'demo_queue_table',
queue_payload_type => 'demo_queue_payload_type',
multiple_consumers => false
);
end;
執行完後可以查看oracle表中自動生成了demo_queue_table表,可以查看影響子段(含義比較清晰)。
3. 創建隊列並啓動
創建隊列並啓動隊列:
begin
dbms_aqadm.create_queue (
queue_name => 'demo_queue',
queue_table => 'demo_queue_table'
);
dbms_aqadm.start_queue(
queue_name => 'demo_queue'
);
end;
至此,我們已經創建了隊列有效負荷,隊列表和隊列。可以查看以下系統創建了哪些相關的對象:
SELECT object_name, object_type FROM user_objects WHERE object_name != 'DEMO_QUEUE_PAYLOAD_TYPE';
OBJECT_NAME OBJECT_TYPE
------------------------------ ---------------
DEMO_QUEUE_TABLE TABLE
SYS_C009392 INDEX
SYS_LOB0000060502C00030$$ LOB
AQ$_DEMO_QUEUE_TABLE_T INDEX
AQ$_DEMO_QUEUE_TABLE_I INDEX
AQ$_DEMO_QUEUE_TABLE_E QUEUE
AQ$DEMO_QUEUE_TABLE VIEW
DEMO_QUEUE QUEUE
我們看到一個隊列帶出了一系列自動生成對象,有些是被後面直接用到的。不過有趣的是,創建了第二個隊列。這就是所謂的異常隊列(exception queue)。如果AQ無法從我們的隊列接收消息,將記錄在該異常隊列中。
消息多次處理出錯等情況會自動轉移到異常的隊列,對於異常隊列如何處理目前筆者還沒有找到相應的寫法,因爲我使用的場景並不要求消息必須一對一的被處理,只要起到通知的作用即可。所以如果消息轉移到異常隊列,可以執行清空隊列表中的數據
delete from demo_queue_table;
4. 隊列的停止和刪除
如果需要刪除或重建可以使用下面的方法進行操作:
BEGIN
DBMS_AQADM.STOP_QUEUE(
queue_name => 'demo_queue'
);
DBMS_AQADM.DROP_QUEUE(
queue_name => 'demo_queue'
);
DBMS_AQADM.DROP_QUEUE_TABLE(
queue_table => 'demo_queue_table'
);
END;
5. 入隊消息
入列操作是一個基本的事務操作(就像往隊列表Insert),因此我們需要提交。
declare
r_enqueue_options DBMS_AQ.ENQUEUE_OPTIONS_T;
r_message_properties DBMS_AQ.MESSAGE_PROPERTIES_T;
v_message_handle RAW(16);
o_payload demo_queue_payload_type;
begin
o_payload := demo_queue_payload_type('what is you name ?');
dbms_aq.enqueue(
queue_name => 'demo_queue',
enqueue_options => r_enqueue_options,
message_properties => r_message_properties,
payload => o_payload,
msgid => v_message_handle
);
commit;
end;
通過SQL語句查看消息是否正常入隊:
select * from aq$demo_queue_table;
select user_data from aq$demo_queue_table;
6. 出隊消息
使用Oracle進行出隊操作,我沒有實驗成功(不確定是否和DBMS_OUTPUT的執行權限有關),代碼如下,讀者可以進行調試:
declare
r_dequeue_options DBMS_AQ.DEQUEUE_OPTIONS_T;
r_message_properties DBMS_AQ.MESSAGE_PROPERTIES_T;
v_message_handle RAW(16);
o_payload demo_queue_payload_type;
begin
DBMS_AQ.DEQUEUE(
queue_name => 'demo_queue',
dequeue_options => r_dequeue_options,
message_properties => r_message_properties,
payload => o_payload,
msgid => v_message_handle
);
DBMS_OUTPUT.PUT_LINE(
'***** Browse message is [' || o_payload.message || ']****'
);
end;
二、Java使用JMS監聽並處理Oracle AQ隊列
Java使用JMS進行相應的處理,需要使用Oracle提供的jar,在Oracle安裝目錄可以找到:在linux中可以使用find命令進行查找,例如
find `pwd` -name 'jmscommon.jar'
需要的jar爲:
- app/oracle/product/12.1.0/dbhome_1/rdbms/jlib/jmscommon.jar
- app/oracle/product/12.1.0/dbhome_1/jdbc/lib/ojdbc7.jar
- app/oracle/product/12.1.0/dbhome_1/jlib/orai18n.jar
- app/oracle/product/12.1.0/dbhome_1/jlib/jta.jar
- app/oracle/product/12.1.0/dbhome_1/rdbms/jlib/aqapi_g.jar
1. 創建連接參數類
實際使用時可以把參數信息配置在properties文件中,使用Spring進行注入。
package org.kevin.jms;
/**
*
* @author 李文鍇
* 連接參數信息
*
*/
public class JmsConfig {
public String username = "c##kevin";
public String password = "a111111111";
public String jdbcUrl = "jdbc:oracle:thin:@127.0.0.1:1521:orcl";
public String queueName = "demo_queue";
}
2. 創建消息轉換類
因爲消息載荷是Oracle數據類型,需要提供一個轉換工廠類將Oracle類型轉換爲Java類型。
package org.kevin.jms;
import java.sql.SQLException;
import oracle.jdbc.driver.OracleConnection;
import oracle.jdbc.internal.OracleTypes;
import oracle.jpub.runtime.MutableStruct;
import oracle.sql.CustomDatum;
import oracle.sql.CustomDatumFactory;
import oracle.sql.Datum;
import oracle.sql.STRUCT;
/**
*
* @author 李文鍇
* 數據類型轉換類
*
*/
@SuppressWarnings("deprecation")
public class QUEUE_MESSAGE_TYPE implements CustomDatum, CustomDatumFactory {
public static final String _SQL_NAME = "QUEUE_MESSAGE_TYPE";
public static final int _SQL_TYPECODE = OracleTypes.STRUCT;
MutableStruct _struct;
// 12表示字符串
static int[] _sqlType = { 12 };
static CustomDatumFactory[] _factory = new CustomDatumFactory[1];
static final QUEUE_MESSAGE_TYPE _MessageFactory = new QUEUE_MESSAGE_TYPE();
public static CustomDatumFactory getFactory() {
return _MessageFactory;
}
public QUEUE_MESSAGE_TYPE() {
_struct = new MutableStruct(new Object[1], _sqlType, _factory);
}
public Datum toDatum(OracleConnection c) throws SQLException {
return _struct.toDatum(c, _SQL_NAME);
}
public CustomDatum create(Datum d, int sqlType) throws SQLException {
if (d == null)
return null;
QUEUE_MESSAGE_TYPE o = new QUEUE_MESSAGE_TYPE();
o._struct = new MutableStruct((STRUCT) d, _sqlType, _factory);
return o;
}
public String getContent() throws SQLException {
return (String) _struct.getAttribute(0);
}
}
3. 主類進行消息處理
package org.kevin.jms;
import java.util.Properties;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.MessageListener;
import javax.jms.Queue;
import javax.jms.QueueConnection;
import javax.jms.QueueConnectionFactory;
import javax.jms.Session;
import oracle.jms.AQjmsAdtMessage;
import oracle.jms.AQjmsDestination;
import oracle.jms.AQjmsFactory;
import oracle.jms.AQjmsSession;
/**
*
* @author 李文鍇 消息處理類
*
*/
public class Main {
public static void main(String[] args) throws Exception {
JmsConfig config = new JmsConfig();
QueueConnectionFactory queueConnectionFactory = AQjmsFactory.getQueueConnectionFactory(config.jdbcUrl,
new Properties());
QueueConnection conn = queueConnectionFactory.createQueueConnection(config.username, config.password);
AQjmsSession session = (AQjmsSession) conn.createQueueSession(false, Session.AUTO_ACKNOWLEDGE);
conn.start();
Queue queue = (AQjmsDestination) session.getQueue(config.username, config.queueName);
MessageConsumer consumer = session.createConsumer(queue, null, QUEUE_MESSAGE_TYPE.getFactory(), null, false);
consumer.setMessageListener(new MessageListener() {
@Override
public void onMessage(Message message) {
System.out.println("ok");
AQjmsAdtMessage adtMessage = (AQjmsAdtMessage) message;
try {
QUEUE_MESSAGE_TYPE payload = (QUEUE_MESSAGE_TYPE) adtMessage.getAdtPayload();
System.out.println(payload.getContent());
} catch (Exception e) {
e.printStackTrace();
}
}
});
Thread.sleep(1000000);
}
}
使用Oracle程序塊進行入隊操作,在沒有啓動Java時看到隊列表中存在數據。啓動Java後,控制檯正確的輸出的消息;通過Oracle程序塊再次寫入消息,發現控制檯正確處理消息。Java的JMS監聽不是立刻進行處理,可能存在幾秒中的時間差,時間不等。
三、監控表記錄變化通知Java
下面的例子創建一個數據表,然後在表中添加觸發器,當數據變化後觸發器調用存儲過程給Oracle AQ發送消息,然後使用Java JMS對消息進行處理。
1. 創建表
創建student表,包含username和age兩個子段,其中username時varchar2類型,age時number類型。
2. 創建存儲過程
創建send_aq_msg存儲過程,因爲存儲過程中調用dbms數據包,系統包在存儲過程中執行需要進行授權(使用sys用戶進行授權):
grant execute on dbms_aq to c##kevin;
注意存儲過程中包含commit語句。
create or replace
PROCEDURE send_aq_msg (info IN VARCHAR2) as
r_enqueue_options DBMS_AQ.ENQUEUE_OPTIONS_T;
r_message_properties DBMS_AQ.MESSAGE_PROPERTIES_T;
v_message_handle RAW(16);
o_payload demo_queue_payload_type;
begin
o_payload := demo_queue_payload_type(info);
dbms_aq.enqueue(
queue_name => 'demo_queue',
enqueue_options => r_enqueue_options,
message_properties => r_message_properties,
payload => o_payload,
msgid => v_message_handle
);
commit;
end send_aq_msg;
3. 創建觸發器
在student表中創建觸發器,當數據寫入或更新時,如果age=18,則進行入隊操作。需要調用存儲過程發送消息,但觸發器中不能包含事物提交語句,因此需要使用pragma autonomous_transaction;聲明自由事物:
CREATE OR REPLACE TRIGGER STUDENT_TR
AFTER INSERT OR UPDATE OF AGE ON STUDENT FOR EACH ROW
DECLARE
pragma autonomous_transaction;
BEGIN
if :new.age = 18 then
send_aq_msg(:new.username);
end if;
END;
創建完觸發器後向執行插入或更新操作:
insert into student (username,age) values ('jack.lee.3k', 18);
update student set age=18 where username='jack003';
Java JMS可以正確的處理消息。