Une base de données hybride de moyenne à grande taille implémentée en C#, fonctionnant sur la plateforme .NET (prend en charge .NET Framework 4.5 ~ .NET 10), prenant en charge le mode double embarqué/serveur, intégrant les capacités relationnelles, de séries chronologiques, de file d'attente de messages et NoSQL (KV).
NewLife.NovaDb (abrégé Nova) est l'infrastructure de base de l'écosystème NewLife, un moteur de données intégré pour les applications .NET. En supprimant de nombreuses fonctionnalités de niche (telles que les procédures stockées/déclencheurs/fonctions de fenêtre), il obtient de meilleures performances de lecture/écriture et des coûts opérationnels plus faibles ; le volume de données est logiquement illimité (contraint par le disque et les stratégies de partitionnement), et peut remplacer SQLite/MySQL/Redis/TDengine dans des scénarios spécifiques.
- Modes de déploiement doubles :
- Mode embarqué : S'exécute comme une bibliothèque comme SQLite, avec des données stockées dans des dossiers locaux, configuration zéro
- Mode serveur : Processus autonome + protocole TCP, accès réseau comme MySQL ; prend en charge le déploiement en cluster et la réplication maître-esclave (un maître, plusieurs esclaves)
- Dossier comme base de données : Copiez le dossier pour terminer la migration/sauvegarde, aucun processus dump/restore nécessaire. Chaque table a des groupes de fichiers indépendants (
.data/.idx/.wal). - Intégration de quatre moteurs :
- Nova Engine (relationnel général) : Index SkipList + transactions MVCC (Read Committed), prend en charge CRUD, requêtes SQL, JOIN
- Flux Engine (séries temporelles + MQ) : Partitionnement basé sur le temps Append Only, prend en charge le nettoyage automatique TTL, groupes de consommateurs de style Redis Stream + Pending + Ack
- Mode KV (vue logique) : Réutilise Nova Engine, l'API masque les détails SQL, chaque ligne contient
Key + Value + TTL - ADO.NET Provider : Reconnaît automatiquement le mode embarqué/serveur, intégration native avec XCode ORM
- Séparation d'index chaud-froid dynamique : Les données chaudes sont entièrement chargées dans la mémoire physique (nœuds SkipList), les données froides sont déchargées vers MMF avec seulement un répertoire clairsemé conservé. Une table de 10 millions de lignes interrogeant uniquement les 10 000 dernières lignes utilise < 20 Mo de mémoire.
- Code purement géré : Aucune dépendance de composant natif (pur C#/.NET), facile à déployer sur plusieurs plateformes et dans des environnements restreints.
| Moteur | Structure de données | Cas d'utilisation |
|---|---|---|
| Nova Engine | SkipList (Séparation mémoire+MMF chaud-froid) | CRUD général, tables de configuration, commandes commerciales, données utilisateur |
| Flux Engine | Partitionnement basé sur le temps (Append Only) | Capteurs IoT, collecte de journaux, files d'attente de messages internes, journaux d'audit |
| Mode KV | Vue logique de table Nova | Verrous distribués, mise en cache, stockage de session, compteurs, centre de configuration |
| Catégorie | Type SQL | Mapping C# | Description |
|---|---|---|---|
| Booléen | BOOL |
Boolean |
1 octet |
| Entier | INT / LONG |
Int32 / Int64 |
4/8 octets |
| Flottant | DOUBLE |
Double |
8 octets |
| Décimal | DECIMAL |
Decimal |
128 bits, précision unifiée |
| Chaîne | STRING(n) / STRING |
String |
UTF-8, la longueur peut être spécifiée |
| Binaire | BINARY(n) / BLOB |
Byte[] |
La longueur peut être spécifiée |
| DateTime | DATETIME |
DateTime |
Précision en Ticks (100 nanosecondes) |
| GeoPoint | GEOPOINT |
Structure personnalisée | Coordonnées latitude/longitude (prévu) |
| Vecteur | VECTOR(n) |
Single[] |
Recherche vectorielle IA (prévu) |
Sous-ensemble SQL standard implémenté, couvrant environ 60% des scénarios commerciaux courants :
| Fonctionnalité | Statut | Description |
|---|---|---|
| DDL | ✅ | CREATE/DROP TABLE/INDEX/DATABASE, ALTER TABLE (ADD/MODIFY/DROP COLUMN, COMMENT), avec IF NOT EXISTS, PRIMARY KEY, UNIQUE, ENGINE |
| DML | ✅ | INSERT (plusieurs lignes), UPDATE, DELETE, UPSERT (ON DUPLICATE KEY UPDATE), TRUNCATE TABLE |
| Requête | ✅ | SELECT/WHERE/ORDER BY/GROUP BY/HAVING/LIMIT/OFFSET |
| Agrégation | ✅ | COUNT/SUM/AVG/MIN/MAX |
| JOIN | ✅ | INNER/LEFT/RIGHT JOIN (Nested Loop), prend en charge les alias de table |
| Paramétrage | ✅ | Espaces réservés @param |
| Transaction | ✅ | MVCC, Read Committed, COMMIT/ROLLBACK |
| Fonctions SQL | ✅ | Fonctions de chaîne/numérique/date/conversion/conditionnelle/hachage (60+ fonctions) |
| Sous-requête | ✅ | Sous-requêtes IN/EXISTS |
| Avancé | ❌ | Pas de vues/déclencheurs/procédures stockées/fonctions de fenêtre |
Installez le package principal NovaDb via NuGet :
dotnet add package NewLife.NovaDbNovaDb fournit deux méthodes d'accès client pour différents scénarios :
| Méthode d'accès | Moteur cible | Description |
|---|---|---|
| ADO.NET + SQL | Nova (Relationnel), Flux (Séries temporelles) | DbConnection/DbCommand/DbDataReader standard, compatible avec tous les ORM |
| NovaClient | MQ (File d'attente de messages), KV (Stockage clé-valeur) | Client RPC fournissant des API de publication/consommation/acquittement de messages et de lecture/écriture KV |
Le moteur relationnel (Nova Engine) est accessible via l'interface standard ADO.NET. Un Data Source dans la chaîne de connexion indique le mode embarqué ; un Server indique le mode serveur.
Le mode embarqué ne nécessite aucun service autonome, idéal pour les applications de bureau, les appareils IoT et les tests unitaires.
using NewLife.NovaDb.Client;
// Créer une connexion (mode embarqué, dossier comme base de données)
using var conn = new NovaConnection { ConnectionString = "Data Source=./mydb" };
conn.Open();
// Créer une table
using var cmd = conn.CreateCommand();
cmd.CommandText = @"CREATE TABLE IF NOT EXISTS users (
id INT PRIMARY KEY AUTO_INCREMENT,
name STRING(50) NOT NULL,
age INT DEFAULT 0,
created DATETIME
)";
cmd.ExecuteNonQuery();
// Insérer des données
cmd.CommandText = "INSERT INTO users (name, age, created) VALUES ('Alice', 25, NOW())";
cmd.ExecuteNonQuery();
// Insertion par lots
cmd.CommandText = @"INSERT INTO users (name, age) VALUES
('Bob', 30),
('Charlie', 28)";
cmd.ExecuteNonQuery();
// Requête de données
cmd.CommandText = "SELECT * FROM users WHERE age >= 25 ORDER BY age";
using var reader = cmd.ExecuteReader();
while (reader.Read())
{
Console.WriteLine($"id={reader["id"]}, name={reader["name"]}, age={reader["age"]}");
}Le mode serveur fournit un accès distant via TCP, prenant en charge plusieurs connexions clients simultanées.
Démarrer le serveur :
using NewLife.NovaDb.Server;
var svr = new NovaServer(3306) { DbPath = "./data" };
svr.Start();
Console.ReadLine();
svr.Stop("Manual shutdown");Connexion client ADO.NET (API identique au mode embarqué) :
using var conn = new NovaConnection
{
ConnectionString = "Server=127.0.0.1;Port=3306;Database=mydb"
};
conn.Open();
using var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT * FROM users WHERE age > 20";
using var reader = cmd.ExecuteReader();
while (reader.Read())
{
Console.WriteLine($"name={reader["name"]}");
}Les requêtes paramétrées empêchent l'injection SQL en utilisant des paramètres nommés @name :
using var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT * FROM users WHERE age > @minAge AND name LIKE @pattern";
cmd.Parameters.Add(new NovaParameter("@minAge", 18));
cmd.Parameters.Add(new NovaParameter("@pattern", "A%"));
using var reader = cmd.ExecuteReader();
while (reader.Read())
{
Console.WriteLine($"{reader["name"]}, {reader["age"]}");
}NovaDb implémente l'isolation des transactions basée sur MVCC avec un niveau d'isolation par défaut de Read Committed :
using var conn = new NovaConnection { ConnectionString = "Data Source=./mydb" };
conn.Open();
using var tx = conn.BeginTransaction();
try
{
using var cmd = conn.CreateCommand();
cmd.Transaction = tx;
cmd.CommandText = "UPDATE products SET stock = stock - 1 WHERE id = 1 AND stock > 0";
var affected = cmd.ExecuteNonQuery();
if (affected == 0) throw new InvalidOperationException("Insufficient stock");
cmd.CommandText = "INSERT INTO orders (product_id, amount) VALUES (1, 1)";
cmd.ExecuteNonQuery();
tx.Commit();
}
catch
{
tx.Rollback();
throw;
}using var cmd = conn.CreateCommand();
cmd.CommandText = @"
SELECT o.id, u.name, o.total
FROM orders o
INNER JOIN users u ON o.user_id = u.id
WHERE o.total > @minTotal
ORDER BY o.total DESC
LIMIT 10";
cmd.Parameters.Add(new NovaParameter("@minTotal", 100));
using var reader = cmd.ExecuteReader();
while (reader.Read())
{
Console.WriteLine($"Order {reader["id"]}: {reader["name"]} - ${reader["total"]}");
}| Paramètre | Exemple | Description |
|---|---|---|
Data Source |
Data Source=./mydb |
Mode embarqué, chemin du dossier de la base de données |
Server |
Server=127.0.0.1 |
Mode serveur, adresse du serveur |
Port |
Port=3306 |
Port du serveur (par défaut 3306) |
Database |
Database=mydb |
Nom de la base de données |
WalMode |
WalMode=Full |
Mode WAL (Full/Normal/None) |
ReadOnly |
ReadOnly=true |
Mode lecture seule |
Le moteur de séries temporelles (Flux Engine) est également accessible via ADO.NET + SQL. Spécifiez ENGINE=FLUX lors de la création des tables.
using var cmd = conn.CreateCommand();
cmd.CommandText = @"CREATE TABLE IF NOT EXISTS metrics (
timestamp DATETIME,
device_id STRING(50),
temperature DOUBLE,
humidity DOUBLE
) ENGINE=FLUX";
cmd.ExecuteNonQuery();// Insertion unique
cmd.CommandText = @"INSERT INTO metrics (timestamp, device_id, temperature, humidity)
VALUES (NOW(), 'sensor-001', 23.5, 65.0)";
cmd.ExecuteNonQuery();
// Insertion par lots
cmd.CommandText = @"INSERT INTO metrics (timestamp, device_id, temperature, humidity) VALUES
('2025-07-01 10:00:00', 'sensor-001', 22.1, 60.0),
('2025-07-01 10:01:00', 'sensor-001', 22.3, 61.0),
('2025-07-01 10:02:00', 'sensor-002', 25.0, 55.0)";
cmd.ExecuteNonQuery();cmd.CommandText = @"SELECT device_id, temperature, humidity, timestamp
FROM metrics
WHERE timestamp >= @start AND timestamp < @end
ORDER BY timestamp DESC";
cmd.Parameters.Add(new NovaParameter("@start", DateTime.Now.AddHours(-1)));
cmd.Parameters.Add(new NovaParameter("@end", DateTime.Now));
using var reader = cmd.ExecuteReader();
while (reader.Read())
{
Console.WriteLine($"[{reader["timestamp"]}] {reader["device_id"]}: " +
$"temp={reader["temperature"]}°C, humidity={reader["humidity"]}%");
}cmd.CommandText = @"SELECT device_id, COUNT(*) AS cnt, AVG(temperature) AS avg_temp,
MIN(temperature) AS min_temp, MAX(temperature) AS max_temp
FROM metrics
WHERE timestamp >= @start
GROUP BY device_id";
cmd.Parameters.Add(new NovaParameter("@start", DateTime.Now.AddDays(-1)));
using var reader = cmd.ExecuteReader();
while (reader.Read())
{
Console.WriteLine($"{reader["device_id"]}: avg={reader["avg_temp"]:F1}°C, " +
$"min={reader["min_temp"]}°C, max={reader["max_temp"]}°C, count={reader["cnt"]}");
}NovaDb implémente une file d'attente de messages de style Redis Stream basée sur le moteur de séries temporelles Flux. La file d'attente de messages est accessible via l'interface RPC NovaClient.
using NewLife.NovaDb.Client;
using var client = new NovaClient("tcp://127.0.0.1:3306");
client.Open();var affected = await client.ExecuteAsync(
"INSERT INTO order_events (timestamp, orderId, action, amount) " +
"VALUES (NOW(), 10001, 'created', 299.00)");
Console.WriteLine($"Message published, affected rows: {affected}");var messages = await client.QueryAsync<IDictionary<String, Object>[]>(
"SELECT * FROM order_events WHERE timestamp > @since ORDER BY timestamp LIMIT 10",
new { since = DateTime.Now.AddMinutes(-5) });var serverTime = await client.PingAsync();
Console.WriteLine($"Server connected: {serverTime}");
Console.WriteLine($"Is connected: {client.IsConnected}");- ID de message : Horodatage + numéro de séquence (auto-incrément dans la même milliseconde), ordonné globalement
- Groupe de consommateurs :
Topic/Stream+ConsumerGroup+Consumer+Pending - Fiabilité : At-Least-Once, entre dans Pending après lecture, Ack après succès métier
- Rétention des données : Prend en charge TTL (supprime automatiquement les anciens fragments par temps/taille de fichier)
- Messages différés : Spécifier la durée du délai ou l'heure de livraison exacte
- File d'attente de lettres mortes : Entre automatiquement dans la DLQ après dépassement du nombre maximal de tentatives
Le stockage KV est accessible via NovaClient. Spécifiez ENGINE=KV lors de la création des tables. Les tables KV ont un schéma fixe de Key + Value + TTL.
using var client = new NovaClient("tcp://127.0.0.1:3306");
client.Open();
await client.ExecuteAsync(@"CREATE TABLE IF NOT EXISTS session_cache (
Key STRING(200) PRIMARY KEY, Value BLOB, TTL DATETIME
) ENGINE=KV DEFAULT_TTL=7200");// Écriture (sémantique UPSERT)
await client.ExecuteAsync(
"INSERT INTO session_cache (Key, Value, TTL) VALUES ('session:1001', 'user-data', " +
"DATEADD(NOW(), 30, 'MINUTE')) ON DUPLICATE KEY UPDATE Value = 'user-data'");
// Lecture
var result = await client.QueryAsync<IDictionary<String, Object>[]>(
"SELECT Value FROM session_cache WHERE Key = 'session:1001' " +
"AND (TTL IS NULL OR TTL > NOW())");
// Suppression
await client.ExecuteAsync("DELETE FROM session_cache WHERE Key = 'session:1001'");await client.ExecuteAsync(
"INSERT INTO counters (Key, Value) VALUES ('page:views', 1) " +
"ON DUPLICATE KEY UPDATE Value = Value + 1");| Opération | Description |
|---|---|
Get |
Lire la valeur avec vérification paresseuse du TTL |
Set |
Définir la valeur avec TTL optionnel |
Add |
Ajouter uniquement lorsque la clé n'existe pas (verrou distribué) |
Delete |
Supprimer la clé |
Inc |
Incrément/décrément atomique (compteur) |
TTL |
Invisible automatiquement à l'expiration, nettoyage périodique en arrière-plan |
NovaDb fournit trois stratégies de persistance WAL :
| Mode | Description | Cas d'utilisation |
|---|---|---|
FULL |
Écriture disque synchrone, flush immédiatement à chaque commit | Scénarios financiers/de trading, sécurité des données la plus forte |
NORMAL |
Flush asynchrone 1s (par défaut) | La plupart des scénarios commerciaux, équilibre performance et sécurité |
NONE |
Entièrement asynchrone, pas de flush proactif | Scénarios de données temporaires/cache, débit le plus élevé |
Choisir un mode autre que synchrone (
FULL) signifie accepter une perte de données possible dans les scénarios de crash/panne de courant.
NovaDb prend en charge une architecture un maître, plusieurs esclaves avec réplication asynchrone des données via Binlog :
┌──────────┐ Binlog Sync ┌──────────┐
│ Master │ ──────────────→ │ Slave 1 │
│ (R/W) │ │ (R/O) │
└──────────┘ └──────────┘
│ Binlog Sync ┌──────────┐
└──────────────────────→ │ Slave 2 │
│ (R/O) │
└──────────┘
- Le nœud maître gère toutes les opérations d'écriture ; les nœuds esclaves fournissent des requêtes en lecture seule
- Réplication asynchrone via Binlog avec prise en charge de la reprise après interruption
- La couche applicative est responsable de la séparation lecture/écriture
| Version | Fonctionnalités prévues |
|---|---|
| v1.0 (Terminé) | Mode double embarqué+serveur, moteurs Nova/Flux/KV, SQL DDL/DML/SELECT/JOIN, transactions/MVCC, WAL/récupération, séparation chaud-froid, partitionnement, groupes de consommateurs MQ, ADO.NET Provider, synchronisation maître-esclave en cluster |
| v1.1 | Fonctions SQL de niveau P0 (chaîne/numérique/date/conversion/conditionnelle ~30 fonctions) |
| v1.2 | Lecture bloquante MQ, opérations KV Add/Inc, fonctions SQL de niveau P1 |
| v1.3 | Messages différés MQ, file d'attente de lettres mortes |
| v2.0 | Géocodage GeoPoint + type Vector (recherche vectorielle IA), observabilité et outils de gestion |
NovaDb ne poursuit pas la conformité complète à la norme SQL92, mais couvre le sous-ensemble de 80% couramment utilisé en affaires en échange des capacités différenciées suivantes :
| Différenciation | Description |
|---|---|
| Pur .NET géré | Aucune dépendance native, déploiement via xcopy, surcharge de sérialisation nulle dans le même processus avec les applications .NET |
| Mode double embarqué+serveur | Embarqué pour le développement/débogage comme SQLite, service autonome pour la production comme MySQL, même API |
| Dossier comme base de données | Copier le dossier pour terminer la migration/sauvegarde, aucun dump/restore nécessaire |
| Séparation d'index chaud-froid | Table de 10M lignes interrogeant uniquement les points chauds utilise < 20 Mo de mémoire, données froides déchargées automatiquement vers MMF |
| Intégration de quatre moteurs | Un seul composant couvre les scénarios courants SQLite + TDengine + Redis, réduit le nombre de composants opérationnels |
| Intégration native NewLife | Adaptation directe avec XCode ORM + ADO.NET, aucun pilote tiers nécessaire |