Merge pull request #16454 from taosdata/test/td-18644-java-1
test: add java fast_write_example for 3.0
This commit is contained in:
commit
9d4a6de45a
Binary file not shown.
After Width: | Height: | Size: 7.1 KiB |
|
@ -0,0 +1,63 @@
|
|||
package com.taos.example.highvolume;
|
||||
|
||||
import java.sql.*;
|
||||
|
||||
/**
|
||||
* Prepare target database.
|
||||
* Count total records in database periodically so that we can estimate the writing speed.
|
||||
*/
|
||||
public class DataBaseMonitor {
|
||||
private Connection conn;
|
||||
private Statement stmt;
|
||||
|
||||
public DataBaseMonitor init() throws SQLException {
|
||||
if (conn == null) {
|
||||
String jdbcURL = System.getenv("TDENGINE_JDBC_URL");
|
||||
conn = DriverManager.getConnection(jdbcURL);
|
||||
stmt = conn.createStatement();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public void close() {
|
||||
try {
|
||||
stmt.close();
|
||||
} catch (SQLException e) {
|
||||
}
|
||||
try {
|
||||
conn.close();
|
||||
} catch (SQLException e) {
|
||||
}
|
||||
}
|
||||
|
||||
public void prepareDatabase() throws SQLException {
|
||||
stmt.execute("DROP DATABASE IF EXISTS test");
|
||||
stmt.execute("CREATE DATABASE test");
|
||||
stmt.execute("CREATE STABLE test.meters (ts TIMESTAMP, current FLOAT, voltage INT, phase FLOAT) TAGS (location BINARY(64), groupId INT)");
|
||||
}
|
||||
|
||||
public Long count() throws SQLException {
|
||||
if (!stmt.isClosed()) {
|
||||
ResultSet result = stmt.executeQuery("SELECT count(*) from test.meters");
|
||||
result.next();
|
||||
return result.getLong(1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* show test.stables;
|
||||
*
|
||||
* name | created_time | columns | tags | tables |
|
||||
* ============================================================================================
|
||||
* meters | 2022-07-20 08:39:30.902 | 4 | 2 | 620000 |
|
||||
*/
|
||||
public Long getTableCount() throws SQLException {
|
||||
if (!stmt.isClosed()) {
|
||||
ResultSet result = stmt.executeQuery("show test.stables");
|
||||
result.next();
|
||||
return result.getLong(5);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
package com.taos.example.highvolume;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.sql.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
|
||||
|
||||
public class FastWriteExample {
|
||||
final static Logger logger = LoggerFactory.getLogger(FastWriteExample.class);
|
||||
|
||||
final static int taskQueueCapacity = 1000000;
|
||||
final static List<BlockingQueue<String>> taskQueues = new ArrayList<>();
|
||||
final static List<ReadTask> readTasks = new ArrayList<>();
|
||||
final static List<WriteTask> writeTasks = new ArrayList<>();
|
||||
final static DataBaseMonitor databaseMonitor = new DataBaseMonitor();
|
||||
|
||||
public static void stopAll() {
|
||||
logger.info("shutting down");
|
||||
readTasks.forEach(task -> task.stop());
|
||||
writeTasks.forEach(task -> task.stop());
|
||||
databaseMonitor.close();
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws InterruptedException, SQLException {
|
||||
int readTaskCount = args.length > 0 ? Integer.parseInt(args[0]) : 1;
|
||||
int writeTaskCount = args.length > 1 ? Integer.parseInt(args[1]) : 3;
|
||||
int tableCount = args.length > 2 ? Integer.parseInt(args[2]) : 1000;
|
||||
int maxBatchSize = args.length > 3 ? Integer.parseInt(args[3]) : 3000;
|
||||
|
||||
logger.info("readTaskCount={}, writeTaskCount={} tableCount={} maxBatchSize={}",
|
||||
readTaskCount, writeTaskCount, tableCount, maxBatchSize);
|
||||
|
||||
databaseMonitor.init().prepareDatabase();
|
||||
|
||||
// Create task queues, whiting tasks and start writing threads.
|
||||
for (int i = 0; i < writeTaskCount; ++i) {
|
||||
BlockingQueue<String> queue = new ArrayBlockingQueue<>(taskQueueCapacity);
|
||||
taskQueues.add(queue);
|
||||
WriteTask task = new WriteTask(queue, maxBatchSize);
|
||||
Thread t = new Thread(task);
|
||||
t.setName("WriteThread-" + i);
|
||||
t.start();
|
||||
}
|
||||
|
||||
// create reading tasks and start reading threads
|
||||
int tableCountPerTask = tableCount / readTaskCount;
|
||||
for (int i = 0; i < readTaskCount; ++i) {
|
||||
ReadTask task = new ReadTask(i, taskQueues, tableCountPerTask);
|
||||
Thread t = new Thread(task);
|
||||
t.setName("ReadThread-" + i);
|
||||
t.start();
|
||||
}
|
||||
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(FastWriteExample::stopAll));
|
||||
|
||||
long lastCount = 0;
|
||||
while (true) {
|
||||
Thread.sleep(10000);
|
||||
long numberOfTable = databaseMonitor.getTableCount();
|
||||
long count = databaseMonitor.count();
|
||||
logger.info("numberOfTable={} count={} speed={}", numberOfTable, count, (count - lastCount) / 10);
|
||||
lastCount = count;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
package com.taos.example.highvolume;
|
||||
|
||||
import java.util.Iterator;
|
||||
|
||||
/**
|
||||
* Generate test data
|
||||
*/
|
||||
class MockDataSource implements Iterator {
|
||||
private String tbNamePrefix;
|
||||
private int tableCount;
|
||||
private long maxRowsPerTable = 1000000000L;
|
||||
|
||||
// 100 milliseconds between two neighbouring rows.
|
||||
long startMs = System.currentTimeMillis() - maxRowsPerTable * 100;
|
||||
private int currentRow = 0;
|
||||
private int currentTbId = -1;
|
||||
|
||||
// mock values
|
||||
String[] location = {"LosAngeles", "SanDiego", "Hollywood", "Compton", "San Francisco"};
|
||||
float[] current = {8.8f, 10.7f, 9.9f, 8.9f, 9.4f};
|
||||
int[] voltage = {119, 116, 111, 113, 118};
|
||||
float[] phase = {0.32f, 0.34f, 0.33f, 0.329f, 0.141f};
|
||||
|
||||
public MockDataSource(String tbNamePrefix, int tableCount) {
|
||||
this.tbNamePrefix = tbNamePrefix;
|
||||
this.tableCount = tableCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
currentTbId += 1;
|
||||
if (currentTbId == tableCount) {
|
||||
currentTbId = 0;
|
||||
currentRow += 1;
|
||||
}
|
||||
return currentRow < maxRowsPerTable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String next() {
|
||||
long ts = startMs + 100 * currentRow;
|
||||
int groupId = currentTbId % 5 == 0 ? currentTbId / 5 : currentTbId / 5 + 1;
|
||||
StringBuilder sb = new StringBuilder(tbNamePrefix + "_" + currentTbId + ","); // tbName
|
||||
sb.append(ts).append(','); // ts
|
||||
sb.append(current[currentRow % 5]).append(','); // current
|
||||
sb.append(voltage[currentRow % 5]).append(','); // voltage
|
||||
sb.append(phase[currentRow % 5]).append(','); // phase
|
||||
sb.append(location[currentRow % 5]).append(','); // location
|
||||
sb.append(groupId); // groupID
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
package com.taos.example.highvolume;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
|
||||
class ReadTask implements Runnable {
|
||||
private final static Logger logger = LoggerFactory.getLogger(ReadTask.class);
|
||||
private final int taskId;
|
||||
private final List<BlockingQueue<String>> taskQueues;
|
||||
private final int queueCount;
|
||||
private final int tableCount;
|
||||
private boolean active = true;
|
||||
|
||||
public ReadTask(int readTaskId, List<BlockingQueue<String>> queues, int tableCount) {
|
||||
this.taskId = readTaskId;
|
||||
this.taskQueues = queues;
|
||||
this.queueCount = queues.size();
|
||||
this.tableCount = tableCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign data received to different queues.
|
||||
* Here we use the suffix number in table name.
|
||||
* You are expected to define your own rule in practice.
|
||||
*
|
||||
* @param line record received
|
||||
* @return which queue to use
|
||||
*/
|
||||
public int getQueueId(String line) {
|
||||
String tbName = line.substring(0, line.indexOf(',')); // For example: tb1_101
|
||||
String suffixNumber = tbName.split("_")[1];
|
||||
return Integer.parseInt(suffixNumber) % this.queueCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
logger.info("started");
|
||||
Iterator<String> it = new MockDataSource("tb" + this.taskId, tableCount);
|
||||
try {
|
||||
while (it.hasNext() && active) {
|
||||
String line = it.next();
|
||||
int queueId = getQueueId(line);
|
||||
taskQueues.get(queueId).put(line);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Read Task Error", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
logger.info("stop");
|
||||
this.active = false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,205 @@
|
|||
package com.taos.example.highvolume;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.sql.*;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A helper class encapsulate the logic of writing using SQL.
|
||||
* <p>
|
||||
* The main interfaces are two methods:
|
||||
* <ol>
|
||||
* <li>{@link SQLWriter#processLine}, which receive raw lines from WriteTask and group them by table names.</li>
|
||||
* <li>{@link SQLWriter#flush}, which assemble INSERT statement and execute it.</li>
|
||||
* </ol>
|
||||
* <p>
|
||||
* There is a technical skill worth mentioning: we create table as needed when "table does not exist" error occur instead of creating table automatically using syntax "INSET INTO tb USING stb".
|
||||
* This ensure that checking table existence is a one-time-only operation.
|
||||
* </p>
|
||||
*
|
||||
* </p>
|
||||
*/
|
||||
public class SQLWriter {
|
||||
final static Logger logger = LoggerFactory.getLogger(SQLWriter.class);
|
||||
|
||||
private Connection conn;
|
||||
private Statement stmt;
|
||||
|
||||
/**
|
||||
* current number of buffered records
|
||||
*/
|
||||
private int bufferedCount = 0;
|
||||
/**
|
||||
* Maximum number of buffered records.
|
||||
* Flush action will be triggered if bufferedCount reached this value,
|
||||
*/
|
||||
private int maxBatchSize;
|
||||
|
||||
|
||||
/**
|
||||
* Maximum SQL length.
|
||||
*/
|
||||
private int maxSQLLength;
|
||||
|
||||
/**
|
||||
* Map from table name to column values. For example:
|
||||
* "tb001" -> "(1648432611249,2.1,114,0.09) (1648432611250,2.2,135,0.2)"
|
||||
*/
|
||||
private Map<String, String> tbValues = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Map from table name to tag values in the same order as creating stable.
|
||||
* Used for creating table.
|
||||
*/
|
||||
private Map<String, String> tbTags = new HashMap<>();
|
||||
|
||||
public SQLWriter(int maxBatchSize) {
|
||||
this.maxBatchSize = maxBatchSize;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get Database Connection
|
||||
*
|
||||
* @return Connection
|
||||
* @throws SQLException
|
||||
*/
|
||||
private static Connection getConnection() throws SQLException {
|
||||
String jdbcURL = System.getenv("TDENGINE_JDBC_URL");
|
||||
return DriverManager.getConnection(jdbcURL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Connection and Statement
|
||||
*
|
||||
* @throws SQLException
|
||||
*/
|
||||
public void init() throws SQLException {
|
||||
conn = getConnection();
|
||||
stmt = conn.createStatement();
|
||||
stmt.execute("use test");
|
||||
ResultSet rs = stmt.executeQuery("show variables");
|
||||
while (rs.next()) {
|
||||
String configName = rs.getString(1);
|
||||
if ("maxSQLLength".equals(configName)) {
|
||||
maxSQLLength = Integer.parseInt(rs.getString(2));
|
||||
logger.info("maxSQLLength={}", maxSQLLength);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert raw data to SQL fragments, group them by table name and cache them in a HashMap.
|
||||
* Trigger writing when number of buffered records reached maxBachSize.
|
||||
*
|
||||
* @param line raw data get from task queue in format: tbName,ts,current,voltage,phase,location,groupId
|
||||
*/
|
||||
public void processLine(String line) throws SQLException {
|
||||
bufferedCount += 1;
|
||||
int firstComma = line.indexOf(',');
|
||||
String tbName = line.substring(0, firstComma);
|
||||
int lastComma = line.lastIndexOf(',');
|
||||
int secondLastComma = line.lastIndexOf(',', lastComma - 1);
|
||||
String value = "(" + line.substring(firstComma + 1, secondLastComma) + ") ";
|
||||
if (tbValues.containsKey(tbName)) {
|
||||
tbValues.put(tbName, tbValues.get(tbName) + value);
|
||||
} else {
|
||||
tbValues.put(tbName, value);
|
||||
}
|
||||
if (!tbTags.containsKey(tbName)) {
|
||||
String location = line.substring(secondLastComma + 1, lastComma);
|
||||
String groupId = line.substring(lastComma + 1);
|
||||
String tagValues = "('" + location + "'," + groupId + ')';
|
||||
tbTags.put(tbName, tagValues);
|
||||
}
|
||||
if (bufferedCount == maxBatchSize) {
|
||||
flush();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Assemble INSERT statement using buffered SQL fragments in Map {@link SQLWriter#tbValues} and execute it.
|
||||
* In case of "Table does not exit" exception, create all tables in the sql and retry the sql.
|
||||
*/
|
||||
public void flush() throws SQLException {
|
||||
StringBuilder sb = new StringBuilder("INSERT INTO ");
|
||||
for (Map.Entry<String, String> entry : tbValues.entrySet()) {
|
||||
String tableName = entry.getKey();
|
||||
String values = entry.getValue();
|
||||
String q = tableName + " values " + values + " ";
|
||||
if (sb.length() + q.length() > maxSQLLength) {
|
||||
executeSQL(sb.toString());
|
||||
logger.warn("increase maxSQLLength or decrease maxBatchSize to gain better performance");
|
||||
sb = new StringBuilder("INSERT INTO ");
|
||||
}
|
||||
sb.append(q);
|
||||
}
|
||||
executeSQL(sb.toString());
|
||||
tbValues.clear();
|
||||
bufferedCount = 0;
|
||||
}
|
||||
|
||||
private void executeSQL(String sql) throws SQLException {
|
||||
try {
|
||||
stmt.executeUpdate(sql);
|
||||
} catch (SQLException e) {
|
||||
// convert to error code defined in taoserror.h
|
||||
int errorCode = e.getErrorCode() & 0xffff;
|
||||
if (errorCode == 0x362 || errorCode == 0x218) {
|
||||
// Table does not exist
|
||||
createTables();
|
||||
executeSQL(sql);
|
||||
} else {
|
||||
logger.error("Execute SQL: {}", sql);
|
||||
throw e;
|
||||
}
|
||||
} catch (Throwable throwable) {
|
||||
logger.error("Execute SQL: {}", sql);
|
||||
throw throwable;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create tables in batch using syntax:
|
||||
* <p>
|
||||
* CREATE TABLE [IF NOT EXISTS] tb_name1 USING stb_name TAGS (tag_value1, ...) [IF NOT EXISTS] tb_name2 USING stb_name TAGS (tag_value2, ...) ...;
|
||||
* </p>
|
||||
*/
|
||||
private void createTables() throws SQLException {
|
||||
StringBuilder sb = new StringBuilder("CREATE TABLE ");
|
||||
for (String tbName : tbValues.keySet()) {
|
||||
String tagValues = tbTags.get(tbName);
|
||||
sb.append("IF NOT EXISTS ").append(tbName).append(" USING meters TAGS ").append(tagValues).append(" ");
|
||||
}
|
||||
String sql = sb.toString();
|
||||
try {
|
||||
stmt.executeUpdate(sql);
|
||||
} catch (Throwable throwable) {
|
||||
logger.error("Execute SQL: {}", sql);
|
||||
throw throwable;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasBufferedValues() {
|
||||
return bufferedCount > 0;
|
||||
}
|
||||
|
||||
public int getBufferedCount() {
|
||||
return bufferedCount;
|
||||
}
|
||||
|
||||
public void close() {
|
||||
try {
|
||||
stmt.close();
|
||||
} catch (SQLException e) {
|
||||
}
|
||||
try {
|
||||
conn.close();
|
||||
} catch (SQLException e) {
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
package com.taos.example.highvolume;
|
||||
|
||||
public class StmtWriter {
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
package com.taos.example.highvolume;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
|
||||
class WriteTask implements Runnable {
|
||||
private final static Logger logger = LoggerFactory.getLogger(WriteTask.class);
|
||||
private final int maxBatchSize;
|
||||
|
||||
// the queue from which this writing task get raw data.
|
||||
private final BlockingQueue<String> queue;
|
||||
|
||||
// A flag indicate whether to continue.
|
||||
private boolean active = true;
|
||||
|
||||
public WriteTask(BlockingQueue<String> taskQueue, int maxBatchSize) {
|
||||
this.queue = taskQueue;
|
||||
this.maxBatchSize = maxBatchSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
logger.info("started");
|
||||
String line = null; // data getting from the queue just now.
|
||||
SQLWriter writer = new SQLWriter(maxBatchSize);
|
||||
try {
|
||||
writer.init();
|
||||
while (active) {
|
||||
line = queue.poll();
|
||||
if (line != null) {
|
||||
// parse raw data and buffer the data.
|
||||
writer.processLine(line);
|
||||
} else if (writer.hasBufferedValues()) {
|
||||
// write data immediately if no more data in the queue
|
||||
writer.flush();
|
||||
} else {
|
||||
// sleep a while to avoid high CPU usage if no more data in the queue and no buffered records, .
|
||||
Thread.sleep(100);
|
||||
}
|
||||
}
|
||||
if (writer.hasBufferedValues()) {
|
||||
writer.flush();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
String msg = String.format("line=%s, bufferedCount=%s", line, writer.getBufferedCount());
|
||||
logger.error(msg, e);
|
||||
} finally {
|
||||
writer.close();
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
logger.info("stop");
|
||||
this.active = false;
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 7.1 KiB |
Loading…
Reference in New Issue