From a7660ec2d620579a1c2408058f194010c9d3f4cb Mon Sep 17 00:00:00 2001 From: Allen Cai Date: Wed, 4 Mar 2026 20:37:22 +0800 Subject: [PATCH 01/15] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=AD=97=E7=AC=A6?= =?UTF-8?q?=E4=B8=B2=E4=B8=8EUTF-8=E5=AD=97=E8=8A=82=E6=95=B0=E7=BB=84?= =?UTF-8?q?=E6=B1=A0=E5=8C=96=E4=B8=8E=E7=BC=96=E7=A0=81=E6=80=A7=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 本次提交引入 PooledUtf8Bytes 工具类,统一并池化字符串与 UTF-8 字节数组的编码解码,显著减少内存分配和 GC 压力。KV 存储、协议、日志等核心模块全面切换为池化字节数组,相关方法支持 ReadOnlySpan,提升高并发场景下的性能和资源利用率。兼容多 .NET 版本,优化了代码风格和静态方法使用,为后续扩展和维护打下基础。 --- NewLife.NovaDb/Core/IDataCodec.cs | 45 +-- .../Engine/Flux/FluxEngine.Persist.cs | 33 ++- NewLife.NovaDb/Engine/KV/KvStore.Persist.cs | 29 +- NewLife.NovaDb/Engine/KV/KvStore.cs | 53 +++- NewLife.NovaDb/Server/AuthManager.cs | 7 +- NewLife.NovaDb/Server/KvPacket.cs | 267 +++++++++++------- NewLife.NovaDb/Sql/SqlEngine.Expression.cs | 15 +- NewLife.NovaDb/Storage/DatabaseLock.cs | 7 +- NewLife.NovaDb/Utilities/PooledUtf8Bytes.cs | 119 ++++++++ NewLife.NovaDb/WAL/BinlogWriter.cs | 19 +- 10 files changed, 419 insertions(+), 175 deletions(-) create mode 100644 NewLife.NovaDb/Utilities/PooledUtf8Bytes.cs diff --git a/NewLife.NovaDb/Core/IDataCodec.cs b/NewLife.NovaDb/Core/IDataCodec.cs index c77e26d..ac8e77d 100644 --- a/NewLife.NovaDb/Core/IDataCodec.cs +++ b/NewLife.NovaDb/Core/IDataCodec.cs @@ -34,6 +34,8 @@ public class DefaultDataCodec : IDataCodec /// 非 NULL 标记字节 private const Byte NotNullFlag = 0x01; + private static readonly Encoding _encoding = Encoding.UTF8; + /// 编码值到二进制 /// 要编码的值 /// 数据类型 @@ -137,7 +139,7 @@ public Int32 GetEncodedLength(Object? value, DataType dataType) DataType.Int64 => 8, DataType.Double => 8, DataType.Decimal => 16, // 128-bit - DataType.String => 4 + Encoding.UTF8.GetByteCount((String)value), // 长度前缀 + UTF-8 + DataType.String => 4 + _encoding.GetByteCount((String)value), // 长度前缀 + UTF-8 DataType.Binary => 4 + ((Byte[])value).Length, // 长度前缀 + 数据 DataType.DateTime => 8, // Ticks DataType.GeoPoint => 16, // 2 × Double @@ -146,7 +148,7 @@ public Int32 GetEncodedLength(Object? value, DataType dataType) }; } - private Byte[] EncodeDecimal(Decimal value) + private static Byte[] EncodeDecimal(Decimal value) { var bits = Decimal.GetBits(value); var buffer = new Byte[16]; @@ -154,42 +156,42 @@ private Byte[] EncodeDecimal(Decimal value) return buffer; } - private Boolean DecodeBoolean(Byte[] buffer, Int32 offset) + private static Boolean DecodeBoolean(Byte[] buffer, Int32 offset) { if (buffer.Length < offset + 1) throw new ArgumentException($"Buffer too short to read Boolean (need {offset + 1} bytes, got {buffer.Length})"); return BitConverter.ToBoolean(buffer, offset); } - private Int32 DecodeInt32(Byte[] buffer, Int32 offset) + private static Int32 DecodeInt32(Byte[] buffer, Int32 offset) { if (buffer.Length < offset + 4) throw new ArgumentException($"Buffer too short to read Int32 (need {offset + 4} bytes, got {buffer.Length})"); return BitConverter.ToInt32(buffer, offset); } - private Int64 DecodeInt64(Byte[] buffer, Int32 offset) + private static Int64 DecodeInt64(Byte[] buffer, Int32 offset) { if (buffer.Length < offset + 8) throw new ArgumentException($"Buffer too short to read Int64 (need {offset + 8} bytes, got {buffer.Length})"); return BitConverter.ToInt64(buffer, offset); } - private Double DecodeDouble(Byte[] buffer, Int32 offset) + private static Double DecodeDouble(Byte[] buffer, Int32 offset) { if (buffer.Length < offset + 8) throw new ArgumentException($"Buffer too short to read Double (need {offset + 8} bytes, got {buffer.Length})"); return BitConverter.ToDouble(buffer, offset); } - private DateTime DecodeDateTime(Byte[] buffer, Int32 offset) + private static DateTime DecodeDateTime(Byte[] buffer, Int32 offset) { if (buffer.Length < offset + 8) throw new ArgumentException($"Buffer too short to read DateTime (need {offset + 8} bytes, got {buffer.Length})"); return new DateTime(BitConverter.ToInt64(buffer, offset)); } - private Decimal DecodeDecimal(Byte[] buffer, Int32 offset) + private static Decimal DecodeDecimal(Byte[] buffer, Int32 offset) { if (buffer.Length < offset + 16) throw new ArgumentException($"Buffer too short to read Decimal (need {offset + 16} bytes, got {buffer.Length})"); @@ -198,22 +200,23 @@ private Decimal DecodeDecimal(Byte[] buffer, Int32 offset) return new Decimal(bits); } - private Byte[] EncodeString(String value) + private static Byte[] EncodeString(String value) { - var utf8Bytes = System.Text.Encoding.UTF8.GetBytes(value); - var buffer = new Byte[4 + utf8Bytes.Length]; - Buffer.BlockCopy(BitConverter.GetBytes(utf8Bytes.Length), 0, buffer, 0, 4); - Buffer.BlockCopy(utf8Bytes, 0, buffer, 4, utf8Bytes.Length); + var valueBytesLength = _encoding.GetByteCount(value); + var buffer = new Byte[4 + valueBytesLength]; + Buffer.BlockCopy(BitConverter.GetBytes(buffer.Length), 0, buffer, 0, 4); + _encoding.GetBytes(value, buffer.AsSpan(4)); + return buffer; } - private String DecodeString(Byte[] buffer, Int32 offset) + private static String DecodeString(Byte[] buffer, Int32 offset) { var length = BitConverter.ToInt32(buffer, offset); - return System.Text.Encoding.UTF8.GetString(buffer, offset + 4, length); + return _encoding.GetString(buffer, offset + 4, length); } - private Byte[] EncodeByteArray(Byte[] value) + private static Byte[] EncodeByteArray(Byte[] value) { var buffer = new Byte[4 + value.Length]; Buffer.BlockCopy(BitConverter.GetBytes(value.Length), 0, buffer, 0, 4); @@ -221,7 +224,7 @@ private Byte[] EncodeByteArray(Byte[] value) return buffer; } - private Byte[] EncodeGeoPoint(GeoPoint value) + private static Byte[] EncodeGeoPoint(GeoPoint value) { var buffer = new Byte[16]; Buffer.BlockCopy(BitConverter.GetBytes(value.Latitude), 0, buffer, 0, 8); @@ -229,7 +232,7 @@ private Byte[] EncodeGeoPoint(GeoPoint value) return buffer; } - private GeoPoint DecodeGeoPoint(Byte[] buffer, Int32 offset) + private static GeoPoint DecodeGeoPoint(Byte[] buffer, Int32 offset) { if (buffer.Length < offset + 16) throw new ArgumentException($"Buffer too short to read GeoPoint (need {offset + 16} bytes, got {buffer.Length})"); @@ -238,7 +241,7 @@ private GeoPoint DecodeGeoPoint(Byte[] buffer, Int32 offset) return new GeoPoint(lat, lon); } - private Byte[] EncodeVector(Single[] value) + private static Byte[] EncodeVector(Single[] value) { var buffer = new Byte[4 + value.Length * 4]; Buffer.BlockCopy(BitConverter.GetBytes(value.Length), 0, buffer, 0, 4); @@ -246,7 +249,7 @@ private Byte[] EncodeVector(Single[] value) return buffer; } - private Single[] DecodeVector(Byte[] buffer, Int32 offset) + private static Single[] DecodeVector(Byte[] buffer, Int32 offset) { if (buffer.Length < offset + 4) throw new ArgumentException($"Buffer too short to read Vector length (need {offset + 4} bytes, got {buffer.Length})"); @@ -260,7 +263,7 @@ private Single[] DecodeVector(Byte[] buffer, Int32 offset) return result; } - private Byte[] DecodeByteArray(Byte[] buffer, Int32 offset) + private static Byte[] DecodeByteArray(Byte[] buffer, Int32 offset) { if (buffer.Length < offset + 4) throw new ArgumentException($"Buffer too short to read ByteArray length (need {offset + 4} bytes, got {buffer.Length})"); diff --git a/NewLife.NovaDb/Engine/Flux/FluxEngine.Persist.cs b/NewLife.NovaDb/Engine/Flux/FluxEngine.Persist.cs index 1b1f361..d02b0ed 100644 --- a/NewLife.NovaDb/Engine/Flux/FluxEngine.Persist.cs +++ b/NewLife.NovaDb/Engine/Flux/FluxEngine.Persist.cs @@ -1,4 +1,5 @@ -using System.Text; +using System.Text; +using NewLife.NovaDb.Utilities; using NewLife.Security; namespace NewLife.NovaDb.Engine.Flux; @@ -22,6 +23,7 @@ public partial class FluxEngine private const Byte RecordType_FluxAppend = 1; private const Byte RecordType_FluxPurge = 2; + private static readonly Encoding _encoding = Encoding.UTF8; private static readonly Byte[] FluxLogMagic = [(Byte)'N', (Byte)'F', (Byte)'L', (Byte)'G']; private const Int32 FluxLogHeaderSize = 32; @@ -120,7 +122,8 @@ private void PersistFluxAppend(FluxEntry entry) WriteString(bw, kv.Value); } - WriteFluxRecord(RecordType_FluxAppend, ms.ToArray()); + var data = ms.ToArray(); + WriteFluxRecord(RecordType_FluxAppend, data, 0, data.Length); } /// 持久化 Purge 记录(删除过期分区) @@ -129,26 +132,26 @@ private void PersistFluxPurge(String cutoffKey) { if (_fluxLogStream == null) return; - var data = Encoding.UTF8.GetBytes(cutoffKey); - WriteFluxRecord(RecordType_FluxPurge, data); + using var pooledBytes = _encoding.GetPooledEncodedBytes(cutoffKey); + WriteFluxRecord(RecordType_FluxPurge, pooledBytes.Buffer, 0, pooledBytes.Length); } /// 写入一条记录 - private void WriteFluxRecord(Byte recordType, Byte[] data) + private void WriteFluxRecord(Byte recordType, Byte[] data, int offset, int count) { - var recordLength = 1 + data.Length + 4; + var recordLength = 1 + count + 4; using var ms = new MemoryStream(4 + recordLength); using var bw = new BinaryWriter(ms); bw.Write(recordLength); bw.Write(recordType); - bw.Write(data); + bw.Write(data, offset, count); // CRC32 校验 - var checkBuffer = new Byte[1 + data.Length]; + var checkBuffer = new Byte[1 + count]; checkBuffer[0] = recordType; - Array.Copy(data, 0, checkBuffer, 1, data.Length); + Array.Copy(data, offset, checkBuffer, 1, count); var checksum = Crc32.Compute(checkBuffer, 0, checkBuffer.Length); bw.Write(checksum); @@ -165,9 +168,13 @@ private void WriteFluxRecord(Byte recordType, Byte[] data) /// 写入 UTF-8 字符串(长度前缀) private static void WriteString(BinaryWriter bw, String value) { - var bytes = Encoding.UTF8.GetBytes(value); + using var bytes = _encoding.GetPooledEncodedBytes(value); bw.Write(bytes.Length); - bw.Write(bytes); +#if NETSTANDARD2_1_OR_GREATER + bw.Write(bytes.AsSpan()); +#else + bw.Write(bytes.Buffer, 0, bytes.Length); +#endif } /// 读取 UTF-8 字符串(长度前缀) @@ -175,7 +182,7 @@ private static String ReadString(BinaryReader br) { var len = br.ReadInt32(); var bytes = br.ReadBytes(len); - return Encoding.UTF8.GetString(bytes); + return _encoding.GetString(bytes); } /// 写入字段值(带类型标签) @@ -324,7 +331,7 @@ private void ReplayFluxAppend(Byte[] body, Int32 offset, Int32 dataLength) /// 回放 Purge 记录 private void ReplayFluxPurge(Byte[] body, Int32 offset, Int32 dataLength) { - var cutoffKey = Encoding.UTF8.GetString(body, offset, dataLength); + var cutoffKey = _encoding.GetString(body, offset, dataLength); var toRemove = new List(); foreach (var key in _partitions.Keys) diff --git a/NewLife.NovaDb/Engine/KV/KvStore.Persist.cs b/NewLife.NovaDb/Engine/KV/KvStore.Persist.cs index b0dda7d..62379ce 100644 --- a/NewLife.NovaDb/Engine/KV/KvStore.Persist.cs +++ b/NewLife.NovaDb/Engine/KV/KvStore.Persist.cs @@ -5,6 +5,7 @@ using NewLife.Data; using NewLife.NovaDb.Core; using NewLife.NovaDb.Storage; +using NewLife.NovaDb.Utilities; using NewLife.Security; namespace NewLife.NovaDb.Engine.KV; @@ -144,10 +145,8 @@ private void FlushTimerCallback(Object? state) /// 过期时间(UTC) /// 是否按 WAL 模式自动刷盘 /// 值在文件中的偏移(若值为空则返回 -1) - private Int64 WriteSetRecordNoLock(String key, Byte[] value, DateTime expiresAt, Boolean autoFlush = true) + private Int64 WriteSetRecordNoLock(String key, ReadOnlySpan value, DateTime expiresAt, Boolean autoFlush = true) { - if (value == null) throw new ArgumentNullException(nameof(value)); - var valueOffset = WriteSetRecordToStream(_fileStream!, key, value, expiresAt); _writeCount++; if (autoFlush) FlushByWalMode(); @@ -160,13 +159,13 @@ private Int64 WriteSetRecordNoLock(String key, Byte[] value, DateTime expiresAt, /// 值,最少为空数组,不能为 null /// 过期时间(UTC) /// 值在文件中的偏移(若值为空则返回 -1) - private static Int64 WriteSetRecordToStream(FileStream target, String key, Byte[] value, DateTime expiresAt) + private static Int64 WriteSetRecordToStream(FileStream target, String key, ReadOnlySpan value, DateTime expiresAt) { - var keyBytes = Encoding.UTF8.GetBytes(key); + var pooledKeyBytes = _encoding.GetPooledEncodedBytes(key); var valueLen = value.Length; // Record: [TotalLength: 4B] [RecordType: 1B] [KeyLen: 2B] [Key] [ExpiresAt: 8B] [ValueLen: 4B] [Value] [CRC32: 4B] - var totalLength = 1 + 2 + keyBytes.Length + 8 + 4 + valueLen + 4; + var totalLength = 1 + 2 + pooledKeyBytes.Length + 8 + 4 + valueLen + 4; var recordSize = 4 + totalLength; var buf = ArrayPool.Shared.Rent(recordSize); @@ -176,8 +175,8 @@ private static Int64 WriteSetRecordToStream(FileStream target, String key, Byte[ writer.Write(totalLength); writer.Write((Byte)KvRecordType.Set); - writer.Write((UInt16)keyBytes.Length); - writer.Write(keyBytes); + writer.Write((UInt16)pooledKeyBytes.Length); + writer.Write(pooledKeyBytes.AsSpan()); writer.Write(expiresAt.Ticks); writer.Write(valueLen); writer.Write(value); @@ -191,10 +190,11 @@ private static Int64 WriteSetRecordToStream(FileStream target, String key, Byte[ target.Write(buf, 0, recordSize); // 计算值在文件中的偏移: recordStart + 4(TotalLen) + 1(Type) + 2(KeyLen) + key + 8(ExpiresAt) + 4(ValueLen) - return valueLen > 0 ? recordStart + 4 + 1 + 2 + keyBytes.Length + 8 + 4 : -1L; + return valueLen > 0 ? recordStart + 4 + 1 + 2 + pooledKeyBytes.Length + 8 + 4 : -1L; } finally { + pooledKeyBytes.Dispose(); ArrayPool.Shared.Return(buf); } } @@ -203,10 +203,10 @@ private static Int64 WriteSetRecordToStream(FileStream target, String key, Byte[ /// 键 private void WriteDeleteRecordNoLock(String key) { - var keyBytes = Encoding.UTF8.GetBytes(key); + var pooledKeyBytes = _encoding.GetPooledEncodedBytes(key); // Delete: [TotalLength: 4B] [RecordType: 1B] [KeyLen: 2B] [Key] [CRC32: 4B] - var totalLength = 1 + 2 + keyBytes.Length + 4; + var totalLength = 1 + 2 + pooledKeyBytes.Length + 4; var recordSize = 4 + totalLength; var buf = ArrayPool.Shared.Rent(recordSize); @@ -216,8 +216,8 @@ private void WriteDeleteRecordNoLock(String key) writer.Write(totalLength); writer.Write((Byte)KvRecordType.Delete); - writer.Write((UInt16)keyBytes.Length); - writer.Write(keyBytes); + writer.Write((UInt16)pooledKeyBytes.Length); + writer.Write(pooledKeyBytes.AsSpan()); var crc = Crc32.Compute(buf, 4, totalLength - 4); writer.Write(crc); @@ -227,6 +227,7 @@ private void WriteDeleteRecordNoLock(String key) } finally { + pooledKeyBytes.Dispose(); ArrayPool.Shared.Return(buf); } @@ -507,7 +508,7 @@ private void CompactNoLock() if (kvp.Value.IsExpired()) continue; using var pk = ReadValueFromDiskNoLock(kvp.Value); - var value = pk != null ? pk.GetSpan().ToArray() : []; + var value = pk != null ? pk.GetSpan() : ReadOnlySpan.Empty; var valueOffset = WriteSetRecordToStream(tempStream, kvp.Key, value, kvp.Value.ExpiresAt); newEntries[kvp.Key] = new KvEntry diff --git a/NewLife.NovaDb/Engine/KV/KvStore.cs b/NewLife.NovaDb/Engine/KV/KvStore.cs index 712fb89..89f76d3 100644 --- a/NewLife.NovaDb/Engine/KV/KvStore.cs +++ b/NewLife.NovaDb/Engine/KV/KvStore.cs @@ -1,9 +1,11 @@ using System.Collections.Concurrent; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Text; -using NewLife; using NewLife.Buffers; using NewLife.Data; using NewLife.NovaDb.Core; +using NewLife.NovaDb.Utilities; namespace NewLife.NovaDb.Engine.KV; @@ -18,6 +20,7 @@ namespace NewLife.NovaDb.Engine.KV; public partial class KvStore : IDisposable { #region 属性 + private static readonly Encoding _encoding = Encoding.UTF8; private readonly ConcurrentDictionary _data = new(StringComparer.Ordinal); #if NET9_0_OR_GREATER private readonly System.Threading.Lock _writeLock = new(); @@ -97,7 +100,15 @@ public void Dispose() /// 键 /// 值 /// 过期时间,null 表示使用默认 TTL(无默认则永不过期) + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Set(String key, Byte[]? value, TimeSpan? ttl = null) + => Set(key, value == null ? ReadOnlySpan.Empty : new ReadOnlySpan(value), ttl); + + /// 设置键值对 + /// 键 + /// 值 + /// 过期时间,null 表示使用默认 TTL(无默认则永不过期) + public void Set(String key, ReadOnlySpan value, TimeSpan? ttl = null) { if (String.IsNullOrEmpty(key)) throw new ArgumentException("键不能为空", nameof(key)); CheckDisposed(); @@ -107,11 +118,11 @@ public void Set(String key, Byte[]? value, TimeSpan? ttl = null) lock (_writeLock) { - var valueOffset = WriteSetRecordNoLock(key, value ?? [], expiresAt); + var valueOffset = WriteSetRecordNoLock(key, value, expiresAt); _data[key] = new KvEntry { ValueOffset = valueOffset, - ValueLength = value?.Length ?? 0, + ValueLength = value.Length, ExpiresAt = expiresAt, }; @@ -238,7 +249,8 @@ public void SetString(String key, String value, TimeSpan? ttl = null) { if (value == null) throw new ArgumentNullException(nameof(value)); - Set(key, value.GetBytes(), ttl); + using var pooledUtf8Bytes = _encoding.GetPooledEncodedBytes(value); + Set(key, pooledUtf8Bytes.AsSpan(), ttl); } /// 获取字符串值(UTF-8 解码) @@ -260,7 +272,15 @@ public void SetString(String key, String value, TimeSpan? ttl = null) /// 值 /// 过期时间 /// 添加成功返回 true,key 已存在且未过期返回 false - public Boolean Add(String key, Byte[] value, TimeSpan ttl) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Boolean Add(String key, Byte[] value, TimeSpan ttl) => Add(key, new ReadOnlySpan(value), ttl); + + /// 仅当 key 不存在时添加,返回是否成功(分布式锁场景) + /// 键 + /// 值 + /// 过期时间 + /// 添加成功返回 true,key 已存在且未过期返回 false + public Boolean Add(String key, ReadOnlySpan value, TimeSpan ttl) { if (String.IsNullOrEmpty(key)) throw new ArgumentException("键不能为空", nameof(key)); CheckDisposed(); @@ -293,7 +313,8 @@ public Boolean AddString(String key, String value, TimeSpan ttl) { if (value == null) throw new ArgumentNullException(nameof(value)); - return Add(key, Encoding.UTF8.GetBytes(value), ttl); + using var pooledUtf8Bytes = _encoding.GetPooledEncodedBytes(value); + return Add(key, pooledUtf8Bytes.AsSpan(), ttl); } /// 替换并返回旧值(原子操作) @@ -301,7 +322,16 @@ public Boolean AddString(String key, String value, TimeSpan ttl) /// 新值 /// 过期时间,null 表示保持原有 TTL /// 旧值池化数据包,不存在返回 null。调用方用完后需 Dispose + [MethodImpl(MethodImplOptions.AggressiveInlining)] public IOwnerPacket? Replace(String key, Byte[]? value, TimeSpan? ttl = null) + => Replace(key, value == null ? ReadOnlySpan.Empty : new ReadOnlySpan(value), ttl); + + /// 替换并返回旧值(原子操作) + /// 键 + /// 新值 + /// 过期时间,null 表示保持原有 TTL + /// 旧值池化数据包,不存在返回 null。调用方用完后需 Dispose + public IOwnerPacket? Replace(String key, ReadOnlySpan value, TimeSpan? ttl = null) { if (String.IsNullOrEmpty(key)) throw new ArgumentException("键不能为空", nameof(key)); CheckDisposed(); @@ -321,11 +351,11 @@ public Boolean AddString(String key, String value, TimeSpan ttl) expiresAt = ttl != null ? DateTime.UtcNow.Add(ttl.Value) : _defaultTtl != null ? DateTime.UtcNow.Add(_defaultTtl.Value) : DateTime.MaxValue; } - var valueOffset = WriteSetRecordNoLock(key, value ?? [], expiresAt); + var valueOffset = WriteSetRecordNoLock(key, value, expiresAt); _data[key] = new KvEntry { ValueOffset = valueOffset, - ValueLength = value?.Length ?? 0, + ValueLength = value.Length, ExpiresAt = expiresAt, }; @@ -466,11 +496,12 @@ public void SetAll(IDictionary values, TimeSpan? ttl = null) { if (String.IsNullOrEmpty(kvp.Key)) continue; - var valueOffset = WriteSetRecordNoLock(kvp.Key, kvp.Value ?? [], expiresAt); + var value = kvp.Value == null ? ReadOnlySpan.Empty : new ReadOnlySpan(kvp.Value); + var valueOffset = WriteSetRecordNoLock(kvp.Key, value, expiresAt); _data[kvp.Key] = new KvEntry { ValueOffset = valueOffset, - ValueLength = kvp.Value?.Length ?? 0, + ValueLength = value.Length, ExpiresAt = expiresAt, }; } @@ -549,7 +580,7 @@ public Boolean SetExpiration(String key, TimeSpan ttl) // 从磁盘读取当前值,以新 TTL 重新追加写入 using var valuePk = ReadValueFromDiskNoLock(index); - var value = valuePk?.ReadBytes() ?? []; + var value = valuePk == null ? ReadOnlySpan.Empty : new ReadOnlySpan(valuePk.ReadBytes()); var valueOffset = WriteSetRecordNoLock(key, value, newExpiresAt); _data[key] = new KvEntry { diff --git a/NewLife.NovaDb/Server/AuthManager.cs b/NewLife.NovaDb/Server/AuthManager.cs index 554c413..a2ff235 100644 --- a/NewLife.NovaDb/Server/AuthManager.cs +++ b/NewLife.NovaDb/Server/AuthManager.cs @@ -1,4 +1,5 @@ -using NewLife.NovaDb.Core; +using NewLife.NovaDb.Core; +using NewLife.NovaDb.Utilities; namespace NewLife.NovaDb.Server; @@ -233,9 +234,9 @@ public List GetAllUsers() /// 密码哈希(SHA256) private static String HashPassword(String password) { - var bytes = System.Text.Encoding.UTF8.GetBytes(password); using var sha256 = System.Security.Cryptography.SHA256.Create(); - var hash = sha256.ComputeHash(bytes); + using var bytes = System.Text.Encoding.UTF8.GetPooledEncodedBytes(password); + var hash = sha256.ComputeHash(bytes.Buffer, 0, bytes.Length); return Convert.ToBase64String(hash); } } diff --git a/NewLife.NovaDb/Server/KvPacket.cs b/NewLife.NovaDb/Server/KvPacket.cs index cfe13ac..d15e4f5 100644 --- a/NewLife.NovaDb/Server/KvPacket.cs +++ b/NewLife.NovaDb/Server/KvPacket.cs @@ -1,6 +1,7 @@ -using System.Text; +using System.Text; using NewLife.Buffers; using NewLife.Data; +using NewLife.NovaDb.Utilities; namespace NewLife.NovaDb.Server; @@ -16,150 +17,208 @@ namespace NewLife.NovaDb.Server; /// internal static class KvPacket { + private static readonly Encoding _encoding = Encoding.UTF8; + #region 编码请求 /// 编码 Set 请求 public static IPacket EncodeSet(String tableName, String key, Byte[]? value, Int32 ttlSeconds) { - var tableBytes = Encoding.UTF8.GetBytes(tableName ?? "default"); - var keyBytes = Encoding.UTF8.GetBytes(key); - var bufSize = 32 + tableBytes.Length + keyBytes.Length + (value?.Length ?? 0); - var buf = new Byte[bufSize]; - var writer = new SpanWriter(buf, 0, bufSize); - WriteString(ref writer, tableBytes); - WriteString(ref writer, keyBytes); - WriteNullableBytes(ref writer, value); - writer.Write(ttlSeconds); - return new ArrayPacket(buf, 0, writer.Position); + var tableBytes = _encoding.GetPooledEncodedBytes(tableName ?? "default"); + var keyBytes = _encoding.GetPooledEncodedBytes(key); + try + { + var bufSize = 32 + tableBytes.Length + keyBytes.Length + (value?.Length ?? 0); + var buf = new Byte[bufSize]; + var writer = new SpanWriter(buf, 0, bufSize); + WriteString(ref writer, tableBytes.AsSpan()); + WriteString(ref writer, keyBytes.AsSpan()); + WriteNullableBytes(ref writer, value); + writer.Write(ttlSeconds); + return new ArrayPacket(buf, 0, writer.Position); + } + finally + { + tableBytes.Dispose(); + keyBytes.Dispose(); + } } /// 编码 Get / Delete / Exists 请求(tableName + key) public static IPacket EncodeTableKey(String tableName, String key) { - var tableBytes = Encoding.UTF8.GetBytes(tableName ?? "default"); - var keyBytes = Encoding.UTF8.GetBytes(key); - var bufSize = 16 + tableBytes.Length + keyBytes.Length; - var buf = new Byte[bufSize]; - var writer = new SpanWriter(buf, 0, bufSize); - WriteString(ref writer, tableBytes); - WriteString(ref writer, keyBytes); - return new ArrayPacket(buf, 0, writer.Position); + var tableBytes = _encoding.GetPooledEncodedBytes(tableName ?? "default"); + var keyBytes = _encoding.GetPooledEncodedBytes(key); + try + { + var bufSize = 16 + tableBytes.Length + keyBytes.Length; + var buf = new Byte[bufSize]; + var writer = new SpanWriter(buf, 0, bufSize); + WriteString(ref writer, tableBytes.AsSpan()); + WriteString(ref writer, keyBytes.AsSpan()); + return new ArrayPacket(buf, 0, writer.Position); + } + finally + { + tableBytes.Dispose(); + keyBytes.Dispose(); + } } /// 编码仅含 tableName 的请求(GetCount / GetAllKeys / Clear) public static IPacket EncodeTableOnly(String tableName) { - var tableBytes = Encoding.UTF8.GetBytes(tableName ?? "default"); + using var tableBytes = _encoding.GetPooledEncodedBytes(tableName ?? "default"); var buf = new Byte[8 + tableBytes.Length]; var writer = new SpanWriter(buf, 0, buf.Length); - WriteString(ref writer, tableBytes); + WriteString(ref writer, tableBytes.AsSpan()); return new ArrayPacket(buf, 0, writer.Position); } /// 编码 SetExpire 请求(tableName + key + ttlSeconds) public static IPacket EncodeSetExpire(String tableName, String key, Int32 ttlSeconds) { - var tableBytes = Encoding.UTF8.GetBytes(tableName ?? "default"); - var keyBytes = Encoding.UTF8.GetBytes(key); - var bufSize = 16 + tableBytes.Length + keyBytes.Length; - var buf = new Byte[bufSize]; - var writer = new SpanWriter(buf, 0, bufSize); - WriteString(ref writer, tableBytes); - WriteString(ref writer, keyBytes); - writer.Write(ttlSeconds); - return new ArrayPacket(buf, 0, writer.Position); + var tableBytes = _encoding.GetPooledEncodedBytes(tableName ?? "default"); + var keyBytes = _encoding.GetPooledEncodedBytes(key); + try + { + var bufSize = 16 + tableBytes.Length + keyBytes.Length; + var buf = new Byte[bufSize]; + var writer = new SpanWriter(buf, 0, bufSize); + WriteString(ref writer, tableBytes.AsSpan()); + WriteString(ref writer, keyBytes.AsSpan()); + writer.Write(ttlSeconds); + return new ArrayPacket(buf, 0, writer.Position); + } + finally + { + tableBytes.Dispose(); + keyBytes.Dispose(); + } } /// 编码 Increment 请求(tableName + key + Int64 delta) public static IPacket EncodeIncrement(String tableName, String key, Int64 delta) { - var tableBytes = Encoding.UTF8.GetBytes(tableName ?? "default"); - var keyBytes = Encoding.UTF8.GetBytes(key); - var bufSize = 24 + tableBytes.Length + keyBytes.Length; - var buf = new Byte[bufSize]; - var writer = new SpanWriter(buf, 0, bufSize); - WriteString(ref writer, tableBytes); - WriteString(ref writer, keyBytes); - writer.Write(delta); - return new ArrayPacket(buf, 0, writer.Position); + var tableBytes = _encoding.GetPooledEncodedBytes(tableName ?? "default"); + var keyBytes = _encoding.GetPooledEncodedBytes(key); + try + { + var bufSize = 24 + tableBytes.Length + keyBytes.Length; + var buf = new Byte[bufSize]; + var writer = new SpanWriter(buf, 0, bufSize); + WriteString(ref writer, tableBytes.AsSpan()); + WriteString(ref writer, keyBytes.AsSpan()); + writer.Write(delta); + return new ArrayPacket(buf, 0, writer.Position); + } + finally + { + tableBytes.Dispose(); + keyBytes.Dispose(); + } } /// 编码 IncrementDouble 请求(tableName + key + Double delta) public static IPacket EncodeIncrementDouble(String tableName, String key, Double delta) { - var tableBytes = Encoding.UTF8.GetBytes(tableName ?? "default"); - var keyBytes = Encoding.UTF8.GetBytes(key); - var bufSize = 24 + tableBytes.Length + keyBytes.Length; - var buf = new Byte[bufSize]; - var writer = new SpanWriter(buf, 0, bufSize); - WriteString(ref writer, tableBytes); - WriteString(ref writer, keyBytes); - writer.Write(delta); - return new ArrayPacket(buf, 0, writer.Position); + var tableBytes = _encoding.GetPooledEncodedBytes(tableName ?? "default"); + var keyBytes = _encoding.GetPooledEncodedBytes(key); + try + { + var bufSize = 24 + tableBytes.Length + keyBytes.Length; + var buf = new Byte[bufSize]; + var writer = new SpanWriter(buf, 0, bufSize); + WriteString(ref writer, tableBytes.AsSpan()); + WriteString(ref writer, keyBytes.AsSpan()); + writer.Write(delta); + return new ArrayPacket(buf, 0, writer.Position); + } + finally + { + tableBytes.Dispose(); + keyBytes.Dispose(); + } } /// 编码 Search 请求(tableName + pattern + offset + count) public static IPacket EncodeSearch(String tableName, String pattern, Int32 offset, Int32 count) { - var tableBytes = Encoding.UTF8.GetBytes(tableName ?? "default"); - var patternBytes = Encoding.UTF8.GetBytes(pattern); - var bufSize = 24 + tableBytes.Length + patternBytes.Length; - var buf = new Byte[bufSize]; - var writer = new SpanWriter(buf, 0, bufSize); - WriteString(ref writer, tableBytes); - WriteString(ref writer, patternBytes); - writer.Write(offset); - writer.Write(count); - return new ArrayPacket(buf, 0, writer.Position); + var tableBytes = _encoding.GetPooledEncodedBytes(tableName ?? "default"); + var patternBytes = _encoding.GetPooledEncodedBytes(pattern); + try + { + var bufSize = 24 + tableBytes.Length + patternBytes.Length; + var buf = new Byte[bufSize]; + var writer = new SpanWriter(buf, 0, bufSize); + WriteString(ref writer, tableBytes.AsSpan()); + WriteString(ref writer, patternBytes.AsSpan()); + writer.Write(offset); + writer.Write(count); + return new ArrayPacket(buf, 0, writer.Position); + } + finally + { + tableBytes.Dispose(); + patternBytes.Dispose(); + } } /// 编码 DeleteByPattern 请求(tableName + pattern) public static IPacket EncodeDeleteByPattern(String tableName, String pattern) { - var tableBytes = Encoding.UTF8.GetBytes(tableName ?? "default"); - var patternBytes = Encoding.UTF8.GetBytes(pattern); - var bufSize = 16 + tableBytes.Length + patternBytes.Length; - var buf = new Byte[bufSize]; - var writer = new SpanWriter(buf, 0, bufSize); - WriteString(ref writer, tableBytes); - WriteString(ref writer, patternBytes); - return new ArrayPacket(buf, 0, writer.Position); + var tableBytes = _encoding.GetPooledEncodedBytes(tableName ?? "default"); + var patternBytes = _encoding.GetPooledEncodedBytes(pattern); + try + { + var bufSize = 16 + tableBytes.Length + patternBytes.Length; + var buf = new Byte[bufSize]; + var writer = new SpanWriter(buf, 0, bufSize); + WriteString(ref writer, tableBytes.AsSpan()); + WriteString(ref writer, patternBytes.AsSpan()); + return new ArrayPacket(buf, 0, writer.Position); + } + finally + { + tableBytes.Dispose(); + patternBytes.Dispose(); + } } /// 编码 GetAll 请求(tableName + keys[]) public static IPacket EncodeGetAll(String tableName, String[] keys) { - var tableBytes = Encoding.UTF8.GetBytes(tableName ?? "default"); - var keyBytesArr = keys.Select(k => Encoding.UTF8.GetBytes(k)).ToArray(); - var bufSize = 16 + tableBytes.Length + 4 + keyBytesArr.Sum(b => 4 + b.Length); + using var tableBytes = _encoding.GetPooledEncodedBytes(tableName ?? "default"); + var bufSize = 16 + tableBytes.Length + 4 + keys.Sum(k => 4 + _encoding.GetByteCount(k)); var buf = new Byte[bufSize]; var writer = new SpanWriter(buf, 0, bufSize); - WriteString(ref writer, tableBytes); + WriteString(ref writer, tableBytes.AsSpan()); writer.Write(keys.Length); - foreach (var kb in keyBytesArr) - WriteString(ref writer, kb); + foreach (var key in keys) + { + using var pooledKeyBytes = _encoding.GetPooledEncodedBytes(key); + WriteString(ref writer, pooledKeyBytes.AsSpan()); + } + return new ArrayPacket(buf, 0, writer.Position); } /// 编码 SetAll 请求(tableName + ttlSeconds + values dict) public static IPacket EncodeSetAll(String tableName, IDictionary values, Int32 ttlSeconds) { - var tableBytes = Encoding.UTF8.GetBytes(tableName ?? "default"); - var kvBytesArr = values.Select(kvp => ( - Key: Encoding.UTF8.GetBytes(kvp.Key), - Val: kvp.Value - )).ToArray(); - var bufSize = 32 + tableBytes.Length - + kvBytesArr.Sum(kv => 8 + kv.Key.Length + (kv.Val?.Length ?? 0)); + using var tableBytes = _encoding.GetPooledEncodedBytes(tableName ?? "default"); + var valueBytesLengthTotal = values.Sum(kvp => 8 + _encoding.GetByteCount(kvp.Key) + kvp.Value?.Length ?? 0); + var bufSize = 32 + tableBytes.Length + valueBytesLengthTotal; var buf = new Byte[bufSize]; var writer = new SpanWriter(buf, 0, bufSize); - WriteString(ref writer, tableBytes); + WriteString(ref writer, tableBytes.AsSpan()); writer.Write(ttlSeconds); writer.Write(values.Count); - foreach (var (keyBytes, val) in kvBytesArr) + foreach (var keyValuePair in values) { - WriteString(ref writer, keyBytes); - WriteNullableBytes(ref writer, val); + using (var pooledKeyBytes = _encoding.GetPooledEncodedBytes(keyValuePair.Key)) + WriteString(ref writer, pooledKeyBytes.AsSpan()); + WriteNullableBytes(ref writer, keyValuePair.Value); } return new ArrayPacket(buf, 0, writer.Position); } @@ -306,7 +365,7 @@ public static IPacket EncodeDouble(Double value) } /// 编码空响应(用于 Clear 等无返回值操作,以及 Get 未找到键时的空包) - public static IPacket EncodeEmpty() => new ArrayPacket(new Byte[0]); + public static IPacket EncodeEmpty() => new ArrayPacket(EmptyBytes); /// 编码字符串数组响应(Int32 count + 每项 EncodedString) public static IPacket EncodeStringArray(String[] keys) @@ -318,22 +377,24 @@ public static IPacket EncodeStringArray(String[] keys) return new ArrayPacket(emptyBuf, 0, 4); } - var keyBytesArr = keys.Select(k => Encoding.UTF8.GetBytes(k)).ToArray(); - var bufSize = 4 + keyBytesArr.Sum(b => 4 + b.Length); + var bufSize = 4 + keys.Sum(k => 4 + _encoding.GetByteCount(k)); var buf = new Byte[bufSize]; var writer = new SpanWriter(buf, 0, bufSize); writer.Write(keys.Length); - foreach (var kb in keyBytesArr) - WriteString(ref writer, kb); + foreach (var key in keys) + { + using var pooledKeyBytes = _encoding.GetPooledEncodedBytes(key); + WriteString(ref writer, pooledKeyBytes.AsSpan()); + } + return new ArrayPacket(buf, 0, writer.Position); } /// 编码 GetAll 响应(Int32 keyCount + 每项 key EncodedString + value nullable bytes) public static IPacket EncodeGetAllResponse(String[] keys, IDictionary data) { - var keyBytesArr = keys.Select(k => Encoding.UTF8.GetBytes(k)).ToArray(); // 预估大小:count(4) + n * (keyLen(4)+keyBytes + valueFlag(1)+valueLen(4)+valueBytes) - var estSize = 4 + keys.Length * 16 + keyBytesArr.Sum(b => b.Length); + var estSize = 4 + keys.Length * 16 + keys.Sum(k => _encoding.GetByteCount(k)); foreach (var key in keys) { if (data.TryGetValue(key, out var pk) && pk != null) @@ -343,10 +404,12 @@ public static IPacket EncodeGetAllResponse(String[] keys, IDictionary 0 ? reader.ReadBytes(len).ToArray() : new Byte[0]; + var valueBytes = len > 0 ? reader.ReadBytes(len).ToArray() : EmptyBytes; result[key] = valueBytes; } else @@ -444,6 +507,12 @@ private static void WriteString(ref SpanWriter writer, Byte[] strBytes) if (strBytes.Length > 0) writer.Write(strBytes); } + private static void WriteString(ref SpanWriter writer, ReadOnlySpan strBytes) + { + writer.WriteEncodedInt(strBytes.Length); + if (strBytes.Length > 0) writer.Write(strBytes); + } + private static void WriteNullableBytes(ref SpanWriter writer, Byte[]? value) { if (value == null) @@ -459,16 +528,22 @@ private static String ReadString(ref SpanReader reader) { var len = reader.ReadEncodedInt(); if (len <= 0) return String.Empty; - return Encoding.UTF8.GetString(reader.ReadBytes(len)); + return _encoding.GetString(reader.ReadBytes(len)); } private static Byte[]? ReadNullableBytes(ref SpanReader reader) { var len = reader.ReadEncodedInt(); if (len < 0) return null; - if (len == 0) return new Byte[0]; + if (len == 0) return EmptyBytes; return reader.ReadBytes(len).ToArray(); } +#if NET45 + private static readonly byte[] EmptyBytes = new byte[0]; +#else + private static readonly byte[] EmptyBytes = Array.Empty(); +#endif + #endregion } diff --git a/NewLife.NovaDb/Sql/SqlEngine.Expression.cs b/NewLife.NovaDb/Sql/SqlEngine.Expression.cs index 732c3bb..583e222 100644 --- a/NewLife.NovaDb/Sql/SqlEngine.Expression.cs +++ b/NewLife.NovaDb/Sql/SqlEngine.Expression.cs @@ -1,5 +1,6 @@ -using NewLife.NovaDb.Core; +using NewLife.NovaDb.Core; using NewLife.NovaDb.Engine; +using NewLife.NovaDb.Utilities; namespace NewLife.NovaDb.Sql; @@ -589,8 +590,8 @@ private Boolean EvaluateGroupCondition(SqlExpression expr, List group if (args.Count < 1 || args[0] == null) return null; using (var md5 = System.Security.Cryptography.MD5.Create()) { - var bytes = System.Text.Encoding.UTF8.GetBytes(Convert.ToString(args[0])!); - var hash = md5.ComputeHash(bytes); + using var bytes = Convert.ToString(args[0]).ToPooledUtf8Bytes(); + var hash = md5.ComputeHash(bytes.Buffer, 0, bytes.Length); return BitConverter.ToString(hash).Replace("-", String.Empty).ToLower(); } @@ -598,8 +599,8 @@ private Boolean EvaluateGroupCondition(SqlExpression expr, List group if (args.Count < 1 || args[0] == null) return null; using (var sha1 = System.Security.Cryptography.SHA1.Create()) { - var bytes = System.Text.Encoding.UTF8.GetBytes(Convert.ToString(args[0])!); - var hash = sha1.ComputeHash(bytes); + using var bytes = Convert.ToString(args[0]).ToPooledUtf8Bytes(); + var hash = sha1.ComputeHash(bytes.Buffer, 0, bytes.Length); return BitConverter.ToString(hash).Replace("-", String.Empty).ToLower(); } @@ -613,8 +614,8 @@ private Boolean EvaluateGroupCondition(SqlExpression expr, List group _ => System.Security.Cryptography.SHA256.Create() }) { - var bytes = System.Text.Encoding.UTF8.GetBytes(Convert.ToString(args[0])!); - var hash = sha2.ComputeHash(bytes); + using var bytes = Convert.ToString(args[0]).ToPooledUtf8Bytes(); + var hash = sha2.ComputeHash(bytes.Buffer, 0, bytes.Length); return BitConverter.ToString(hash).Replace("-", String.Empty).ToLower(); } diff --git a/NewLife.NovaDb/Storage/DatabaseLock.cs b/NewLife.NovaDb/Storage/DatabaseLock.cs index 9b54291..c41cbd1 100644 --- a/NewLife.NovaDb/Storage/DatabaseLock.cs +++ b/NewLife.NovaDb/Storage/DatabaseLock.cs @@ -1,3 +1,5 @@ +using NewLife.NovaDb.Utilities; + namespace NewLife.NovaDb.Storage; /// 数据库文件锁,实现跨进程单写者协调 @@ -51,9 +53,8 @@ public Boolean TryAcquire() // 写入进程信息 var pid = System.Diagnostics.Process.GetCurrentProcess().Id; - var info = System.Text.Encoding.UTF8.GetBytes( - $"PID={pid}, Time={DateTime.Now:yyyy-MM-dd HH:mm:ss}"); - _lockStream.Write(info, 0, info.Length); + using (var info = $"PID={pid}, Time={DateTime.Now:yyyy-MM-dd HH:mm:ss}".ToPooledUtf8Bytes()) + _lockStream.Write(info.Buffer, 0, info.Length); _lockStream.Flush(); return true; diff --git a/NewLife.NovaDb/Utilities/PooledUtf8Bytes.cs b/NewLife.NovaDb/Utilities/PooledUtf8Bytes.cs new file mode 100644 index 0000000..1e16f74 --- /dev/null +++ b/NewLife.NovaDb/Utilities/PooledUtf8Bytes.cs @@ -0,0 +1,119 @@ +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Text; + +namespace NewLife.NovaDb.Utilities +{ + /// + /// 使用对象池管理 UTF-8 编码的字节数组,避免频繁分配和垃圾回收。 + /// + internal struct PooledUtf8Bytes : IDisposable + { + private static readonly Encoding Encoding = Encoding.UTF8; +#if NET45 + private static readonly byte[] EmptyBytes = new byte[0]; +#else + private static readonly byte[] EmptyBytes = Array.Empty(); +#endif + + /// + /// UTF-8 编码的字节数组长度,表示有效数据的长度。
+ /// 数组长度可能大于此值,因为它是从 对象池租用的。 + ///
+ public int Length { get; private set; } + + /// + /// 获取 UTF-8 编码的字节数组,有效数据的长度由 属性决定。
+ /// 使用完毕后应调用 方法归还数组到对象池。 + ///
+ public byte[] Buffer { get; private set; } + + public PooledUtf8Bytes() + { + Length = 0; + Buffer = EmptyBytes; + } + + internal PooledUtf8Bytes(byte[] pooledBytes, int length) + { + Length = length; + Buffer = pooledBytes; + } + +#if NETSTANDARD2_1_OR_GREATER + public PooledUtf8Bytes(ReadOnlySpan value) + { + if (value.IsEmpty) + { + Length = 0; + Buffer = EmptyBytes; + } + else + { + Length = Encoding.GetByteCount(value); // GetByteCount函数在.NET Standard 2.0版本中不支持 ReadOnlySpan 参数 + Buffer = ArrayPool.Shared.Rent(Length); + Encoding.GetBytes(value, Buffer); + } + } +#endif + + public PooledUtf8Bytes(string value) + { + if (string.IsNullOrEmpty(value)) + { + Length = 0; + Buffer = EmptyBytes; + } + else + { + Length = Encoding.GetByteCount(value); + Buffer = ArrayPool.Shared.Rent(Length); + Encoding.GetBytes(value, 0, value.Length, Buffer, 0); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ReadOnlySpan AsSpan() => Length == 0 ? ReadOnlySpan.Empty : Buffer.AsSpan(0, Length); + + public void Dispose() + { + if (Buffer == null || Buffer.Length == 0) return; + ArrayPool.Shared.Return(Buffer); + Buffer = EmptyBytes; + Length = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator ReadOnlySpan(PooledUtf8Bytes pooledBytes) => pooledBytes.AsSpan(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator PooledUtf8Bytes(string value) => new PooledUtf8Bytes(value); + } + + /// + /// 提供字符串与 UTF-8 编码字节数组之间的转换扩展方法,使用对象池管理字节数组以提高性能。 + /// + internal static class EncodingExtensions + { + /// + /// 将字符串转换为使用对象池管理的 UTF-8 编码字节数组。 + /// + /// 要转换的字符串。 + /// 返回一个 实例,包含 UTF-8 编码的字节数组。 + public static PooledUtf8Bytes ToPooledUtf8Bytes(this string value) => new PooledUtf8Bytes(value); + + /// + /// 将字符串转换为使用对象池管理的指定编码的字节数组。 + /// + /// 要使用的编码。 + /// 要转换的字符串。 + /// 返回一个 实例,包含指定编码的字节数组。 + public static PooledUtf8Bytes GetPooledEncodedBytes(this Encoding encoding, string value) + { + var length = encoding.GetByteCount(value); + var pooledBytes = ArrayPool.Shared.Rent(length); + encoding.GetBytes(value, 0, value.Length, pooledBytes, 0); + return new PooledUtf8Bytes(pooledBytes, length); + } + } +} diff --git a/NewLife.NovaDb/WAL/BinlogWriter.cs b/NewLife.NovaDb/WAL/BinlogWriter.cs index 31e4295..618b9fe 100644 --- a/NewLife.NovaDb/WAL/BinlogWriter.cs +++ b/NewLife.NovaDb/WAL/BinlogWriter.cs @@ -1,4 +1,5 @@ -using System.Text; +using System.Text; +using NewLife.NovaDb.Utilities; using NewLife.Security; namespace NewLife.NovaDb.WAL; @@ -225,14 +226,18 @@ private void WriteEvent(BinlogEventType eventType, String sql, Int32 affectedRow bw.Write(DateTime.UtcNow.Ticks); // 数据库名 - var dbBytes = Encoding.UTF8.GetBytes(_database); - bw.Write(dbBytes.Length); - bw.Write(dbBytes); + using (var dbBytes = _database.ToPooledUtf8Bytes()) + { + bw.Write(dbBytes.Length); + bw.Write(dbBytes.Buffer, 0, dbBytes.Length); + } // SQL 文本 - var sqlBytes = Encoding.UTF8.GetBytes(sql ?? ""); - bw.Write(sqlBytes.Length); - bw.Write(sqlBytes); + using (var sqlBytes = (sql ?? "").ToPooledUtf8Bytes()) + { + bw.Write(sqlBytes.Length); + bw.Write(sqlBytes.Buffer, 0, sqlBytes.Length); + } bw.Write(affectedRows); From 93ed327443ff1c1437ec019082d98146f55e56c6 Mon Sep 17 00:00:00 2001 From: Allen Cai Date: Wed, 4 Mar 2026 22:13:08 +0800 Subject: [PATCH 02/15] =?UTF-8?q?=E4=BA=8C=E8=BF=9B=E5=88=B6=E5=BA=8F?= =?UTF-8?q?=E5=88=97=E5=8C=96=E4=BC=98=E5=8C=96=EF=BC=9A=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E7=94=A8BinaryPrimitives=E5=86=99=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 统一采用BinaryPrimitives进行小端序写入,替换BitConverter,提升性能和兼容性。针对.NET Standard 2.1+,大量使用Span和stackalloc减少内存分配。优化了字符串、字节数组、GeoPoint、向量等类型的序列化方式。BinlogWriter、WalWriter等日志相关类的头部、长度、校验和写入也做了相应调整。 --- NewLife.NovaDb/Core/CompressionCodec.cs | 10 ++++- NewLife.NovaDb/Core/IDataCodec.cs | 33 +++++++++++----- NewLife.NovaDb/Engine/KV/KvStore.cs | 10 ++++- NewLife.NovaDb/Sql/SqlEngine.Expression.cs | 6 +-- NewLife.NovaDb/WAL/BinlogWriter.cs | 45 ++++++++++++++++++---- NewLife.NovaDb/WAL/WalWriter.cs | 12 +++++- 6 files changed, 91 insertions(+), 25 deletions(-) diff --git a/NewLife.NovaDb/Core/CompressionCodec.cs b/NewLife.NovaDb/Core/CompressionCodec.cs index a12abb7..657f3d7 100644 --- a/NewLife.NovaDb/Core/CompressionCodec.cs +++ b/NewLife.NovaDb/Core/CompressionCodec.cs @@ -1,3 +1,4 @@ +using System.Buffers.Binary; using System.IO.Compression; namespace NewLife.NovaDb.Core; @@ -38,8 +39,15 @@ public Byte[] Compress(Byte[] data) output.WriteByte((Byte)Algorithm); // 写入原始长度(4 字节,用于预分配解压缓冲区) - var lenBytes = BitConverter.GetBytes(data.Length); +#if NETSTANDARD2_1_OR_GREATER + Span lenBytes = stackalloc Byte[4]; + BinaryPrimitives.WriteInt32LittleEndian(lenBytes, data.Length); + output.Write(lenBytes); +#else + var lenBytes = new Byte[4]; + BinaryPrimitives.WriteInt32LittleEndian(lenBytes, data.Length); output.Write(lenBytes, 0, 4); +#endif // 压缩 using (var compressStream = CreateCompressStream(output)) diff --git a/NewLife.NovaDb/Core/IDataCodec.cs b/NewLife.NovaDb/Core/IDataCodec.cs index ac8e77d..f923f67 100644 --- a/NewLife.NovaDb/Core/IDataCodec.cs +++ b/NewLife.NovaDb/Core/IDataCodec.cs @@ -1,4 +1,6 @@ -using System.Text; +using System.Buffers.Binary; +using System.Runtime.InteropServices; +using System.Text; namespace NewLife.NovaDb.Core; @@ -204,9 +206,8 @@ private static Byte[] EncodeString(String value) { var valueBytesLength = _encoding.GetByteCount(value); var buffer = new Byte[4 + valueBytesLength]; - Buffer.BlockCopy(BitConverter.GetBytes(buffer.Length), 0, buffer, 0, 4); + BinaryPrimitives.WriteInt32LittleEndian(buffer.AsSpan(0, 4), valueBytesLength); _encoding.GetBytes(value, buffer.AsSpan(4)); - return buffer; } @@ -219,16 +220,16 @@ private static String DecodeString(Byte[] buffer, Int32 offset) private static Byte[] EncodeByteArray(Byte[] value) { var buffer = new Byte[4 + value.Length]; - Buffer.BlockCopy(BitConverter.GetBytes(value.Length), 0, buffer, 0, 4); - Buffer.BlockCopy(value, 0, buffer, 4, value.Length); + BinaryPrimitives.WriteInt32LittleEndian(buffer.AsSpan(0, 4), value.Length); + value.AsSpan().CopyTo(buffer.AsSpan(4)); return buffer; } private static Byte[] EncodeGeoPoint(GeoPoint value) { var buffer = new Byte[16]; - Buffer.BlockCopy(BitConverter.GetBytes(value.Latitude), 0, buffer, 0, 8); - Buffer.BlockCopy(BitConverter.GetBytes(value.Longitude), 0, buffer, 8, 8); + WriteDoubleLittleEndian(buffer.AsSpan(0, 8), value.Latitude); + WriteDoubleLittleEndian(buffer.AsSpan(8, 8), value.Longitude); return buffer; } @@ -243,9 +244,11 @@ private static GeoPoint DecodeGeoPoint(Byte[] buffer, Int32 offset) private static Byte[] EncodeVector(Single[] value) { - var buffer = new Byte[4 + value.Length * 4]; - Buffer.BlockCopy(BitConverter.GetBytes(value.Length), 0, buffer, 0, 4); - Buffer.BlockCopy(value, 0, buffer, 4, value.Length * 4); + var byteLen = checked(value.Length * sizeof(float)); + var buffer = new Byte[sizeof(int) + byteLen]; + BinaryPrimitives.WriteInt32LittleEndian(buffer.AsSpan(0, sizeof(int)), value.Length); + ReadOnlySpan srcBytes = MemoryMarshal.AsBytes(value.AsSpan()); + srcBytes.CopyTo(buffer.AsSpan(sizeof(int))); return buffer; } @@ -278,4 +281,14 @@ private static Byte[] DecodeByteArray(Byte[] buffer, Int32 offset) Buffer.BlockCopy(buffer, offset + 4, result, 0, length); return result; } + + private static void WriteDoubleLittleEndian(Span destination, double value) + { +#if NET6_0_OR_GREATER + BinaryPrimitives.WriteDoubleLittleEndian(destination, value); +#else + var bits = BitConverter.DoubleToInt64Bits(value); + BinaryPrimitives.WriteInt64LittleEndian(destination, bits); +#endif + } } diff --git a/NewLife.NovaDb/Engine/KV/KvStore.cs b/NewLife.NovaDb/Engine/KV/KvStore.cs index 89f76d3..61f5eb6 100644 --- a/NewLife.NovaDb/Engine/KV/KvStore.cs +++ b/NewLife.NovaDb/Engine/KV/KvStore.cs @@ -394,7 +394,10 @@ public Int64 Inc(String key, Int64 delta = 1, TimeSpan? ttl = null) expiresAt = ttl != null ? DateTime.UtcNow.Add(ttl.Value) : _defaultTtl != null ? DateTime.UtcNow.Add(_defaultTtl.Value) : DateTime.MaxValue; } - var valueBytes = BitConverter.GetBytes(newValue); + const int length = sizeof(long); + Span valueBytes = stackalloc byte[length]; + Unsafe.WriteUnaligned(ref MemoryMarshal.GetReference(valueBytes), newValue); + var valueOffset = WriteSetRecordNoLock(key, valueBytes, expiresAt); _data[key] = new KvEntry { @@ -437,7 +440,10 @@ public Double IncDouble(String key, Double delta, TimeSpan? ttl = null) expiresAt = ttl != null ? DateTime.UtcNow.Add(ttl.Value) : _defaultTtl != null ? DateTime.UtcNow.Add(_defaultTtl.Value) : DateTime.MaxValue; } - var valueBytes = BitConverter.GetBytes(newValue); + const int length = sizeof(double); + Span valueBytes = stackalloc byte[length]; + Unsafe.WriteUnaligned(ref MemoryMarshal.GetReference(valueBytes), newValue); + var valueOffset = WriteSetRecordNoLock(key, valueBytes, expiresAt); _data[key] = new KvEntry { diff --git a/NewLife.NovaDb/Sql/SqlEngine.Expression.cs b/NewLife.NovaDb/Sql/SqlEngine.Expression.cs index 583e222..01ac9db 100644 --- a/NewLife.NovaDb/Sql/SqlEngine.Expression.cs +++ b/NewLife.NovaDb/Sql/SqlEngine.Expression.cs @@ -590,7 +590,7 @@ private Boolean EvaluateGroupCondition(SqlExpression expr, List group if (args.Count < 1 || args[0] == null) return null; using (var md5 = System.Security.Cryptography.MD5.Create()) { - using var bytes = Convert.ToString(args[0]).ToPooledUtf8Bytes(); + using var bytes = Convert.ToString(args[0])!.ToPooledUtf8Bytes(); var hash = md5.ComputeHash(bytes.Buffer, 0, bytes.Length); return BitConverter.ToString(hash).Replace("-", String.Empty).ToLower(); } @@ -599,7 +599,7 @@ private Boolean EvaluateGroupCondition(SqlExpression expr, List group if (args.Count < 1 || args[0] == null) return null; using (var sha1 = System.Security.Cryptography.SHA1.Create()) { - using var bytes = Convert.ToString(args[0]).ToPooledUtf8Bytes(); + using var bytes = Convert.ToString(args[0])!.ToPooledUtf8Bytes(); var hash = sha1.ComputeHash(bytes.Buffer, 0, bytes.Length); return BitConverter.ToString(hash).Replace("-", String.Empty).ToLower(); } @@ -614,7 +614,7 @@ private Boolean EvaluateGroupCondition(SqlExpression expr, List group _ => System.Security.Cryptography.SHA256.Create() }) { - using var bytes = Convert.ToString(args[0]).ToPooledUtf8Bytes(); + using var bytes = Convert.ToString(args[0])!.ToPooledUtf8Bytes(); var hash = sha2.ComputeHash(bytes.Buffer, 0, bytes.Length); return BitConverter.ToString(hash).Replace("-", String.Empty).ToLower(); } diff --git a/NewLife.NovaDb/WAL/BinlogWriter.cs b/NewLife.NovaDb/WAL/BinlogWriter.cs index 618b9fe..34b5b3c 100644 --- a/NewLife.NovaDb/WAL/BinlogWriter.cs +++ b/NewLife.NovaDb/WAL/BinlogWriter.cs @@ -1,4 +1,5 @@ -using System.Text; +using System.Buffers.Binary; +using System.Text; using NewLife.NovaDb.Utilities; using NewLife.Security; @@ -154,17 +155,29 @@ private void OpenBinlogFile() /// 写入文件头 private void WriteHeader() { +#if NETSTANDARD2_1_OR_GREATER + Span header = stackalloc Byte[HeaderSize]; + BinlogMagic.AsSpan().CopyTo(header.Slice(0, 4)); + header[4] = 1; // Version + + // 写入文件索引 + BinaryPrimitives.WriteInt32LittleEndian(header.Slice(8, 4), _fileIndex); + + _stream!.Position = 0; + _stream.Write(header); + _stream.Flush(); +#else var header = new Byte[HeaderSize]; - Array.Copy(BinlogMagic, 0, header, 0, 4); + BinlogMagic.AsSpan().CopyTo(header.AsSpan(0, 4)); header[4] = 1; // Version // 写入文件索引 - var indexBytes = BitConverter.GetBytes(_fileIndex); - Array.Copy(indexBytes, 0, header, 8, 4); + BinaryPrimitives.WriteInt32LittleEndian(header.AsSpan(8, 4), _fileIndex); _stream!.Position = 0; _stream.Write(header, 0, header.Length); _stream.Flush(); +#endif } /// 校验文件头 @@ -173,9 +186,13 @@ private void ValidateHeader() if (_stream!.Length < HeaderSize) return; _stream.Position = 0; +#if NETSTANDARD2_1_OR_GREATER + Span header = stackalloc Byte[HeaderSize]; + if (_stream.Read(header) < HeaderSize) return; +#else var header = new Byte[HeaderSize]; if (_stream.Read(header, 0, header.Length) < HeaderSize) return; - +#endif if (header[0] != BinlogMagic[0] || header[1] != BinlogMagic[1] || header[2] != BinlogMagic[2] || header[3] != BinlogMagic[3]) throw new InvalidOperationException("Invalid Binlog file header"); @@ -251,13 +268,27 @@ private void WriteEvent(BinlogEventType eventType, String sql, Int32 affectedRow _stream.Position = _stream.Length; - var lenBuf = BitConverter.GetBytes(recordLength); +#if NETSTANDARD2_1_OR_GREATER + Span lenBuf = stackalloc Byte[4]; + BinaryPrimitives.WriteInt32LittleEndian(lenBuf, recordLength); + _stream.Write(lenBuf); + _stream.Write(data.AsSpan()); + + Span csumBuf = stackalloc Byte[4]; + BinaryPrimitives.WriteUInt32LittleEndian(csumBuf, checksum); + _stream.Write(csumBuf); + _stream.Flush(); +#else + var lenBuf = new Byte[4]; + BinaryPrimitives.WriteInt32LittleEndian(lenBuf.AsSpan(), recordLength); _stream.Write(lenBuf, 0, 4); _stream.Write(data, 0, data.Length); - var csumBuf = BitConverter.GetBytes(checksum); + var csumBuf = new Byte[4]; + BinaryPrimitives.WriteUInt32LittleEndian(csumBuf.AsSpan(), checksum); _stream.Write(csumBuf, 0, 4); _stream.Flush(); +#endif _position++; } diff --git a/NewLife.NovaDb/WAL/WalWriter.cs b/NewLife.NovaDb/WAL/WalWriter.cs index 9cb67bc..d7ccd51 100644 --- a/NewLife.NovaDb/WAL/WalWriter.cs +++ b/NewLife.NovaDb/WAL/WalWriter.cs @@ -1,4 +1,5 @@ -using NewLife.Data; +using System.Buffers.Binary; +using NewLife.Data; using NewLife.NovaDb.Core; namespace NewLife.NovaDb.WAL; @@ -89,8 +90,15 @@ public UInt64 Write(WalRecord record) pk.TryGetArray(out var segment); // 写入长度前缀(4 字节) - var lengthPrefix = BitConverter.GetBytes(pk.Length); +#if NETSTANDARD2_1_OR_GREATER + Span lengthPrefix = stackalloc Byte[4]; + BinaryPrimitives.WriteInt32LittleEndian(lengthPrefix, pk.Length); + _fileStream.Write(lengthPrefix); +#else + var lengthPrefix = new Byte[4]; + BinaryPrimitives.WriteInt32LittleEndian(lengthPrefix, pk.Length); _fileStream.Write(lengthPrefix, 0, 4); +#endif // 写入记录数据 _fileStream.Write(segment.Array!, segment.Offset, segment.Count); From 60566ff0128b05d311538e4b2459f5f944256260 Mon Sep 17 00:00:00 2001 From: Allen Cai Date: Wed, 4 Mar 2026 22:15:16 +0800 Subject: [PATCH 03/15] =?UTF-8?q?=E9=87=8D=E6=9E=84=E5=AD=97=E8=8A=82?= =?UTF-8?q?=E6=95=B0=E7=BB=84=E6=B1=A0=E5=8C=96=E7=AE=A1=E7=90=86=EF=BC=8C?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E4=B8=BAPooledBytes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 删除PooledUtf8Bytes相关实现,新增PooledBytes结构体和EncodingExtensions扩展类,实现更通用的对象池字节数组管理与编码转换。 --- .../Utilities/EncodingExtensions.cs | 34 +++++ NewLife.NovaDb/Utilities/PooledBytes.cs | 58 +++++++++ NewLife.NovaDb/Utilities/PooledUtf8Bytes.cs | 119 ------------------ 3 files changed, 92 insertions(+), 119 deletions(-) create mode 100644 NewLife.NovaDb/Utilities/EncodingExtensions.cs create mode 100644 NewLife.NovaDb/Utilities/PooledBytes.cs delete mode 100644 NewLife.NovaDb/Utilities/PooledUtf8Bytes.cs diff --git a/NewLife.NovaDb/Utilities/EncodingExtensions.cs b/NewLife.NovaDb/Utilities/EncodingExtensions.cs new file mode 100644 index 0000000..4ef01ff --- /dev/null +++ b/NewLife.NovaDb/Utilities/EncodingExtensions.cs @@ -0,0 +1,34 @@ +using System.Buffers; +using System.Text; + +namespace NewLife.NovaDb.Utilities +{ + /// + /// 提供字符串与 UTF-8 编码字节数组之间的转换扩展方法,使用对象池管理字节数组以提高性能。 + /// + internal static class EncodingExtensions + { + private static readonly Encoding Encoding = Encoding.UTF8; + + /// + /// 将字符串转换为使用对象池管理的 UTF-8 编码字节数组。 + /// + /// 要转换的字符串。 + /// 返回一个 实例,包含 UTF-8 编码的字节数组。 + public static PooledBytes ToPooledUtf8Bytes(this string value) => Encoding.GetPooledEncodedBytes(value); + + /// + /// 将字符串转换为使用对象池管理的指定编码的字节数组。 + /// + /// 要使用的编码。 + /// 要转换的字符串。 + /// 返回一个 实例,包含指定编码的字节数组。 + public static PooledBytes GetPooledEncodedBytes(this Encoding encoding, string value) + { + var length = encoding.GetByteCount(value); + var pooledBytes = ArrayPool.Shared.Rent(length); + encoding.GetBytes(value, 0, value.Length, pooledBytes, 0); + return new PooledBytes(pooledBytes, length); + } + } +} \ No newline at end of file diff --git a/NewLife.NovaDb/Utilities/PooledBytes.cs b/NewLife.NovaDb/Utilities/PooledBytes.cs new file mode 100644 index 0000000..ac990e5 --- /dev/null +++ b/NewLife.NovaDb/Utilities/PooledBytes.cs @@ -0,0 +1,58 @@ +using System.Buffers; +using System.Runtime.CompilerServices; + +namespace NewLife.NovaDb.Utilities +{ + /// + /// 使用对象池管理字节数组,避免频繁分配和垃圾回收。 + /// + internal struct PooledBytes : IDisposable + { +#if NET45 + private static readonly Byte[] EmptyBytes = new Byte[0]; +#else + private static readonly Byte[] EmptyBytes = Array.Empty(); +#endif + + public static readonly PooledBytes Empty = new(); + + /// + /// 字节数组的有效数据长度。
+ /// 字节数组的长度可能大于此值,因为它是从 对象池租用的。 + ///
+ public Int32 Length { get; private set; } + + /// + /// 获取字节数组。
+ /// 注意:有效数据的长度由 属性决定。
+ /// 使用完毕后应调用 方法归还数组到对象池。 + ///
+ public Byte[] Buffer { get; private set; } + + public PooledBytes() + { + Length = 0; + Buffer = EmptyBytes; + } + + internal PooledBytes(Byte[] pooledBytes, Int32 length) + { + Length = length; + Buffer = pooledBytes; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ReadOnlySpan AsSpan() => Length == 0 ? ReadOnlySpan.Empty : Buffer.AsSpan(0, Length); + + public void Dispose() + { + if (Buffer == null || Buffer.Length == 0) return; + ArrayPool.Shared.Return(Buffer); + Buffer = EmptyBytes; + Length = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator ReadOnlySpan(PooledBytes pooled) => pooled.AsSpan(); + } +} diff --git a/NewLife.NovaDb/Utilities/PooledUtf8Bytes.cs b/NewLife.NovaDb/Utilities/PooledUtf8Bytes.cs deleted file mode 100644 index 1e16f74..0000000 --- a/NewLife.NovaDb/Utilities/PooledUtf8Bytes.cs +++ /dev/null @@ -1,119 +0,0 @@ -using System.Buffers; -using System.Runtime.CompilerServices; -using System.Text; - -namespace NewLife.NovaDb.Utilities -{ - /// - /// 使用对象池管理 UTF-8 编码的字节数组,避免频繁分配和垃圾回收。 - /// - internal struct PooledUtf8Bytes : IDisposable - { - private static readonly Encoding Encoding = Encoding.UTF8; -#if NET45 - private static readonly byte[] EmptyBytes = new byte[0]; -#else - private static readonly byte[] EmptyBytes = Array.Empty(); -#endif - - /// - /// UTF-8 编码的字节数组长度,表示有效数据的长度。
- /// 数组长度可能大于此值,因为它是从 对象池租用的。 - ///
- public int Length { get; private set; } - - /// - /// 获取 UTF-8 编码的字节数组,有效数据的长度由 属性决定。
- /// 使用完毕后应调用 方法归还数组到对象池。 - ///
- public byte[] Buffer { get; private set; } - - public PooledUtf8Bytes() - { - Length = 0; - Buffer = EmptyBytes; - } - - internal PooledUtf8Bytes(byte[] pooledBytes, int length) - { - Length = length; - Buffer = pooledBytes; - } - -#if NETSTANDARD2_1_OR_GREATER - public PooledUtf8Bytes(ReadOnlySpan value) - { - if (value.IsEmpty) - { - Length = 0; - Buffer = EmptyBytes; - } - else - { - Length = Encoding.GetByteCount(value); // GetByteCount函数在.NET Standard 2.0版本中不支持 ReadOnlySpan 参数 - Buffer = ArrayPool.Shared.Rent(Length); - Encoding.GetBytes(value, Buffer); - } - } -#endif - - public PooledUtf8Bytes(string value) - { - if (string.IsNullOrEmpty(value)) - { - Length = 0; - Buffer = EmptyBytes; - } - else - { - Length = Encoding.GetByteCount(value); - Buffer = ArrayPool.Shared.Rent(Length); - Encoding.GetBytes(value, 0, value.Length, Buffer, 0); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ReadOnlySpan AsSpan() => Length == 0 ? ReadOnlySpan.Empty : Buffer.AsSpan(0, Length); - - public void Dispose() - { - if (Buffer == null || Buffer.Length == 0) return; - ArrayPool.Shared.Return(Buffer); - Buffer = EmptyBytes; - Length = 0; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static implicit operator ReadOnlySpan(PooledUtf8Bytes pooledBytes) => pooledBytes.AsSpan(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static implicit operator PooledUtf8Bytes(string value) => new PooledUtf8Bytes(value); - } - - /// - /// 提供字符串与 UTF-8 编码字节数组之间的转换扩展方法,使用对象池管理字节数组以提高性能。 - /// - internal static class EncodingExtensions - { - /// - /// 将字符串转换为使用对象池管理的 UTF-8 编码字节数组。 - /// - /// 要转换的字符串。 - /// 返回一个 实例,包含 UTF-8 编码的字节数组。 - public static PooledUtf8Bytes ToPooledUtf8Bytes(this string value) => new PooledUtf8Bytes(value); - - /// - /// 将字符串转换为使用对象池管理的指定编码的字节数组。 - /// - /// 要使用的编码。 - /// 要转换的字符串。 - /// 返回一个 实例,包含指定编码的字节数组。 - public static PooledUtf8Bytes GetPooledEncodedBytes(this Encoding encoding, string value) - { - var length = encoding.GetByteCount(value); - var pooledBytes = ArrayPool.Shared.Rent(length); - encoding.GetBytes(value, 0, value.Length, pooledBytes, 0); - return new PooledUtf8Bytes(pooledBytes, length); - } - } -} From 68d564b21324732c40a424f39de18cbad6e28ddc Mon Sep 17 00:00:00 2001 From: Allen Cai Date: Wed, 4 Mar 2026 22:48:49 +0800 Subject: [PATCH 04/15] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E5=A4=B4=E8=AF=BB=E5=86=99=EF=BC=8C=E6=8F=90=E5=8D=87=E6=80=A7?= =?UTF-8?q?=E8=83=BD=E5=B9=B6=E5=85=BC=E5=AE=B9=E5=A4=9A=E7=89=88=E6=9C=AC?= =?UTF-8?q?.NET?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 针对文件头的读写逻辑进行优化:在 .NET Standard 2.1 及以上版本下,采用 stackalloc 和 Span 结合 Stream 的新 API,减少堆分配并提升效率;低版本则保持原有 Byte[] 方式,确保兼容性。相关方法均做了条件编译适配。 --- .../Engine/Flux/FluxEngine.Persist.cs | 23 ++++++++++++++++--- NewLife.NovaDb/Engine/KV/KvStore.Persist.cs | 23 ++++++++++++++++++- NewLife.NovaDb/Engine/NovaTable.Persist.cs | 23 +++++++++++++++++-- 3 files changed, 63 insertions(+), 6 deletions(-) diff --git a/NewLife.NovaDb/Engine/Flux/FluxEngine.Persist.cs b/NewLife.NovaDb/Engine/Flux/FluxEngine.Persist.cs index d02b0ed..8394d0f 100644 --- a/NewLife.NovaDb/Engine/Flux/FluxEngine.Persist.cs +++ b/NewLife.NovaDb/Engine/Flux/FluxEngine.Persist.cs @@ -1,4 +1,5 @@ -using System.Text; +using System.Runtime.InteropServices.ComTypes; +using System.Text; using NewLife.NovaDb.Utilities; using NewLife.Security; @@ -65,12 +66,24 @@ private void OpenFluxLog() /// 写入文件头 private void WriteFluxLogHeader() { +#if NETSTANDARD2_1_OR_GREATER + Span header = stackalloc Byte[FluxLogHeaderSize]; + FluxLogMagic.AsSpan().CopyTo(header.Slice(0, 4)); +#else var header = new Byte[FluxLogHeaderSize]; - Array.Copy(FluxLogMagic, 0, header, 0, 4); + FluxLogMagic.AsSpan().CopyTo(header.AsSpan(0, 4)); +#endif + header[4] = 1; // Version _fluxLogStream!.Position = 0; + +#if NETSTANDARD2_1_OR_GREATER + _fluxLogStream.Write(header); +#else _fluxLogStream.Write(header, 0, header.Length); +#endif + _fluxLogStream.Flush(); } @@ -80,9 +93,13 @@ private void ValidateFluxLogHeader() if (_fluxLogStream!.Length < FluxLogHeaderSize) return; _fluxLogStream.Position = 0; +#if NETSTANDARD2_1_OR_GREATER + Span header = stackalloc Byte[FluxLogHeaderSize]; + if (_fluxLogStream.Read(header) < FluxLogHeaderSize) return; +#else var header = new Byte[FluxLogHeaderSize]; if (_fluxLogStream.Read(header, 0, header.Length) < FluxLogHeaderSize) return; - +#endif if (header[0] != FluxLogMagic[0] || header[1] != FluxLogMagic[1] || header[2] != FluxLogMagic[2] || header[3] != FluxLogMagic[3]) throw new InvalidOperationException("Invalid Flux log file header"); diff --git a/NewLife.NovaDb/Engine/KV/KvStore.Persist.cs b/NewLife.NovaDb/Engine/KV/KvStore.Persist.cs index 62379ce..0d28508 100644 --- a/NewLife.NovaDb/Engine/KV/KvStore.Persist.cs +++ b/NewLife.NovaDb/Engine/KV/KvStore.Persist.cs @@ -88,11 +88,20 @@ private void WriteFileHeader() CreateTime = DateTime.UtcNow, }; +#if NETSTANDARD2_1_OR_GREATER + Span buf = stackalloc Byte[FileHeaderSize]; +#else var buf = new Byte[FileHeaderSize]; +#endif header.Write(buf); _fileStream!.Position = 0; + +#if NETSTANDARD2_1_OR_GREATER + _fileStream.Write(buf); +#else _fileStream.Write(buf, 0, buf.Length); +#endif _fileStream.Flush(); } @@ -102,8 +111,14 @@ private void ValidateFileHeader() if (_fileStream!.Length < FileHeaderSize) return; _fileStream.Position = 0; + +#if NETSTANDARD2_1_OR_GREATER + Span buf = stackalloc Byte[FileHeaderSize]; + if (_fileStream.Read(buf) < FileHeaderSize) return; +#else var buf = new Byte[FileHeaderSize]; if (_fileStream.Read(buf, 0, buf.Length) < FileHeaderSize) return; +#endif var header = FileHeader.Read(buf); if (header.FileType != FileType.KvData) @@ -499,9 +514,15 @@ private void CompactNoLock() PageSize = 1, CreateTime = DateTime.UtcNow, }; +#if NETSTANDARD2_1_OR_GREATER + Span headerBuf = stackalloc Byte[FileHeaderSize]; + header.Write(headerBuf); + tempStream.Write(headerBuf); +#else var headerBuf = new Byte[FileHeaderSize]; header.Write(headerBuf); tempStream.Write(headerBuf, 0, headerBuf.Length); +#endif foreach (var kvp in _data) { @@ -554,5 +575,5 @@ private void CompactNoLock() _compacting = false; } } - #endregion +#endregion } diff --git a/NewLife.NovaDb/Engine/NovaTable.Persist.cs b/NewLife.NovaDb/Engine/NovaTable.Persist.cs index e0db25f..41ed88c 100644 --- a/NewLife.NovaDb/Engine/NovaTable.Persist.cs +++ b/NewLife.NovaDb/Engine/NovaTable.Persist.cs @@ -1,4 +1,4 @@ -using NewLife.NovaDb.Core; +using NewLife.NovaDb.Core; using NewLife.Security; namespace NewLife.NovaDb.Engine; @@ -64,12 +64,24 @@ private void OpenRowLog() /// 写入行日志文件头(32 字节) private void WriteRowLogHeader() { +#if NETSTANDARD2_1_OR_GREATER + Span header = stackalloc Byte[RowLogHeaderSize]; + RowLogMagic.AsSpan().CopyTo(header.Slice(0, 4)); +#else var header = new Byte[RowLogHeaderSize]; - Array.Copy(RowLogMagic, 0, header, 0, 4); + RowLogMagic.AsSpan().CopyTo(header.AsSpan(0, 4)); +#endif + header[4] = 1; // Version = 1 _rowLogStream!.Position = 0; + +#if NETSTANDARD2_1_OR_GREATER + _rowLogStream.Write(header); +#else _rowLogStream.Write(header, 0, header.Length); +#endif + _rowLogStream.Flush(); } @@ -79,8 +91,15 @@ private void ValidateRowLogHeader() if (_rowLogStream!.Length < RowLogHeaderSize) return; _rowLogStream.Position = 0; + +#if NETSTANDARD2_1_OR_GREATER + Span header = stackalloc Byte[RowLogHeaderSize]; + var read = _rowLogStream.Read(header); +#else var header = new Byte[RowLogHeaderSize]; var read = _rowLogStream.Read(header, 0, header.Length); +#endif + if (read < RowLogHeaderSize) return; // 校验魔数 From 55777805b565dcc5e5145dc810b9d9f5c261ac66 Mon Sep 17 00:00:00 2001 From: Allen Cai Date: Thu, 5 Mar 2026 12:45:15 +0800 Subject: [PATCH 05/15] =?UTF-8?q?=E6=94=AF=E6=8C=81=20.NET=20Core=203.1=20?= =?UTF-8?q?=E7=9A=84=20Span/stackalloc=20=E6=9D=A1=E4=BB=B6=E7=BC=96?= =?UTF-8?q?=E8=AF=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将条件编译宏由 #if NETSTANDARD2_1_OR_GREATER 扩展为 #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER,使 stackalloc、Span 等高性能代码在 .NET Core 3.1 及以上版本均可启用。涉及文件头写入、数据读取、字符串处理、ID 解析等多处条件编译判断,提升了兼容性和性能。 --- NewLife.NovaDb/Core/CompressionCodec.cs | 2 +- NewLife.NovaDb/Engine/Flux/FluxEngine.Persist.cs | 8 ++++---- NewLife.NovaDb/Engine/Flux/FluxEntry.cs | 2 +- NewLife.NovaDb/Engine/Flux/MessageId.cs | 4 ++-- NewLife.NovaDb/Engine/KV/KvStore.Persist.cs | 8 ++++---- NewLife.NovaDb/Engine/NovaTable.Persist.cs | 6 +++--- NewLife.NovaDb/WAL/BinlogWriter.cs | 6 +++--- NewLife.NovaDb/WAL/WalWriter.cs | 2 +- 8 files changed, 19 insertions(+), 19 deletions(-) diff --git a/NewLife.NovaDb/Core/CompressionCodec.cs b/NewLife.NovaDb/Core/CompressionCodec.cs index 657f3d7..1e724a4 100644 --- a/NewLife.NovaDb/Core/CompressionCodec.cs +++ b/NewLife.NovaDb/Core/CompressionCodec.cs @@ -39,7 +39,7 @@ public Byte[] Compress(Byte[] data) output.WriteByte((Byte)Algorithm); // 写入原始长度(4 字节,用于预分配解压缓冲区) -#if NETSTANDARD2_1_OR_GREATER +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER Span lenBytes = stackalloc Byte[4]; BinaryPrimitives.WriteInt32LittleEndian(lenBytes, data.Length); output.Write(lenBytes); diff --git a/NewLife.NovaDb/Engine/Flux/FluxEngine.Persist.cs b/NewLife.NovaDb/Engine/Flux/FluxEngine.Persist.cs index 8394d0f..7d27126 100644 --- a/NewLife.NovaDb/Engine/Flux/FluxEngine.Persist.cs +++ b/NewLife.NovaDb/Engine/Flux/FluxEngine.Persist.cs @@ -66,7 +66,7 @@ private void OpenFluxLog() /// 写入文件头 private void WriteFluxLogHeader() { -#if NETSTANDARD2_1_OR_GREATER +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER Span header = stackalloc Byte[FluxLogHeaderSize]; FluxLogMagic.AsSpan().CopyTo(header.Slice(0, 4)); #else @@ -78,7 +78,7 @@ private void WriteFluxLogHeader() _fluxLogStream!.Position = 0; -#if NETSTANDARD2_1_OR_GREATER +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER _fluxLogStream.Write(header); #else _fluxLogStream.Write(header, 0, header.Length); @@ -93,7 +93,7 @@ private void ValidateFluxLogHeader() if (_fluxLogStream!.Length < FluxLogHeaderSize) return; _fluxLogStream.Position = 0; -#if NETSTANDARD2_1_OR_GREATER +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER Span header = stackalloc Byte[FluxLogHeaderSize]; if (_fluxLogStream.Read(header) < FluxLogHeaderSize) return; #else @@ -187,7 +187,7 @@ private static void WriteString(BinaryWriter bw, String value) { using var bytes = _encoding.GetPooledEncodedBytes(value); bw.Write(bytes.Length); -#if NETSTANDARD2_1_OR_GREATER +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER bw.Write(bytes.AsSpan()); #else bw.Write(bytes.Buffer, 0, bytes.Length); diff --git a/NewLife.NovaDb/Engine/Flux/FluxEntry.cs b/NewLife.NovaDb/Engine/Flux/FluxEntry.cs index 412ad20..4b6d50d 100644 --- a/NewLife.NovaDb/Engine/Flux/FluxEntry.cs +++ b/NewLife.NovaDb/Engine/Flux/FluxEntry.cs @@ -30,7 +30,7 @@ public static (Int64 timestamp, Int32 seq) ParseMessageId(String id) if (dashIndex < 0) throw new FormatException($"Invalid message ID format: '{id}'"); -#if NETSTANDARD2_1_OR_GREATER +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER var timestamp = Int64.Parse(id.AsSpan(0, dashIndex)); var seq = Int32.Parse(id.AsSpan(dashIndex + 1)); return (timestamp, seq); diff --git a/NewLife.NovaDb/Engine/Flux/MessageId.cs b/NewLife.NovaDb/Engine/Flux/MessageId.cs index 6107190..dd0fcb1 100644 --- a/NewLife.NovaDb/Engine/Flux/MessageId.cs +++ b/NewLife.NovaDb/Engine/Flux/MessageId.cs @@ -51,7 +51,7 @@ public static MessageId Parse(String value) if (dashIndex < 0) throw new FormatException($"Invalid MessageId format: '{value}'"); -#if NETSTANDARD2_1_OR_GREATER +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER var timestamp = Int64.Parse(value.AsSpan(0, dashIndex)); var sequence = Int32.Parse(value.AsSpan(dashIndex + 1)); return new MessageId(timestamp, sequence); @@ -93,7 +93,7 @@ public Boolean Equals(MessageId? other) /// 哈希码 public override Int32 GetHashCode() { -#if NETSTANDARD2_1_OR_GREATER +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER return HashCode.Combine(Timestamp, Sequence); #else unchecked diff --git a/NewLife.NovaDb/Engine/KV/KvStore.Persist.cs b/NewLife.NovaDb/Engine/KV/KvStore.Persist.cs index 0d28508..0f4de0a 100644 --- a/NewLife.NovaDb/Engine/KV/KvStore.Persist.cs +++ b/NewLife.NovaDb/Engine/KV/KvStore.Persist.cs @@ -88,7 +88,7 @@ private void WriteFileHeader() CreateTime = DateTime.UtcNow, }; -#if NETSTANDARD2_1_OR_GREATER +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER Span buf = stackalloc Byte[FileHeaderSize]; #else var buf = new Byte[FileHeaderSize]; @@ -97,7 +97,7 @@ private void WriteFileHeader() _fileStream!.Position = 0; -#if NETSTANDARD2_1_OR_GREATER +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER _fileStream.Write(buf); #else _fileStream.Write(buf, 0, buf.Length); @@ -112,7 +112,7 @@ private void ValidateFileHeader() _fileStream.Position = 0; -#if NETSTANDARD2_1_OR_GREATER +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER Span buf = stackalloc Byte[FileHeaderSize]; if (_fileStream.Read(buf) < FileHeaderSize) return; #else @@ -514,7 +514,7 @@ private void CompactNoLock() PageSize = 1, CreateTime = DateTime.UtcNow, }; -#if NETSTANDARD2_1_OR_GREATER +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER Span headerBuf = stackalloc Byte[FileHeaderSize]; header.Write(headerBuf); tempStream.Write(headerBuf); diff --git a/NewLife.NovaDb/Engine/NovaTable.Persist.cs b/NewLife.NovaDb/Engine/NovaTable.Persist.cs index 41ed88c..356ad02 100644 --- a/NewLife.NovaDb/Engine/NovaTable.Persist.cs +++ b/NewLife.NovaDb/Engine/NovaTable.Persist.cs @@ -64,7 +64,7 @@ private void OpenRowLog() /// 写入行日志文件头(32 字节) private void WriteRowLogHeader() { -#if NETSTANDARD2_1_OR_GREATER +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER Span header = stackalloc Byte[RowLogHeaderSize]; RowLogMagic.AsSpan().CopyTo(header.Slice(0, 4)); #else @@ -76,7 +76,7 @@ private void WriteRowLogHeader() _rowLogStream!.Position = 0; -#if NETSTANDARD2_1_OR_GREATER +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER _rowLogStream.Write(header); #else _rowLogStream.Write(header, 0, header.Length); @@ -92,7 +92,7 @@ private void ValidateRowLogHeader() _rowLogStream.Position = 0; -#if NETSTANDARD2_1_OR_GREATER +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER Span header = stackalloc Byte[RowLogHeaderSize]; var read = _rowLogStream.Read(header); #else diff --git a/NewLife.NovaDb/WAL/BinlogWriter.cs b/NewLife.NovaDb/WAL/BinlogWriter.cs index 34b5b3c..c7b06b2 100644 --- a/NewLife.NovaDb/WAL/BinlogWriter.cs +++ b/NewLife.NovaDb/WAL/BinlogWriter.cs @@ -155,7 +155,7 @@ private void OpenBinlogFile() /// 写入文件头 private void WriteHeader() { -#if NETSTANDARD2_1_OR_GREATER +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER Span header = stackalloc Byte[HeaderSize]; BinlogMagic.AsSpan().CopyTo(header.Slice(0, 4)); header[4] = 1; // Version @@ -186,7 +186,7 @@ private void ValidateHeader() if (_stream!.Length < HeaderSize) return; _stream.Position = 0; -#if NETSTANDARD2_1_OR_GREATER +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER Span header = stackalloc Byte[HeaderSize]; if (_stream.Read(header) < HeaderSize) return; #else @@ -268,7 +268,7 @@ private void WriteEvent(BinlogEventType eventType, String sql, Int32 affectedRow _stream.Position = _stream.Length; -#if NETSTANDARD2_1_OR_GREATER +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER Span lenBuf = stackalloc Byte[4]; BinaryPrimitives.WriteInt32LittleEndian(lenBuf, recordLength); _stream.Write(lenBuf); diff --git a/NewLife.NovaDb/WAL/WalWriter.cs b/NewLife.NovaDb/WAL/WalWriter.cs index d7ccd51..8a081b8 100644 --- a/NewLife.NovaDb/WAL/WalWriter.cs +++ b/NewLife.NovaDb/WAL/WalWriter.cs @@ -90,7 +90,7 @@ public UInt64 Write(WalRecord record) pk.TryGetArray(out var segment); // 写入长度前缀(4 字节) -#if NETSTANDARD2_1_OR_GREATER +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER Span lengthPrefix = stackalloc Byte[4]; BinaryPrimitives.WriteInt32LittleEndian(lengthPrefix, pk.Length); _fileStream.Write(lengthPrefix); From fa27a72b4758c50823c2761cbc09c97078c1f2d5 Mon Sep 17 00:00:00 2001 From: Allen Cai Date: Thu, 5 Mar 2026 16:09:03 +0800 Subject: [PATCH 06/15] =?UTF-8?q?GeoPoint=E7=A9=BA=E9=97=B4=E8=A7=A3?= =?UTF-8?q?=E6=9E=90=E4=B8=8ESQL=E7=A9=BA=E9=97=B4=E5=87=BD=E6=95=B0?= =?UTF-8?q?=E6=80=A7=E8=83=BD=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 增强GeoPoint结构体,新增Parse/ParsePolygonWkt等高效解析方法,支持WKT多边形点数预估与零分配解析。优化WithinPolygon/WithinRadius等空间判断,提升大数据量场景下的性能。SQL引擎WITHIN_POLYGON实现改为池化数组,显著减少GC压力。完善异常处理与注释,提升健壮性和可维护性。 --- NewLife.NovaDb/Core/DataType.cs | 366 +++++++++++++++++++-- NewLife.NovaDb/Sql/SqlEngine.Expression.cs | 20 +- 2 files changed, 351 insertions(+), 35 deletions(-) diff --git a/NewLife.NovaDb/Core/DataType.cs b/NewLife.NovaDb/Core/DataType.cs index 5b6f415..d09ae98 100644 --- a/NewLife.NovaDb/Core/DataType.cs +++ b/NewLife.NovaDb/Core/DataType.cs @@ -1,4 +1,6 @@ -namespace NewLife.NovaDb.Core; +using System.Globalization; + +namespace NewLife.NovaDb.Core; /// NovaDb 支持的数据类型(严格映射 C# 类型) /// 基础类型的枚举值与 TypeCode 保持一致 @@ -94,6 +96,7 @@ public readonly struct GeoPoint(Double latitude, Double longitude) : IEquatable< { /// 地球平均半径(米) private const Double EarthRadiusMeters = 6_371_000.0; + private const double DegToRad = Math.PI / 180.0; /// 纬度(-90 到 90) public Double Latitude { get; } = latitude; @@ -106,35 +109,74 @@ public readonly struct GeoPoint(Double latitude, Double longitude) : IEquatable< /// 距离(米) public Double Distance(GeoPoint other) { - var lat1 = Latitude * Math.PI / 180.0; - var lat2 = other.Latitude * Math.PI / 180.0; - var dLat = (other.Latitude - Latitude) * Math.PI / 180.0; - var dLon = (other.Longitude - Longitude) * Math.PI / 180.0; + var lat1 = Latitude * DegToRad; + var lat2 = other.Latitude * DegToRad; + var dLat = (other.Latitude - Latitude) * DegToRad; + var dLon = (other.Longitude - Longitude) * DegToRad; + + var sinDLat = Math.Sin(dLat * 0.5); + var sinDLon = Math.Sin(dLon * 0.5); - var a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2) + - Math.Cos(lat1) * Math.Cos(lat2) * - Math.Sin(dLon / 2) * Math.Sin(dLon / 2); + var a = sinDLat * sinDLat + Math.Cos(lat1) * Math.Cos(lat2) * (sinDLon * sinDLon); var c = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a)); return EarthRadiusMeters * c; } + ///// 判断是否在指定中心点的半径范围内 + ///// 中心坐标点 + ///// 半径(米) + ///// 是否在范围内 + //public Boolean WithinRadius(ref readonly GeoPoint center, Double radiusMeters) => Distance(center) <= radiusMeters; + /// 判断是否在指定中心点的半径范围内 /// 中心坐标点 /// 半径(米) /// 是否在范围内 - public Boolean WithinRadius(GeoPoint center, Double radiusMeters) => Distance(center) <= radiusMeters; + /// 用于半径判断的更快版本:不必算出最终距离(少一次 Atan2 + 少一次乘法) + public Boolean WithinRadius(ref readonly GeoPoint center, Double radiusMeters) + { + // 由 haversine:distance = R * 2 * asin(sqrt(a)) + // 比较 distance <= radius 等价于:asin(sqrt(a)) <= radius/(2R) + // 再利用 asin 单调:sqrt(a) <= sin(radius/(2R)) => a <= sin^2(...) + + var max = radiusMeters / (2.0 * EarthRadiusMeters); + var sinMax = Math.Sin(max); + var aMax = sinMax * sinMax; + + var lat1 = Latitude * DegToRad; + var lat2 = center.Latitude * DegToRad; + var dLat = (center.Latitude - Latitude) * DegToRad; + var dLon = (center.Longitude - Longitude) * DegToRad; + + var sinDLat = Math.Sin(dLat * 0.5); + var sinDLon = Math.Sin(dLon * 0.5); + + var a = sinDLat * sinDLat + Math.Cos(lat1) * Math.Cos(lat2) * (sinDLon * sinDLon); + + return a <= aMax; + } /// 判断点是否在多边形内,使用射线法(Ray Casting) /// 多边形顶点数组,首尾自动闭合 /// 是否在多边形内 - public Boolean WithinPolygon(GeoPoint[] polygon) + public Boolean WithinPolygon(GeoPoint[] polygon) => polygon != null && polygon.Length >= 3 && WithinPolygon(polygon.AsSpan()); + + /// 判断点是否在多边形内,使用射线法(Ray Casting) + /// 多边形顶点数组,首尾自动闭合 + /// 是否在多边形内 + public Boolean WithinPolygon(ReadOnlySpan polygon) { - if (polygon == null || polygon.Length < 3) return false; + var n = polygon.Length; + if (n < 3) return false; + + // 将当前点坐标读到局部,避免循环内反复访问属性 + var y = Latitude; + var x = Longitude; var inside = false; - var n = polygon.Length; + // 射线法(Ray Casting):从测试点向右发射水平射线,统计与多边形边的交点数 for (Int32 i = 0, j = n - 1; i < n; j = i++) { var yi = polygon[i].Latitude; @@ -142,9 +184,11 @@ public Boolean WithinPolygon(GeoPoint[] polygon) var yj = polygon[j].Latitude; var xj = polygon[j].Longitude; - // 射线法:从测试点向右发射水平射线,统计与多边形边的交点数 - if ((yi > Latitude) != (yj > Latitude) && - Longitude < (xj - xi) * (Latitude - yi) / (yj - yi) + xi) + var intersect = yi > y != yj > y; + if (!intersect) continue; + + var xIntersect = (xj - xi) * (y - yi) / (yj - yi) + xi; + if (x < xIntersect) { inside = !inside; } @@ -153,6 +197,30 @@ public Boolean WithinPolygon(GeoPoint[] polygon) return inside; } + /// 从字符串解析坐标点,格式为 "(lat, lon)" + /// 字符串 + /// 坐标点 + public static GeoPoint Parse(String s) + { + if (s == null) throw new ArgumentNullException(nameof(s)); + +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER + return Parse(s.AsSpan()); +#else + var trimmed = s.Trim(); + if (trimmed.StartsWith("(") && trimmed.EndsWith(")")) + trimmed = trimmed.Substring(1, trimmed.Length - 2); + + var parts = trimmed.Split(','); + if (parts.Length != 2) + throw new FormatException($"Invalid GeoPoint format: '{s}', expected '(lat, lon)'"); + + var lat = Double.Parse(parts[0].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture); + var lon = Double.Parse(parts[1].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture); + return new GeoPoint(lat, lon); +#endif + } + /// 从 WKT 格式的多边形字符串解析顶点数组 /// WKT 格式字符串,如 "POLYGON((lon1 lat1, lon2 lat2, ...))" /// 顶点数组 @@ -160,14 +228,17 @@ public static GeoPoint[] ParsePolygonWkt(String wkt) { if (wkt == null) throw new ArgumentNullException(nameof(wkt)); +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER + return ParsePolygonWkt(wkt.AsSpan()); +#else var trimmed = wkt.Trim(); // 支持 POLYGON((lon lat, lon lat, ...)) 格式 if (trimmed.StartsWith("POLYGON", StringComparison.OrdinalIgnoreCase)) { - var start = trimmed.IndexOf("((", StringComparison.Ordinal); - var end = trimmed.LastIndexOf("))", StringComparison.Ordinal); - if (start < 0 || end < 0) + var start = IndexOfDoubleParenOpen(trimmed.AsSpan()); + var end = LastIndexOfDoubleParenClose(trimmed.AsSpan()); + if (start < 0 || end < 0 || end <= start + 1) throw new FormatException($"Invalid POLYGON WKT format: '{wkt}'"); trimmed = trimmed.Substring(start + 2, end - start - 2); @@ -176,37 +247,264 @@ public static GeoPoint[] ParsePolygonWkt(String wkt) var pointStrings = trimmed.Split(','); var points = new GeoPoint[pointStrings.Length]; - for (var i = 0; i < pointStrings.Length; i++) + var idx = 0; + foreach (var pointString in pointStrings) { - var parts = pointStrings[i].Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries); + var parts = pointString.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (parts.Length == 0) continue; if (parts.Length < 2) - throw new FormatException($"Invalid coordinate in polygon: '{pointStrings[i]}'"); + throw new FormatException($"Invalid coordinate in polygon: '{pointString}'"); // WKT 标准格式为 "经度 纬度" var lon = Double.Parse(parts[0].Trim()); var lat = Double.Parse(parts[1].Trim()); - points[i] = new GeoPoint(lat, lon); + points[idx] = new GeoPoint(lat, lon); + idx++; } + if (idx != points.Length) + Array.Resize(ref points, idx); + return points; +#endif + } + + /// + /// 获取 WKT 格式的多边形字符串中坐标点的数量(逗号分隔的坐标对数量),不解析坐标值 + /// + /// WKT 格式字符串,如 "POLYGON((lon1 lat1, lon2 lat2, ...))" + /// 返回逗号分隔的坐标对数量,忽略空项;如果格式不正确(如缺少 POLYGON((...)) 包装),也会尽量统计逗号分隔的项数,而不是抛异常。 + public static Int32 GetPolygonWktCount(String wkt) + { + if (wkt == null) return 0; + return GetPolygonWktCount(wkt.AsSpan()); + } + + /// + /// 获取 WKT 格式的多边形字符串中坐标点的数量(逗号分隔的坐标对数量),不解析坐标值 + /// + /// WKT 格式字符串,如 "POLYGON((lon1 lat1, lon2 lat2, ...))" + /// 返回逗号分隔的坐标对数量,忽略空项;如果格式不正确(如缺少 POLYGON((...)) 包装),也会尽量统计逗号分隔的项数,而不是抛异常。 + public static Int32 GetPolygonWktCount(ReadOnlySpan wkt) + { + if (wkt == null) return 0; + + wkt = wkt.Trim(); + if (wkt.IsEmpty) return 0; + + //if (wkt.StartsWith("POLYGON", StringComparison.OrdinalIgnoreCase)) + //{ + // var start = IndexOfDoubleParenOpen(wkt); + // var end = LastIndexOfDoubleParenClose(wkt); + // if (start < 0 || end < 0 || end <= start + 1) + // throw new FormatException($"Invalid POLYGON WKT format: '{wkt.ToString()}'"); + // wkt = wkt.Slice(start + 2, end - start - 2).Trim(); + //} + + var count = 0; + while (!wkt.IsEmpty) + { + var comma = wkt.IndexOf(','); + var item = comma >= 0 ? wkt.Slice(0, comma) : wkt; + wkt = comma >= 0 ? wkt.Slice(comma + 1) : ReadOnlySpan.Empty; + item = item.Trim(); + if (item.IsEmpty) continue; + count++; + } + return count; } + /// + /// 从 WKT 格式的多边形字符串解析坐标点到预分配的 中,返回实际解析的点数量 + /// + /// 预分配的坐标点数组 + /// WKT 格式字符串,如 "POLYGON((lon1 lat1, lon2 lat2, ...))" + /// 实际解析的点数量 + /// + /// + public static Int32 GetPolygonWkts(GeoPoint[] points, String wkt) + { + if (points == null) throw new ArgumentNullException(nameof(points)); + if (wkt == null) throw new ArgumentNullException(nameof(wkt)); + +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER + return GetPolygonWkts(points.AsSpan(), wkt.AsSpan()); +#else + var trimmed = wkt.Trim(); + + // 支持 POLYGON((lon lat, lon lat, ...)) 格式 + if (trimmed.StartsWith("POLYGON", StringComparison.OrdinalIgnoreCase)) + { + var start = IndexOfDoubleParenOpen(trimmed.AsSpan()); + var end = LastIndexOfDoubleParenClose(trimmed.AsSpan()); + if (start < 0 || end < 0 || end <= start + 1) + throw new FormatException($"Invalid POLYGON WKT format: '{wkt}'"); + + trimmed = trimmed.Substring(start + 2, end - start - 2); + } + + var count = 0; + var pointStrings = trimmed.Split(','); + foreach (var pointString in pointStrings) + { + var parts = pointString.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (parts.Length == 0) continue; + if (parts.Length < 2) + throw new FormatException($"Invalid coordinate in polygon: '{pointString}'"); + + // WKT 标准格式为 "经度 纬度" + var lon = Double.Parse(parts[0].Trim()); + var lat = Double.Parse(parts[1].Trim()); + points[count] = new GeoPoint(lat, lon); + count++; + } + return count; +#endif + } + +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER /// 从字符串解析坐标点,格式为 "(lat, lon)" - /// 字符串 + /// 字符串 /// 坐标点 - public static GeoPoint Parse(String s) + public static GeoPoint Parse(ReadOnlySpan span) { - if (s == null) throw new ArgumentNullException(nameof(s)); + span = span.Trim(); + if (span.Length >= 2 && span[0] == '(' && span[^1] == ')') + span = span.Slice(1, span.Length - 2).Trim(); - var trimmed = s.Trim(); - if (trimmed.StartsWith("(") && trimmed.EndsWith(")")) - trimmed = trimmed.Substring(1, trimmed.Length - 2); + var comma = span.IndexOf(','); + if (comma < 0) + throw new FormatException($"Invalid GeoPoint format: '{span.ToString()}', expected '(lat, lon)'"); - var parts = trimmed.Split(','); - if (parts.Length != 2) - throw new FormatException($"Invalid GeoPoint format: '{s}', expected '(lat, lon)'"); + var latSpan = span.Slice(0, comma).Trim(); + var lonSpan = span.Slice(comma + 1).Trim(); + + var lat = Double.Parse(latSpan, NumberStyles.Float, CultureInfo.InvariantCulture); + var lon = Double.Parse(lonSpan, NumberStyles.Float, CultureInfo.InvariantCulture); + return new GeoPoint(lat, lon); + } + + /// 从 WKT 格式的多边形字符串解析顶点数组 + /// WKT 格式字符串,如 "POLYGON((lon1 lat1, lon2 lat2, ...))" + /// 顶点数组 + public static GeoPoint[] ParsePolygonWkt(ReadOnlySpan wkt) + { + var s = wkt.Trim(); + + // 支持 "POLYGON((lon lat, lon lat, ...))" + if (s.StartsWith("POLYGON", StringComparison.OrdinalIgnoreCase)) + { + var start = IndexOfDoubleParenOpen(s); + var end = LastIndexOfDoubleParenClose(s); + if (start < 0 || end < 0 || end <= start + 1) + throw new FormatException($"Invalid POLYGON WKT format: '{wkt.ToString()}'"); + + s = s.Slice(start + 2, end - (start + 2)).Trim(); + } + + // 先数逗号,预分配 points + var count = 1; + foreach (var t in s) + if (t == ',') count++; + + var points = new GeoPoint[count]; + + var idx = 0; + while (!s.IsEmpty) + { + var comma = s.IndexOf(','); + var item = comma >= 0 ? s.Slice(0, comma) : s; + s = comma >= 0 ? s.Slice(comma + 1) : ReadOnlySpan.Empty; + + item = item.Trim(); + if (item.IsEmpty) continue; + + // "lon lat"(空格分隔,可能有多空格) + var sp = item.IndexOf(' '); + if (sp < 0) throw new FormatException($"Invalid coordinate in polygon: '{item.ToString()}'"); + + // 找到第一个非空格分隔点 + var lonSpan = item.Slice(0, sp).Trim(); + var rest = item.Slice(sp + 1).TrimStart(); + var sp2 = rest.IndexOf(' '); + var latSpan = (sp2 >= 0 ? rest.Slice(0, sp2) : rest).Trim(); + + var lon = Double.Parse(lonSpan, NumberStyles.Float, CultureInfo.InvariantCulture); + var lat = Double.Parse(latSpan, NumberStyles.Float, CultureInfo.InvariantCulture); + points[idx++] = new GeoPoint(lat, lon); + } + + if (idx != points.Length) + Array.Resize(ref points, idx); + + return points; + } - return new GeoPoint(Double.Parse(parts[0].Trim()), Double.Parse(parts[1].Trim())); + /// + /// 从 WKT 格式的多边形字符串解析坐标点到预分配的 中,返回实际解析的点数量 + /// + /// 预分配的坐标点数组 + /// WKT 格式的多边形字符串 + /// 实际解析的点数量 + /// + public static Int32 GetPolygonWkts(Span points, ReadOnlySpan wkt) + { + var s = wkt.Trim(); + + // 支持 "POLYGON((lon lat, lon lat, ...))" + if (s.StartsWith("POLYGON", StringComparison.OrdinalIgnoreCase)) + { + var start = IndexOfDoubleParenOpen(s); + var end = LastIndexOfDoubleParenClose(s); + if (start < 0 || end < 0 || end <= start + 1) + throw new FormatException($"Invalid POLYGON WKT format: '{wkt.ToString()}'"); + + s = s.Slice(start + 2, end - (start + 2)).Trim(); + } + + var count = 0; + while (!s.IsEmpty) + { + var comma = s.IndexOf(','); + var item = comma >= 0 ? s.Slice(0, comma) : s; + s = comma >= 0 ? s.Slice(comma + 1) : ReadOnlySpan.Empty; + item = item.Trim(); + if (item.IsEmpty) continue; + var sp = item.IndexOf(' '); + if (sp < 0) throw new FormatException($"Invalid coordinate in polygon: '{item.ToString()}'"); + var lonSpan = item.Slice(0, sp).Trim(); + var rest = item.Slice(sp + 1).TrimStart(); + var sp2 = rest.IndexOf(' '); + var latSpan = (sp2 >= 0 ? rest.Slice(0, sp2) : rest).Trim(); + var lon = Double.Parse(lonSpan, NumberStyles.Float, CultureInfo.InvariantCulture); + var lat = Double.Parse(latSpan, NumberStyles.Float, CultureInfo.InvariantCulture); + points[count++] = new GeoPoint(lat, lon); + } + return count; + } +#endif + + /// + /// 从前往后找到第一个 "((" 的位置,返回索引;找不到返回 -1 + /// + private static Int32 IndexOfDoubleParenOpen(ReadOnlySpan s) + { + for (var i = 0; i + 1 < s.Length; i++) + if (s[i] == '(' && s[i + 1] == '(') + return i; + return -1; + } + + /// + /// 从后往前找到最后一个 "))" 的位置,返回索引;找不到返回 -1 + /// + private static Int32 LastIndexOfDoubleParenClose(ReadOnlySpan s) + { + // 找最后一个 "))" + for (var i = s.Length - 2; i >= 0; i--) + if (s[i] == ')' && s[i + 1] == ')') + return i; + return -1; } /// 判断是否相等 @@ -223,10 +521,14 @@ public static GeoPoint Parse(String s) /// 哈希码 public override Int32 GetHashCode() { +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER + return HashCode.Combine(Latitude, Longitude); +#else unchecked { return (Latitude.GetHashCode() * 397) ^ Longitude.GetHashCode(); } +#endif } /// 返回字符串表示 diff --git a/NewLife.NovaDb/Sql/SqlEngine.Expression.cs b/NewLife.NovaDb/Sql/SqlEngine.Expression.cs index 01ac9db..979c8ea 100644 --- a/NewLife.NovaDb/Sql/SqlEngine.Expression.cs +++ b/NewLife.NovaDb/Sql/SqlEngine.Expression.cs @@ -1,4 +1,5 @@ -using NewLife.NovaDb.Core; +using System.Buffers; +using NewLife.NovaDb.Core; using NewLife.NovaDb.Engine; using NewLife.NovaDb.Utilities; @@ -638,8 +639,21 @@ private Boolean EvaluateGroupCondition(SqlExpression expr, List group case "WITHIN_POLYGON": if (args.Count < 2 || args[0] == null || args[1] == null) return null; - var polygonPoints = GeoPoint.ParsePolygonWkt(Convert.ToString(args[1])!); - return ((GeoPoint)args[0]!).WithinPolygon(polygonPoints); + var wkt = Convert.ToString(args[1])!; + var count = GeoPoint.GetPolygonWktCount(wkt); + if (count == 0) return false; + var polygonPoints = ArrayPool.Shared.Rent(count); + try + { + count = GeoPoint.GetPolygonWkts(polygonPoints, wkt); + if (count == 0) return false; + + return ((GeoPoint)args[0]!).WithinPolygon(new ReadOnlySpan(polygonPoints, 0, count)); + } + finally + { + ArrayPool.Shared.Return(polygonPoints); + } // Vector 函数 case "VECTOR": From ec17691bcdea8f13ab49f7e4bba5d24e366a70be Mon Sep 17 00:00:00 2001 From: Allen Cai Date: Thu, 5 Mar 2026 17:49:48 +0800 Subject: [PATCH 07/15] =?UTF-8?q?=E5=BC=95=E5=85=A5HashHelper=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E5=93=88=E5=B8=8C=E7=AE=97=E6=B3=95=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E5=B9=B6=E4=BC=98=E5=8C=96=E8=B0=83=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增HashHelper类,统一MD5/SHA1/SHA2等哈希算法的计算与输出,支持十六进制和Base64格式。AuthManager和SqlEngine中相关哈希逻辑改为调用HashHelper,提升了性能、可读性和代码复用性。为不同.NET版本做了兼容处理。 --- NewLife.NovaDb/Server/AuthManager.cs | 12 +-- NewLife.NovaDb/Sql/SqlEngine.Expression.cs | 29 ++--- NewLife.NovaDb/Utilities/HashHelper.cs | 120 +++++++++++++++++++++ 3 files changed, 131 insertions(+), 30 deletions(-) create mode 100644 NewLife.NovaDb/Utilities/HashHelper.cs diff --git a/NewLife.NovaDb/Server/AuthManager.cs b/NewLife.NovaDb/Server/AuthManager.cs index a2ff235..0118934 100644 --- a/NewLife.NovaDb/Server/AuthManager.cs +++ b/NewLife.NovaDb/Server/AuthManager.cs @@ -1,4 +1,5 @@ -using NewLife.NovaDb.Core; +using System.Runtime.CompilerServices; +using NewLife.NovaDb.Core; using NewLife.NovaDb.Utilities; namespace NewLife.NovaDb.Server; @@ -232,13 +233,8 @@ public List GetAllUsers() } /// 密码哈希(SHA256) - private static String HashPassword(String password) - { - using var sha256 = System.Security.Cryptography.SHA256.Create(); - using var bytes = System.Text.Encoding.UTF8.GetPooledEncodedBytes(password); - var hash = sha256.ComputeHash(bytes.Buffer, 0, bytes.Length); - return Convert.ToBase64String(hash); - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static String HashPassword(String password) => HashHelper.Sha256ToBase64(password); } /// 用户信息 diff --git a/NewLife.NovaDb/Sql/SqlEngine.Expression.cs b/NewLife.NovaDb/Sql/SqlEngine.Expression.cs index 979c8ea..3e3e9ce 100644 --- a/NewLife.NovaDb/Sql/SqlEngine.Expression.cs +++ b/NewLife.NovaDb/Sql/SqlEngine.Expression.cs @@ -589,36 +589,21 @@ private Boolean EvaluateGroupCondition(SqlExpression expr, List group // 哈希函数 case "MD5": if (args.Count < 1 || args[0] == null) return null; - using (var md5 = System.Security.Cryptography.MD5.Create()) - { - using var bytes = Convert.ToString(args[0])!.ToPooledUtf8Bytes(); - var hash = md5.ComputeHash(bytes.Buffer, 0, bytes.Length); - return BitConverter.ToString(hash).Replace("-", String.Empty).ToLower(); - } + return HashHelper.Md5ToHex(Convert.ToString(args[0])!); case "SHA1": if (args.Count < 1 || args[0] == null) return null; - using (var sha1 = System.Security.Cryptography.SHA1.Create()) - { - using var bytes = Convert.ToString(args[0])!.ToPooledUtf8Bytes(); - var hash = sha1.ComputeHash(bytes.Buffer, 0, bytes.Length); - return BitConverter.ToString(hash).Replace("-", String.Empty).ToLower(); - } + return HashHelper.Sha1ToHex(Convert.ToString(args[0])!); case "SHA2": if (args.Count < 1 || args[0] == null) return null; var sha2Bits = args.Count >= 2 && args[1] != null ? Convert.ToInt32(args[1]) : 256; - using (var sha2 = sha2Bits switch + return sha2Bits switch { - 384 => (System.Security.Cryptography.HashAlgorithm)System.Security.Cryptography.SHA384.Create(), - 512 => System.Security.Cryptography.SHA512.Create(), - _ => System.Security.Cryptography.SHA256.Create() - }) - { - using var bytes = Convert.ToString(args[0])!.ToPooledUtf8Bytes(); - var hash = sha2.ComputeHash(bytes.Buffer, 0, bytes.Length); - return BitConverter.ToString(hash).Replace("-", String.Empty).ToLower(); - } + 384 => HashHelper.Sha384ToHex(Convert.ToString(args[0])), + 512 => HashHelper.Sha512ToHex(Convert.ToString(args[0])), + _ => HashHelper.Sha256ToHex(Convert.ToString(args[0])) + }; // GeoPoint 函数 case "GEOPOINT": diff --git a/NewLife.NovaDb/Utilities/HashHelper.cs b/NewLife.NovaDb/Utilities/HashHelper.cs new file mode 100644 index 0000000..c7fa897 --- /dev/null +++ b/NewLife.NovaDb/Utilities/HashHelper.cs @@ -0,0 +1,120 @@ +using System.Runtime.CompilerServices; +using System.Security.Cryptography; + +namespace NewLife.NovaDb.Utilities +{ + /// + /// 哈希计算辅助类,提供 MD5、SHA1、SHA256、SHA384、SHA512 等常用哈希算法的计算方法 + /// + internal static class HashHelper + { + private const String Hex = "0123456789abcdef"; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static String Md5ToHex(String str) => ComputeHash(str, HashAlgorithmName.MD5, 16); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static String Sha1ToHex(String str) => ComputeHash(str, HashAlgorithmName.SHA1, 20); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static String Sha256ToHex(String str) => ComputeHash(str, HashAlgorithmName.SHA256, 32); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static String Sha384ToHex(String str) => ComputeHash(str, HashAlgorithmName.SHA384, 48); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static String Sha512ToHex(String str) => ComputeHash(str, HashAlgorithmName.SHA512, 64); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static String Md5ToBase64(String str) => ComputeHash(str, HashAlgorithmName.MD5, 16, false); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static String Sha1ToBase64(String str) => ComputeHash(str, HashAlgorithmName.SHA1, 20, false); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static String Sha256ToBase64(String str) => ComputeHash(str, HashAlgorithmName.SHA256, 32, false); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static String Sha384ToBase64(String str) => ComputeHash(str, HashAlgorithmName.SHA384, 48, false); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static String Sha512ToBase64(String str) => ComputeHash(str, HashAlgorithmName.SHA512, 64, false); + + private static String ComputeHash(String str, HashAlgorithmName alg, Int32 hashSize, Boolean hex = true) + { + using var bytes = str.ToPooledUtf8Bytes(); +#if NET5_0_OR_GREATER + Span hash = stackalloc Byte[hashSize]; + + if (!TryHashData(alg, bytes.AsSpan(), hash)) + throw new CryptographicException(); + + return hex ? ToLowerHex(hash) : Convert.ToBase64String(hash); +#elif NETSTANDARD2_1_OR_GREATER + using var algo = CreateAlgorithm(alg); + + Span hash = stackalloc Byte[hashSize]; + + if (!algo.TryComputeHash(bytes.AsSpan(), hash, out _)) + throw new CryptographicException(); + + return hex ? ToLowerHex(hash) : Convert.ToBase64String(hash); +#else + using var algo = CreateAlgorithm(alg); + var hash = algo.ComputeHash(bytes.Buffer, 0, bytes.Length); + return hex ? ToLowerHex(hash) : Convert.ToBase64String(hash); +#endif + } + +#if NET5_0_OR_GREATER + private static bool TryHashData(HashAlgorithmName alg, ReadOnlySpan data, Span dest) + { + if (alg == HashAlgorithmName.MD5) + return MD5.TryHashData(data, dest, out _); + + if (alg == HashAlgorithmName.SHA1) + return SHA1.TryHashData(data, dest, out _); + + if (alg == HashAlgorithmName.SHA256) + return SHA256.TryHashData(data, dest, out _); + + if (alg == HashAlgorithmName.SHA384) + return SHA384.TryHashData(data, dest, out _); + + if (alg == HashAlgorithmName.SHA512) + return SHA512.TryHashData(data, dest, out _); + + throw new NotSupportedException(); + } +#endif + + private static HashAlgorithm CreateAlgorithm(HashAlgorithmName alg) + { + if (alg == HashAlgorithmName.MD5) return MD5.Create(); + if (alg == HashAlgorithmName.SHA1) return SHA1.Create(); + if (alg == HashAlgorithmName.SHA256) return SHA256.Create(); + if (alg == HashAlgorithmName.SHA384) return SHA384.Create(); + if (alg == HashAlgorithmName.SHA512) return SHA512.Create(); + + throw new NotSupportedException(); + } + + private static String ToLowerHex(ReadOnlySpan bytes) + { +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER + Span chars = stackalloc Char[bytes.Length * 2]; +#else + var chars = new Char[bytes.Length * 2]; +#endif + var j = 0; + + foreach (var b in bytes) + { + chars[j++] = Hex[b >> 4]; + chars[j++] = Hex[b & 0xF]; + } + + return new String(chars); + } + } +} From bacb5daec064f7c7c6ad59dc650346d70f6eba8b Mon Sep 17 00:00:00 2001 From: Allen Cai Date: Thu, 5 Mar 2026 18:48:26 +0800 Subject: [PATCH 08/15] =?UTF-8?q?HashHelper=E5=86=85=E9=83=A8=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E7=A7=81=E6=9C=89=E5=93=88=E5=B8=8C=E7=AE=97=E6=B3=95?= =?UTF-8?q?=E6=9E=9A=E4=B8=BE=EF=BC=8C=E8=A7=A3=E5=86=B3=E9=9D=A2=E5=90=91?= =?UTF-8?q?.NET=204.5=E5=B9=B3=E5=8F=B0HashAlgorithmName=E7=BB=93=E6=9E=84?= =?UTF-8?q?=E4=BD=93=E4=B8=8D=E5=8F=AF=E7=94=A8=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- NewLife.NovaDb/Core/DataType.cs | 2 -- NewLife.NovaDb/Sql/SqlEngine.Expression.cs | 9 +++++---- NewLife.NovaDb/Utilities/HashHelper.cs | 11 +++++++++++ 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/NewLife.NovaDb/Core/DataType.cs b/NewLife.NovaDb/Core/DataType.cs index d09ae98..88d14d6 100644 --- a/NewLife.NovaDb/Core/DataType.cs +++ b/NewLife.NovaDb/Core/DataType.cs @@ -287,8 +287,6 @@ public static Int32 GetPolygonWktCount(String wkt) /// 返回逗号分隔的坐标对数量,忽略空项;如果格式不正确(如缺少 POLYGON((...)) 包装),也会尽量统计逗号分隔的项数,而不是抛异常。 public static Int32 GetPolygonWktCount(ReadOnlySpan wkt) { - if (wkt == null) return 0; - wkt = wkt.Trim(); if (wkt.IsEmpty) return 0; diff --git a/NewLife.NovaDb/Sql/SqlEngine.Expression.cs b/NewLife.NovaDb/Sql/SqlEngine.Expression.cs index 3e3e9ce..b0a83e5 100644 --- a/NewLife.NovaDb/Sql/SqlEngine.Expression.cs +++ b/NewLife.NovaDb/Sql/SqlEngine.Expression.cs @@ -600,9 +600,9 @@ private Boolean EvaluateGroupCondition(SqlExpression expr, List group var sha2Bits = args.Count >= 2 && args[1] != null ? Convert.ToInt32(args[1]) : 256; return sha2Bits switch { - 384 => HashHelper.Sha384ToHex(Convert.ToString(args[0])), - 512 => HashHelper.Sha512ToHex(Convert.ToString(args[0])), - _ => HashHelper.Sha256ToHex(Convert.ToString(args[0])) + 384 => HashHelper.Sha384ToHex(Convert.ToString(args[0])!), + 512 => HashHelper.Sha512ToHex(Convert.ToString(args[0])!), + _ => HashHelper.Sha256ToHex(Convert.ToString(args[0])!) }; // GeoPoint 函数 @@ -620,7 +620,8 @@ private Boolean EvaluateGroupCondition(SqlExpression expr, List group case "WITHIN_RADIUS": if (args.Count < 3 || args[0] == null || args[1] == null || args[2] == null) return null; - return ((GeoPoint)args[0]!).WithinRadius((GeoPoint)args[1]!, Convert.ToDouble(args[2])); + var center = (GeoPoint)args[1]!; + return ((GeoPoint)args[0]!).WithinRadius(in center, Convert.ToDouble(args[2])); case "WITHIN_POLYGON": if (args.Count < 2 || args[0] == null || args[1] == null) return null; diff --git a/NewLife.NovaDb/Utilities/HashHelper.cs b/NewLife.NovaDb/Utilities/HashHelper.cs index c7fa897..f751951 100644 --- a/NewLife.NovaDb/Utilities/HashHelper.cs +++ b/NewLife.NovaDb/Utilities/HashHelper.cs @@ -116,5 +116,16 @@ private static String ToLowerHex(ReadOnlySpan bytes) return new String(chars); } + + private enum HashAlgorithmName + { + // ReSharper disable InconsistentNaming + MD5, + SHA1, + SHA256, + SHA384, + SHA512 + // ReSharper restore InconsistentNaming + } } } From 8a82cdcbe02362e1c42f48628290df4323a0c531 Mon Sep 17 00:00:00 2001 From: Allen Cai Date: Thu, 5 Mar 2026 23:24:48 +0800 Subject: [PATCH 09/15] =?UTF-8?q?=E6=8F=90=E5=8D=87=E5=BA=8F=E5=88=97?= =?UTF-8?q?=E5=8C=96=E4=B8=8E=E6=8C=81=E4=B9=85=E5=8C=96=E6=80=A7=E8=83=BD?= =?UTF-8?q?=EF=BC=8C=E5=87=8F=E5=B0=91=E5=86=85=E5=AD=98=E5=88=86=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 引入 PooledBufferWriter,优化日志、表、KV 等持久化写入,减少内存分配和 GC 压力。IDataCodec 支持直接编码到缓冲区,DefaultDataCodec 针对多类型实现高效写入。KV 存储加载与回放采用 ArrayPool,提升大文件处理效率。相关辅助方法和 CRC32 校验均做性能优化。 --- NewLife.NovaDb/Core/IDataCodec.cs | 234 +++++++++++++++--- .../Engine/Flux/FluxEngine.Persist.cs | 165 +++++++----- NewLife.NovaDb/Engine/KV/KvEntry.cs | 5 +- NewLife.NovaDb/Engine/KV/KvStore.Persist.cs | 69 ++++-- NewLife.NovaDb/Engine/NovaTable.Persist.cs | 38 +-- NewLife.NovaDb/Engine/NovaTable.cs | 23 +- .../Utilities/PooledBufferWriter.cs | 150 +++++++++++ NewLife.NovaDb/WAL/BinlogWriter.cs | 71 +++--- 8 files changed, 565 insertions(+), 190 deletions(-) create mode 100644 NewLife.NovaDb/Utilities/PooledBufferWriter.cs diff --git a/NewLife.NovaDb/Core/IDataCodec.cs b/NewLife.NovaDb/Core/IDataCodec.cs index f923f67..fc3afd1 100644 --- a/NewLife.NovaDb/Core/IDataCodec.cs +++ b/NewLife.NovaDb/Core/IDataCodec.cs @@ -1,4 +1,5 @@ using System.Buffers.Binary; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; @@ -13,6 +14,14 @@ public interface IDataCodec /// 编码后的字节数组 Byte[] Encode(Object? value, DataType dataType); + /// 编码值到二进制 + /// 要编码的值 + /// 数据类型 + /// 目标缓冲区 + /// 起始偏移 + /// 编码后的字节长度 + Int32 Encode(Object? value, DataType dataType, Byte[] buffer, Int32 offset); + /// 从二进制解码值 /// 字节数组 /// 起始偏移 @@ -74,6 +83,85 @@ public Byte[] Encode(Object? value, DataType dataType) } } + /// 编码值到二进制 + /// 要编码的值 + /// 数据类型 + /// 目标缓冲区 + /// 起始偏移 + /// 编码后的字节长度 + public Int32 Encode(Object? value, DataType dataType, Byte[] buffer, Int32 offset) + { + if (value == null) + { + if (buffer.Length < offset + 1) + throw new ArgumentException($"Buffer too short to encode NULL (need {offset + 1} bytes, got {buffer.Length})"); + buffer[offset] = NullFlag; + return 1; + } + + try + { + switch (dataType) + { + case DataType.Boolean: + { + if (buffer.Length < offset + 1) + throw new ArgumentException($"Buffer too short to encode Boolean (need {offset + 1} bytes, got {buffer.Length})"); + buffer[offset] = (Boolean)value ? ((Byte)1) : ((Byte)0); + return 1; + } + case DataType.Int32: + { + if (buffer.Length < offset + 4) + throw new ArgumentException($"Buffer too short to encode Int32 (need {offset + 4} bytes, got {buffer.Length})"); + Unsafe.WriteUnaligned(ref MemoryMarshal.GetReference(buffer.AsSpan(offset)), (Int32)value); + return 4; + } + case DataType.Int64: + { + if (buffer.Length < offset + 8) + throw new ArgumentException($"Buffer too short to encode Int64 (need {offset + 8} bytes, got {buffer.Length})"); + Unsafe.WriteUnaligned(ref MemoryMarshal.GetReference(buffer.AsSpan(offset)), (Int64)value); + return 8; + } + case DataType.Double: + { + if (buffer.Length < offset + 8) + throw new ArgumentException($"Buffer too short to encode Double (need {offset + 8} bytes, got {buffer.Length})"); + Unsafe.WriteUnaligned(ref MemoryMarshal.GetReference(buffer.AsSpan(offset)), (Double)value); + return 8; + } + case DataType.Decimal: + return EncodeDecimal((Decimal)value, buffer, offset); + case DataType.String: + return EncodeString((String)value, buffer, offset); + case DataType.Binary: + return EncodeByteArray((Byte[])value, buffer, offset); + case DataType.DateTime: + { + if (buffer.Length < offset + 8) + throw new ArgumentException($"Buffer too short to encode Int64 (need {offset + 8} bytes, got {buffer.Length})"); + Unsafe.WriteUnaligned(ref MemoryMarshal.GetReference(buffer.AsSpan(offset)), ((DateTime)value).Ticks); + return 8; + } + case DataType.GeoPoint: + return EncodeGeoPoint((GeoPoint)value, buffer, offset); + case DataType.Vector: + return EncodeVector((Single[])value, buffer, offset); + default: + throw new NotSupportedException($"Unsupported data type: {dataType}"); + } + } + catch (InvalidCastException ex) + { + throw new NovaException( + ErrorCode.InvalidArgument, + $"Cannot encode value of type {value.GetType().Name} as {dataType}", + ex + ); + } + } + /// 从二进制解码值 /// 字节数组 /// 起始偏移 @@ -152,12 +240,119 @@ public Int32 GetEncodedLength(Object? value, DataType dataType) private static Byte[] EncodeDecimal(Decimal value) { - var bits = Decimal.GetBits(value); var buffer = new Byte[16]; + +#if NET5_0_OR_GREATER + Span bits = stackalloc Int32[4]; + Decimal.GetBits(value, bits); + + // 把 4 个 int 的原始 16 字节拷贝到 byte[16] + MemoryMarshal.AsBytes(bits).CopyTo(buffer); +#else + var bits = Decimal.GetBits(value); Buffer.BlockCopy(bits, 0, buffer, 0, 16); +#endif + + return buffer; + } + + private static Int32 EncodeDecimal(Decimal value, Byte[] buffer, Int32 offset) + { + if (buffer.Length < offset + 16) + throw new ArgumentException($"Buffer too short to encode Decimal (need {offset + 16} bytes, got {buffer.Length})"); + +#if NET5_0_OR_GREATER + Span bits = stackalloc Int32[4]; + Decimal.GetBits(value, bits); + + // 16 bytes 写入目标 buffer + MemoryMarshal.AsBytes(bits).CopyTo(buffer.AsSpan(offset, 16)); +#else + var bits = Decimal.GetBits(value); + Buffer.BlockCopy(bits, 0, buffer, offset, 16); +#endif + + return 16; + } + + private static Byte[] EncodeString(String value) + { + var valueBytesLength = _encoding.GetByteCount(value); + var buffer = new Byte[4 + valueBytesLength]; + BinaryPrimitives.WriteInt32LittleEndian(buffer.AsSpan(0, 4), valueBytesLength); + _encoding.GetBytes(value, buffer.AsSpan(4)); + return buffer; + } + + private static Int32 EncodeString(String value, Byte[] buffer, Int32 offset) + { + var valueBytesLength = _encoding.GetByteCount(value); + if (buffer.Length < offset + 4 + valueBytesLength) + throw new ArgumentException($"Buffer too short to encode String (need {offset + 4 + valueBytesLength} bytes, got {buffer.Length})"); + + BinaryPrimitives.WriteInt32LittleEndian(buffer.AsSpan(offset, 4), valueBytesLength); + _encoding.GetBytes(value, buffer.AsSpan(offset + 4)); + return 4 + valueBytesLength; + } + + private static Byte[] EncodeByteArray(Byte[] value) + { + var buffer = new Byte[4 + value.Length]; + BinaryPrimitives.WriteInt32LittleEndian(buffer.AsSpan(0, 4), value.Length); + value.AsSpan().CopyTo(buffer.AsSpan(4)); return buffer; } + private static Int32 EncodeByteArray(Byte[] value, Byte[] buffer, Int32 offset) + { + if (buffer.Length < offset + 4 + value.Length) + throw new ArgumentException($"Buffer too short to encode ByteArray (need {offset + 4 + value.Length} bytes, got {buffer.Length})"); + + BinaryPrimitives.WriteInt32LittleEndian(buffer.AsSpan(offset, 4), value.Length); + value.AsSpan().CopyTo(buffer.AsSpan(offset + 4)); + return 4 + value.Length; + } + + private static Byte[] EncodeGeoPoint(GeoPoint value) + { + var buffer = new Byte[16]; + WriteDoubleLittleEndian(buffer.AsSpan(0, 8), value.Latitude); + WriteDoubleLittleEndian(buffer.AsSpan(8, 8), value.Longitude); + return buffer; + } + + private static Int32 EncodeGeoPoint(GeoPoint value, Byte[] buffer, Int32 offset) + { + if (buffer.Length < offset + 16) + throw new ArgumentException($"Buffer too short to encode GeoPoint (need {offset + 16} bytes, got {buffer.Length})"); + + WriteDoubleLittleEndian(buffer.AsSpan(offset, 8), value.Latitude); + WriteDoubleLittleEndian(buffer.AsSpan(offset + 8, 8), value.Longitude); + return 16; + } + + private static Byte[] EncodeVector(Single[] value) + { + var byteLen = checked(value.Length * sizeof(Single)); + var buffer = new Byte[sizeof(Int32) + byteLen]; + BinaryPrimitives.WriteInt32LittleEndian(buffer.AsSpan(0, sizeof(Int32)), value.Length); + ReadOnlySpan srcBytes = MemoryMarshal.AsBytes(value.AsSpan()); + srcBytes.CopyTo(buffer.AsSpan(sizeof(Int32))); + return buffer; + } + + private static Int32 EncodeVector(Single[] value, Byte[] buffer, Int32 offset) + { + var byteLen = checked(value.Length * sizeof(Single)); + if (buffer.Length < offset + sizeof(Int32) + byteLen) + throw new ArgumentException($"Buffer too short to encode Vector (need {offset + sizeof(Int32) + byteLen} bytes, got {buffer.Length})"); + + BinaryPrimitives.WriteInt32LittleEndian(buffer.AsSpan(offset, sizeof(Int32)), value.Length); + ReadOnlySpan srcBytes = MemoryMarshal.AsBytes(value.AsSpan()); + srcBytes.CopyTo(buffer.AsSpan(offset + sizeof(Int32))); + return sizeof(Int32) + byteLen; + } + private static Boolean DecodeBoolean(Byte[] buffer, Int32 offset) { if (buffer.Length < offset + 1) @@ -202,37 +397,12 @@ private static Decimal DecodeDecimal(Byte[] buffer, Int32 offset) return new Decimal(bits); } - private static Byte[] EncodeString(String value) - { - var valueBytesLength = _encoding.GetByteCount(value); - var buffer = new Byte[4 + valueBytesLength]; - BinaryPrimitives.WriteInt32LittleEndian(buffer.AsSpan(0, 4), valueBytesLength); - _encoding.GetBytes(value, buffer.AsSpan(4)); - return buffer; - } - private static String DecodeString(Byte[] buffer, Int32 offset) { var length = BitConverter.ToInt32(buffer, offset); return _encoding.GetString(buffer, offset + 4, length); } - private static Byte[] EncodeByteArray(Byte[] value) - { - var buffer = new Byte[4 + value.Length]; - BinaryPrimitives.WriteInt32LittleEndian(buffer.AsSpan(0, 4), value.Length); - value.AsSpan().CopyTo(buffer.AsSpan(4)); - return buffer; - } - - private static Byte[] EncodeGeoPoint(GeoPoint value) - { - var buffer = new Byte[16]; - WriteDoubleLittleEndian(buffer.AsSpan(0, 8), value.Latitude); - WriteDoubleLittleEndian(buffer.AsSpan(8, 8), value.Longitude); - return buffer; - } - private static GeoPoint DecodeGeoPoint(Byte[] buffer, Int32 offset) { if (buffer.Length < offset + 16) @@ -242,16 +412,6 @@ private static GeoPoint DecodeGeoPoint(Byte[] buffer, Int32 offset) return new GeoPoint(lat, lon); } - private static Byte[] EncodeVector(Single[] value) - { - var byteLen = checked(value.Length * sizeof(float)); - var buffer = new Byte[sizeof(int) + byteLen]; - BinaryPrimitives.WriteInt32LittleEndian(buffer.AsSpan(0, sizeof(int)), value.Length); - ReadOnlySpan srcBytes = MemoryMarshal.AsBytes(value.AsSpan()); - srcBytes.CopyTo(buffer.AsSpan(sizeof(int))); - return buffer; - } - private static Single[] DecodeVector(Byte[] buffer, Int32 offset) { if (buffer.Length < offset + 4) @@ -282,7 +442,7 @@ private static Byte[] DecodeByteArray(Byte[] buffer, Int32 offset) return result; } - private static void WriteDoubleLittleEndian(Span destination, double value) + private static void WriteDoubleLittleEndian(Span destination, Double value) { #if NET6_0_OR_GREATER BinaryPrimitives.WriteDoubleLittleEndian(destination, value); diff --git a/NewLife.NovaDb/Engine/Flux/FluxEngine.Persist.cs b/NewLife.NovaDb/Engine/Flux/FluxEngine.Persist.cs index 7d27126..ef950f0 100644 --- a/NewLife.NovaDb/Engine/Flux/FluxEngine.Persist.cs +++ b/NewLife.NovaDb/Engine/Flux/FluxEngine.Persist.cs @@ -1,4 +1,6 @@ -using System.Runtime.InteropServices.ComTypes; +using System.Buffers; +using System.Buffers.Binary; +using System.Runtime.InteropServices.ComTypes; using System.Text; using NewLife.NovaDb.Utilities; using NewLife.Security; @@ -115,32 +117,38 @@ private void PersistFluxAppend(FluxEntry entry) { if (_fluxLogStream == null) return; - using var ms = new MemoryStream(); - using var bw = new BinaryWriter(ms); + // 初始容量给个经验值:头部(8+4) + fields/tags 数量 + 少量字符串 + // 不够会自动扩容(扩容也不会产生 GC 垃圾,走 ArrayPool) + var w = new PooledBufferWriter(initialCapacity: 1024); + try + { + w.WriteInt64(entry.Timestamp); + w.WriteInt32(entry.SequenceId); - bw.Write(entry.Timestamp); - bw.Write(entry.SequenceId); + // Fields + var fields = entry.Fields; + w.WriteInt32(fields.Count); + foreach (var kv in fields) + { + WriteString(ref w, kv.Key); + WriteFieldValue(ref w, kv.Value); + } - // Fields - var fields = entry.Fields; - bw.Write(fields.Count); - foreach (var kv in fields) - { - WriteString(bw, kv.Key); - WriteFieldValue(bw, kv.Value); - } + // Tags + var tags = entry.Tags; + w.WriteInt32(tags.Count); + foreach (var kv in tags) + { + WriteString(ref w, kv.Key); + WriteString(ref w, kv.Value); + } - // Tags - var tags = entry.Tags; - bw.Write(tags.Count); - foreach (var kv in tags) + WriteFluxRecord(1, w.Buffer, 0, w.WrittenCount); + } + finally { - WriteString(bw, kv.Key); - WriteString(bw, kv.Value); + w.Dispose(); } - - var data = ms.ToArray(); - WriteFluxRecord(RecordType_FluxAppend, data, 0, data.Length); } /// 持久化 Purge 记录(删除过期分区) @@ -153,29 +161,64 @@ private void PersistFluxPurge(String cutoffKey) WriteFluxRecord(RecordType_FluxPurge, pooledBytes.Buffer, 0, pooledBytes.Length); } - /// 写入一条记录 - private void WriteFluxRecord(Byte recordType, Byte[] data, int offset, int count) + private void WriteFluxRecord(byte recordType, Byte[] data, int offset, int count) { - var recordLength = 1 + count + 4; + // recordLength: [recordType(1)] + [payload(count)] + [crc32(4)] + var recordLength = checked(1 + count + 4); + var totalLength = checked(4 + recordLength); // length prefix + record - using var ms = new MemoryStream(4 + recordLength); - using var bw = new BinaryWriter(ms); + Byte[]? rented = null; + var buffer = totalLength <= 1024 + ? stackalloc Byte[totalLength] + : (rented = ArrayPool.Shared.Rent(totalLength)).AsSpan(0, totalLength); - bw.Write(recordLength); - bw.Write(recordType); - bw.Write(data, offset, count); + try + { + // 写 recordLength (Little Endian) + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(0, 4), recordLength); - // CRC32 校验 - var checkBuffer = new Byte[1 + count]; - checkBuffer[0] = recordType; - Array.Copy(data, offset, checkBuffer, 1, count); - var checksum = Crc32.Compute(checkBuffer, 0, checkBuffer.Length); - bw.Write(checksum); + // 写 recordType + buffer[4] = recordType; - var buffer = ms.ToArray(); - _fluxLogStream!.Position = _fluxLogStream.Length; - _fluxLogStream.Write(buffer, 0, buffer.Length); - _fluxLogStream.Flush(); + // 写 payload + data.AsSpan(offset, count).CopyTo(buffer.Slice(5, count)); + + // 计算 CRC32:覆盖 [recordType + payload],也就是 buffer[4..(5+count)) + var checksum = Crc32.Compute(buffer.Slice(4, 1 + count)); + + // 写 CRC32 (Little Endian) + BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(5 + count, 4), checksum); + + // 写入到底层流 + _fluxLogStream!.Seek(0, SeekOrigin.End); +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER + _fluxLogStream.Write(buffer); // .NET Standard 2.1+ 有 Span 重载;低版本见下面兼容写法 +#else + if (rented is not null) + { + _fluxLogStream.Write(rented, 0, totalLength); + } + else + { + var tempBuffer = ArrayPool.Shared.Rent(totalLength); + try + { + buffer.CopyTo(tempBuffer.AsSpan(0, totalLength)); + _fluxLogStream.Write(tempBuffer, 0, totalLength); + } + finally + { + ArrayPool.Shared.Return(tempBuffer); + } + } +#endif + _fluxLogStream.Flush(); + } + finally + { + if (rented is not null) + ArrayPool.Shared.Return(rented); + } } #endregion @@ -183,15 +226,11 @@ private void WriteFluxRecord(Byte recordType, Byte[] data, int offset, int count #region 字段序列化 /// 写入 UTF-8 字符串(长度前缀) - private static void WriteString(BinaryWriter bw, String value) + private static void WriteString(ref PooledBufferWriter w, string value) { - using var bytes = _encoding.GetPooledEncodedBytes(value); - bw.Write(bytes.Length); -#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER - bw.Write(bytes.AsSpan()); -#else - bw.Write(bytes.Buffer, 0, bytes.Length); -#endif + using var bytes = value.ToPooledUtf8Bytes(); + w.WriteInt32(bytes.Length); + w.WriteBytes(bytes.AsSpan()); } /// 读取 UTF-8 字符串(长度前缀) @@ -203,38 +242,38 @@ private static String ReadString(BinaryReader br) } /// 写入字段值(带类型标签) - private static void WriteFieldValue(BinaryWriter bw, Object? value) + private static void WriteFieldValue(ref PooledBufferWriter w, Object? value) { switch (value) { case null: - bw.Write(TypeTag_Null); + w.WriteByte(TypeTag_Null); break; case Int32 i: - bw.Write(TypeTag_Int32); - bw.Write(i); + w.WriteByte(TypeTag_Int32); + w.WriteInt32(i); break; case Int64 l: - bw.Write(TypeTag_Int64); - bw.Write(l); + w.WriteByte(TypeTag_Int64); + w.WriteInt64(l); break; case Double d: - bw.Write(TypeTag_Double); - bw.Write(d); + w.WriteByte(TypeTag_Double); + w.WriteDouble(d); break; case Boolean b: - bw.Write(TypeTag_Boolean); - bw.Write(b); + w.WriteByte(TypeTag_Boolean); + w.WriteBool(b); break; case Byte[] bytes: - bw.Write(TypeTag_Bytes); - bw.Write(bytes.Length); - bw.Write(bytes); + w.WriteByte(TypeTag_Bytes); + w.WriteInt32(bytes.Length); + w.WriteBytes(bytes); break; default: // 其他类型统一转为 String - bw.Write(TypeTag_String); - WriteString(bw, value.ToString() ?? ""); + w.WriteByte(TypeTag_String); + WriteString(ref w, value.ToString() ?? string.Empty); break; } } diff --git a/NewLife.NovaDb/Engine/KV/KvEntry.cs b/NewLife.NovaDb/Engine/KV/KvEntry.cs index e11eecf..a4434be 100644 --- a/NewLife.NovaDb/Engine/KV/KvEntry.cs +++ b/NewLife.NovaDb/Engine/KV/KvEntry.cs @@ -1,4 +1,6 @@ -namespace NewLife.NovaDb.Engine.KV; +using System.Runtime.CompilerServices; + +namespace NewLife.NovaDb.Engine.KV; /// KV 内存索引项。Bitcask 模型仅索引驻留内存,值保留在磁盘按需读取 /// @@ -17,5 +19,6 @@ public struct KvEntry public DateTime ExpiresAt; /// 检查是否已过期 + [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly Boolean IsExpired() => ExpiresAt < DateTime.MaxValue && DateTime.UtcNow >= ExpiresAt; } diff --git a/NewLife.NovaDb/Engine/KV/KvStore.Persist.cs b/NewLife.NovaDb/Engine/KV/KvStore.Persist.cs index 0f4de0a..d808b16 100644 --- a/NewLife.NovaDb/Engine/KV/KvStore.Persist.cs +++ b/NewLife.NovaDb/Engine/KV/KvStore.Persist.cs @@ -1,6 +1,7 @@ using System.Buffers; using System.IO.MemoryMappedFiles; -using System.Text; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using NewLife.Buffers; using NewLife.Data; using NewLife.NovaDb.Core; @@ -349,10 +350,17 @@ private void LoadFromFileMmf(Int64 fileLength) if (totalLength < 5 || pos + 4 + totalLength > fileLength) break; // 读取完整记录体 - var body = new Byte[totalLength]; - accessor.ReadArray(pos + 4, body, 0, totalLength); + var body = ArrayPool.Shared.Rent(totalLength); + try + { + accessor.ReadArray(pos + 4, body, 0, totalLength); - ReplayRecord(body, totalLength, pos); + ReplayRecord(body.AsSpan(0, totalLength), pos); + } + finally + { + ArrayPool.Shared.Return(body); + } pos += 4 + totalLength; } @@ -370,44 +378,59 @@ private void LoadFromFileStream() { _fileStream!.Position = FileHeaderSize; +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER + Span lenBuf = stackalloc Byte[4]; +#else + var lenBuf = new Byte[4]; +#endif while (_fileStream.Position < _fileStream.Length) { var recordStart = _fileStream.Position; - var lenBuf = new Byte[4]; +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER + if (_fileStream.Read(lenBuf) < 4) break; +#else if (_fileStream.Read(lenBuf, 0, 4) < 4) break; - var totalLength = BitConverter.ToInt32(lenBuf, 0); +#endif + + var totalLength = Unsafe.ReadUnaligned(ref lenBuf[0]); if (totalLength < 5) break; - var body = new Byte[totalLength]; - if (_fileStream.Read(body, 0, totalLength) < totalLength) break; + var body = ArrayPool.Shared.Rent(totalLength); + try + { + if (_fileStream.Read(body, 0, totalLength) < totalLength) break; - ReplayRecord(body, totalLength, recordStart); + ReplayRecord(body.AsSpan(0, totalLength), recordStart); + } + finally + { + ArrayPool.Shared.Return(body); + } } } /// 回放一条记录 /// 记录体(不含 TotalLength 前缀) - /// 记录总长度 /// 记录在文件中的起始位置(TotalLength 字段所在位置) - private void ReplayRecord(Byte[] body, Int32 totalLength, Int64 recordFileOffset) + private void ReplayRecord(ReadOnlySpan body, Int64 recordFileOffset) { var recordType = (KvRecordType)body[0]; - var dataLength = totalLength - 1 - 4; + var dataLength = body.Length - 1 - 4; if (dataLength < 0) return; // 校验 CRC32 - var expectedChecksum = BitConverter.ToUInt32(body, totalLength - 4); - var actualChecksum = Crc32.Compute(body, 0, 1 + dataLength); + var expectedChecksum = Unsafe.ReadUnaligned(ref MemoryMarshal.GetReference(body.Slice(body.Length - 4))); + var actualChecksum = Crc32.Compute(body.Slice(0, 1 + dataLength)); if (expectedChecksum != actualChecksum) return; switch (recordType) { case KvRecordType.Set: - ReplaySet(body, 1, dataLength, recordFileOffset); + ReplaySet(body.Slice(1, dataLength), recordFileOffset); break; case KvRecordType.Delete: - ReplayDelete(body, 1, dataLength); + ReplayDelete(body.Slice(1, dataLength)); break; case KvRecordType.Clear: _data.Clear(); @@ -417,12 +440,10 @@ private void ReplayRecord(Byte[] body, Int32 totalLength, Int64 recordFileOffset /// 回放 Set 记录,重建内存索引(值留在磁盘)。跳过已过期的键以避免加载浪费内存 /// 记录体 - /// 数据起始偏移 - /// 数据长度 /// 记录在文件中的起始位置 - private void ReplaySet(Byte[] body, Int32 offset, Int32 dataLength, Int64 recordFileOffset) + private void ReplaySet(ReadOnlySpan body, Int64 recordFileOffset) { - var reader = new SpanReader(body, offset, dataLength); + var reader = new SpanReader(body); var keyLen = reader.ReadUInt16(); var key = reader.ReadString(keyLen); @@ -446,11 +467,9 @@ private void ReplaySet(Byte[] body, Int32 offset, Int32 dataLength, Int64 record /// 回放 Delete 记录 /// 记录体 - /// 数据起始偏移 - /// 数据长度 - private void ReplayDelete(Byte[] body, Int32 offset, Int32 dataLength) + private void ReplayDelete(ReadOnlySpan body) { - var reader = new SpanReader(body, offset, dataLength); + var reader = new SpanReader(body); var keyLen = reader.ReadUInt16(); var key = reader.ReadString(keyLen); @@ -575,5 +594,5 @@ private void CompactNoLock() _compacting = false; } } -#endregion + #endregion } diff --git a/NewLife.NovaDb/Engine/NovaTable.Persist.cs b/NewLife.NovaDb/Engine/NovaTable.Persist.cs index 356ad02..7a368d5 100644 --- a/NewLife.NovaDb/Engine/NovaTable.Persist.cs +++ b/NewLife.NovaDb/Engine/NovaTable.Persist.cs @@ -1,4 +1,6 @@ -using NewLife.NovaDb.Core; +using System.Buffers.Binary; +using NewLife.NovaDb.Core; +using NewLife.NovaDb.Utilities; using NewLife.Security; namespace NewLife.NovaDb.Engine; @@ -140,27 +142,31 @@ private void WriteRecord(Byte recordType, Byte[] data) { // 格式:[RecordLength: 4B] [RecordType: 1B] [Data: variable] [Checksum: 4B] // RecordLength = 1 + data.Length + 4(不含 RecordLength 自身) - var recordLength = 1 + data.Length + 4; + var recordLength = checked(1 + data.Length + 4); + var totalLength = checked(4 + recordLength); // 4B length prefix + record - using var ms = new MemoryStream(4 + recordLength); - using var bw = new BinaryWriter(ms); + using var w = new PooledBufferWriter(initialCapacity: totalLength); - bw.Write(recordLength); - bw.Write(recordType); - bw.Write(data); + // 1) 写 RecordLength + w.WriteInt32(recordLength); - // 计算校验和(覆盖 RecordType + Data) - var checkBuffer = new Byte[1 + data.Length]; - checkBuffer[0] = recordType; - Array.Copy(data, 0, checkBuffer, 1, data.Length); - var checksum = Crc32.Compute(checkBuffer, 0, checkBuffer.Length); - bw.Write(checksum); + // 2) 写 RecordType + w.WriteByte(recordType); - var buffer = ms.ToArray(); + // 3) 写 Data + w.WriteBytes(data.AsSpan()); - // 追加到文件末尾 + // 4) CRC32 覆盖 [RecordType + Data] + // 这里直接对 writer 内部缓冲切片计算,不再构造 checkBuffer + // recordType 位于 offset=4,长度=1+data.Length + var checksum = Crc32.Compute(w.Buffer.AsSpan(4, 1 + data.Length)); + + // 5) 写 Checksum(小端) + w.WriteUInt32(checksum); + + // 6) 直接追加写入文件 _rowLogStream!.Position = _rowLogStream.Length; - _rowLogStream.Write(buffer, 0, buffer.Length); + _rowLogStream.Write(w.Buffer, 0, w.WrittenCount); _rowLogStream.Flush(); } diff --git a/NewLife.NovaDb/Engine/NovaTable.cs b/NewLife.NovaDb/Engine/NovaTable.cs index bfc4cdf..1ed83cc 100644 --- a/NewLife.NovaDb/Engine/NovaTable.cs +++ b/NewLife.NovaDb/Engine/NovaTable.cs @@ -1,6 +1,7 @@ using NewLife.NovaDb.Core; using NewLife.NovaDb.Storage; using NewLife.NovaDb.Tx; +using NewLife.NovaDb.Utilities; using NewLife.NovaDb.WAL; namespace NewLife.NovaDb.Engine; @@ -750,22 +751,28 @@ private void DeleteSecondaryIndexFile(String indexName) /// 序列化行数据 private Byte[] SerializeRow(Object?[] row) { - using var ms = new MemoryStream(); - using var bw = new BinaryWriter(ms); + // 估一个常用初始容量,减少扩容次数。 + // 暂定256字节,实际根据列数和数据类型可能需要调整。 + using var w = new PooledBufferWriter(initialCapacity: 256); - // 写入列数 - bw.Write(row.Length); + // 写入列数(Int32,小端) + w.WriteInt32(row.Length); // 写入每列的值 for (var i = 0; i < row.Length; i++) { var colDef = _schema.Columns[i]; - var encoded = _codec.Encode(row[i], colDef.DataType); - bw.Write(encoded.Length); - bw.Write(encoded); + var encodedLength = _codec.GetEncodedLength(row[i], colDef.DataType); + w.WriteInt32(encodedLength); + + var segment = w.GetWritableSegment(encodedLength); + _codec.Encode(row[i], colDef.DataType, segment.Array!, segment.Offset); } - return ms.ToArray(); + var len = w.WrittenCount; + var result = new Byte[len]; + Buffer.BlockCopy(w.Buffer, 0, result, 0, len); + return result; } /// 反序列化行数据 diff --git a/NewLife.NovaDb/Utilities/PooledBufferWriter.cs b/NewLife.NovaDb/Utilities/PooledBufferWriter.cs new file mode 100644 index 0000000..f9fa3a9 --- /dev/null +++ b/NewLife.NovaDb/Utilities/PooledBufferWriter.cs @@ -0,0 +1,150 @@ +using System.Buffers; +using System.Buffers.Binary; +using System.Runtime.CompilerServices; + +namespace NewLife.NovaDb.Utilities +{ + /// + /// 池化的字节数组写入器,提供高效的写入方法。
+ /// 使用对象池管理字节数组,避免频繁分配和垃圾回收。
+ /// 使用完毕后应调用 方法归还数组到 对象池。 + ///
+ internal struct PooledBufferWriter : IDisposable + { +#if NET45 + private static readonly Byte[] EmptyBytes = new Byte[0]; +#else + private static readonly Byte[] EmptyBytes = Array.Empty(); +#endif + + private Byte[] _buffer; + private Int32 _pos; + + public PooledBufferWriter(Int32 initialCapacity) + { + _buffer = ArrayPool.Shared.Rent(initialCapacity); + _pos = 0; + } + + public readonly Int32 WrittenCount => _pos; + public readonly Byte[] Buffer => _buffer; + + public void Dispose() + { + var buf = _buffer; + _buffer = EmptyBytes; + _pos = 0; + if (buf.Length != 0) + ArrayPool.Shared.Return(buf); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Ensure(Int32 sizeHint) + { + if ((UInt32)(_pos + sizeHint) <= (UInt32)_buffer.Length) return; + + var newSize = _buffer.Length * 2; + var needed = _pos + sizeHint; + if (newSize < needed) newSize = needed; + + var newBuf = ArrayPool.Shared.Rent(newSize); + System.Buffer.BlockCopy(_buffer, 0, newBuf, 0, _pos); + ArrayPool.Shared.Return(_buffer); + _buffer = newBuf; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteByte(Byte value) + { + Ensure(1); + _buffer[_pos++] = value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteInt32(Int32 value) + { + Ensure(4); + BinaryPrimitives.WriteInt32LittleEndian(_buffer.AsSpan(_pos, 4), value); + _pos += 4; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteUInt32(UInt32 value) + { + Ensure(4); + BinaryPrimitives.WriteUInt32LittleEndian(_buffer.AsSpan(_pos, 4), value); + _pos += 4; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteInt64(Int64 value) + { + Ensure(8); + BinaryPrimitives.WriteInt64LittleEndian(_buffer.AsSpan(_pos, 8), value); + _pos += 8; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteUInt64(UInt64 value) + { + Ensure(8); + BinaryPrimitives.WriteUInt64LittleEndian(_buffer.AsSpan(_pos, 8), value); + _pos += 8; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteDouble(Double value) + { + // 兼容低于 .NET 6:用 bits + WriteInt64 +#if NET6_0_OR_GREATER + Ensure(8); + BinaryPrimitives.WriteDoubleLittleEndian(_buffer.AsSpan(_pos, 8), value); + _pos += 8; +#else + WriteInt64(BitConverter.DoubleToInt64Bits(value)); +#endif + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteBool(Boolean value) => WriteByte(value ? (Byte)1 : (Byte)0); + + public void WriteBytes(ReadOnlySpan src) + { + Ensure(src.Length); + src.CopyTo(_buffer.AsSpan(_pos, src.Length)); + _pos += src.Length; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteBytes(Byte[] src) => WriteBytes(src.AsSpan()); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteBytes(Byte[] src, Int32 offset, Int32 count) => WriteBytes(src.AsSpan(offset, count)); + + /// + /// 获取一个可写的字节数组切片,长度由参数指定。 + /// + /// 要获取的字节数组切片的长度 + /// 返回一个可写的字节数组切片 + public Span GetWritableSpan(Int32 length) + { + Ensure(length); + var span = _buffer.AsSpan(_pos, length); + _pos += length; + return span; + } + + /// + /// 获取一个可写的字节数组切片,长度由参数指定,但返回值类型为 ,以兼容某些需要 ArraySegment 的 API。 + /// + /// 要获取的字节数组切片的长度 + /// 返回一个可写的字节数组切片 + public ArraySegment GetWritableSegment(Int32 length) + { + Ensure(length); + var segment = new ArraySegment(_buffer, _pos, length); + _pos += length; + return segment; + } + } +} diff --git a/NewLife.NovaDb/WAL/BinlogWriter.cs b/NewLife.NovaDb/WAL/BinlogWriter.cs index c7b06b2..7d62d33 100644 --- a/NewLife.NovaDb/WAL/BinlogWriter.cs +++ b/NewLife.NovaDb/WAL/BinlogWriter.cs @@ -236,59 +236,50 @@ private void WriteEvent(BinlogEventType eventType, String sql, Int32 affectedRow { if (_stream == null) return; - using var ms = new MemoryStream(); - using var bw = new BinaryWriter(ms); + sql ??= string.Empty; - bw.Write((Byte)eventType); - bw.Write(DateTime.UtcNow.Ticks); + var encoding = Encoding.UTF8; + + // 先构造 Data 部分(不含 RecordLength 和 checksum) + using var dataWriter = new PooledBufferWriter(initialCapacity: 1024); + + dataWriter.WriteByte((Byte)eventType); + dataWriter.WriteInt64(DateTime.UtcNow.Ticks); // 数据库名 - using (var dbBytes = _database.ToPooledUtf8Bytes()) - { - bw.Write(dbBytes.Length); - bw.Write(dbBytes.Buffer, 0, dbBytes.Length); - } + var dbByteLength = encoding.GetByteCount(_database); + dataWriter.WriteInt32(dbByteLength); + var dbBuffer = dataWriter.GetWritableSpan(dbByteLength); + encoding.GetBytes(_database, dbBuffer); // SQL 文本 - using (var sqlBytes = (sql ?? "").ToPooledUtf8Bytes()) - { - bw.Write(sqlBytes.Length); - bw.Write(sqlBytes.Buffer, 0, sqlBytes.Length); - } + var sqlByteLength = encoding.GetByteCount(sql); + dataWriter.WriteInt32(sqlByteLength); + var sqlBuffer = dataWriter.GetWritableSpan(sqlByteLength); + encoding.GetBytes(sql, sqlBuffer); - bw.Write(affectedRows); - - var data = ms.ToArray(); + dataWriter.WriteInt32(affectedRows); // 计算 CRC32 - var checksum = Crc32.Compute(data, 0, data.Length); + var dataLen = dataWriter.WrittenCount; + var checksum = Crc32.Compute(dataWriter.Buffer.AsSpan(0, dataLen)); // 写入记录:[RecordLength: 4B] [Data] [Checksum: 4B] - var recordLength = data.Length + 4; + var recordLength = dataLen + 4; + var totalLen = checked(4 + dataLen + 4); - _stream.Position = _stream.Length; + using var recordWriter = new PooledBufferWriter(initialCapacity: totalLen); -#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER - Span lenBuf = stackalloc Byte[4]; - BinaryPrimitives.WriteInt32LittleEndian(lenBuf, recordLength); - _stream.Write(lenBuf); - _stream.Write(data.AsSpan()); - - Span csumBuf = stackalloc Byte[4]; - BinaryPrimitives.WriteUInt32LittleEndian(csumBuf, checksum); - _stream.Write(csumBuf); - _stream.Flush(); -#else - var lenBuf = new Byte[4]; - BinaryPrimitives.WriteInt32LittleEndian(lenBuf.AsSpan(), recordLength); - _stream.Write(lenBuf, 0, 4); - _stream.Write(data, 0, data.Length); - - var csumBuf = new Byte[4]; - BinaryPrimitives.WriteUInt32LittleEndian(csumBuf.AsSpan(), checksum); - _stream.Write(csumBuf, 0, 4); + BinaryPrimitives.WriteInt32LittleEndian(recordWriter.GetWritableSpan(4), recordLength); + + // 拷贝 Data(一次) + recordWriter.WriteBytes(dataWriter.Buffer, 0, dataLen); + + BinaryPrimitives.WriteUInt32LittleEndian(recordWriter.GetWritableSpan(4), checksum); + + _stream.Position = _stream.Length; + _stream.Write(recordWriter.Buffer, 0, recordWriter.WrittenCount); _stream.Flush(); -#endif _position++; } From a935740aec51dd2d6360f2e94dd81506bd495eee Mon Sep 17 00:00:00 2001 From: Allen Cai Date: Fri, 6 Mar 2026 10:55:24 +0800 Subject: [PATCH 10/15] =?UTF-8?q?=E6=96=B0=E5=A2=9EPooledBufferWriter?= =?UTF-8?q?=E6=97=A0=E5=8F=82=E6=9E=84=E9=80=A0=E5=87=BD=E6=95=B0=EF=BC=8C?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E5=AE=B9=E9=87=8F256?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 增加了PooledBufferWriter的无参构造函数,默认从ArrayPool租用长度为256的字节数组。 顺便防止由于开发者无意中使用default(PooledBufferWriter),导致使用时抛出_buffer空指针异常。 --- NewLife.NovaDb/Utilities/PooledBufferWriter.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/NewLife.NovaDb/Utilities/PooledBufferWriter.cs b/NewLife.NovaDb/Utilities/PooledBufferWriter.cs index f9fa3a9..1c1be10 100644 --- a/NewLife.NovaDb/Utilities/PooledBufferWriter.cs +++ b/NewLife.NovaDb/Utilities/PooledBufferWriter.cs @@ -20,6 +20,12 @@ internal struct PooledBufferWriter : IDisposable private Byte[] _buffer; private Int32 _pos; + public PooledBufferWriter() + { + _buffer = ArrayPool.Shared.Rent(256); + _pos = 0; + } + public PooledBufferWriter(Int32 initialCapacity) { _buffer = ArrayPool.Shared.Rent(initialCapacity); From 45e140e017dd680c6f6710ae90309192998370f3 Mon Sep 17 00:00:00 2001 From: Allen Cai Date: Fri, 6 Mar 2026 10:57:07 +0800 Subject: [PATCH 11/15] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=90=91=E9=87=8F?= =?UTF-8?q?=E7=BC=96=E7=A0=81=E7=9A=84=20buffer=20=E8=BE=B9=E7=95=8C?= =?UTF-8?q?=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 TestEncodeVectorWithBuffer 和 TestEncodeVectorWithShortBuffer 两个测试方法,分别验证向量编码时 buffer 长度充足和不足的情况,提升 _codec.Encode 重载方法的健壮性和正确性。 --- XUnitTest/Core/DataCodecTests.cs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/XUnitTest/Core/DataCodecTests.cs b/XUnitTest/Core/DataCodecTests.cs index 503b573..b6ef651 100644 --- a/XUnitTest/Core/DataCodecTests.cs +++ b/XUnitTest/Core/DataCodecTests.cs @@ -323,4 +323,29 @@ public void TestGetEncodedLengthVector() var value = new Single[] { 1.0f, 2.0f, 3.0f }; Assert.Equal(4 + 3 * 4, _codec.GetEncodedLength(value, DataType.Vector)); } + + [Fact] + public void TestEncodeVectorWithBuffer() + { + var value = new Single[] { 1.0f, 2.0f, 3.0f }; + var length = _codec.GetEncodedLength(value, DataType.Vector); + Assert.Equal(4 + 3 * 4, length); + + var buffer = new Byte[length]; + length = _codec.Encode(value, DataType.Vector, buffer, 0); + Assert.Equal(4 + 3 * 4, length); + + var encoded = _codec.Encode(value, DataType.Vector); + Assert.Equal(encoded, buffer); + } + + [Fact] + public void TestEncodeVectorWithShortBuffer() + { + var value = new Single[] { 1.0f, 2.0f, 3.0f }; + var length = _codec.GetEncodedLength(value, DataType.Vector); + Assert.Equal(4 + 3 * 4, length); + var buffer = new Byte[length - 1]; // 故意比需要的长度短1字节 + Assert.Throws(() => _codec.Encode(value, DataType.Vector, buffer, 0)); + } } From e2a73e3fe115c6be6b585798b30ed019336ed0d3 Mon Sep 17 00:00:00 2001 From: Allen Cai Date: Fri, 6 Mar 2026 11:01:43 +0800 Subject: [PATCH 12/15] =?UTF-8?q?=E7=BB=9F=E4=B8=80=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E4=B8=BA=20.NET=20=E6=A0=87=E5=87=86?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=EF=BC=8C=E8=A7=84=E8=8C=83=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E9=A3=8E=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 本次提交将 int、long、double、byte、string、bool 等基础类型统一替换为 .NET 标准类型(如 Int32、Int64、Double、Byte、String、Boolean),并同步规范了相关方法参数、常量、泛型、空字符串写法等。此更改提升了代码一致性和可维护性,不涉及业务逻辑变更。 --- NewLife.NovaDb/Core/DataType.cs | 2 +- NewLife.NovaDb/Engine/Flux/FluxEngine.Persist.cs | 8 ++++---- NewLife.NovaDb/Engine/KV/KvStore.Persist.cs | 6 +++--- NewLife.NovaDb/Engine/KV/KvStore.cs | 8 ++++---- NewLife.NovaDb/Server/KvPacket.cs | 2 +- NewLife.NovaDb/Utilities/EncodingExtensions.cs | 6 +++--- NewLife.NovaDb/Utilities/HashHelper.cs | 2 +- NewLife.NovaDb/WAL/BinlogWriter.cs | 2 +- 8 files changed, 18 insertions(+), 18 deletions(-) diff --git a/NewLife.NovaDb/Core/DataType.cs b/NewLife.NovaDb/Core/DataType.cs index 88d14d6..73689a4 100644 --- a/NewLife.NovaDb/Core/DataType.cs +++ b/NewLife.NovaDb/Core/DataType.cs @@ -96,7 +96,7 @@ public readonly struct GeoPoint(Double latitude, Double longitude) : IEquatable< { /// 地球平均半径(米) private const Double EarthRadiusMeters = 6_371_000.0; - private const double DegToRad = Math.PI / 180.0; + private const Double DegToRad = Math.PI / 180.0; /// 纬度(-90 到 90) public Double Latitude { get; } = latitude; diff --git a/NewLife.NovaDb/Engine/Flux/FluxEngine.Persist.cs b/NewLife.NovaDb/Engine/Flux/FluxEngine.Persist.cs index ef950f0..6fd7928 100644 --- a/NewLife.NovaDb/Engine/Flux/FluxEngine.Persist.cs +++ b/NewLife.NovaDb/Engine/Flux/FluxEngine.Persist.cs @@ -161,7 +161,7 @@ private void PersistFluxPurge(String cutoffKey) WriteFluxRecord(RecordType_FluxPurge, pooledBytes.Buffer, 0, pooledBytes.Length); } - private void WriteFluxRecord(byte recordType, Byte[] data, int offset, int count) + private void WriteFluxRecord(Byte recordType, Byte[] data, Int32 offset, Int32 count) { // recordLength: [recordType(1)] + [payload(count)] + [crc32(4)] var recordLength = checked(1 + count + 4); @@ -217,7 +217,7 @@ private void WriteFluxRecord(byte recordType, Byte[] data, int offset, int count finally { if (rented is not null) - ArrayPool.Shared.Return(rented); + ArrayPool.Shared.Return(rented); } } @@ -226,7 +226,7 @@ private void WriteFluxRecord(byte recordType, Byte[] data, int offset, int count #region 字段序列化 /// 写入 UTF-8 字符串(长度前缀) - private static void WriteString(ref PooledBufferWriter w, string value) + private static void WriteString(ref PooledBufferWriter w, String value) { using var bytes = value.ToPooledUtf8Bytes(); w.WriteInt32(bytes.Length); @@ -273,7 +273,7 @@ private static void WriteFieldValue(ref PooledBufferWriter w, Object? value) default: // 其他类型统一转为 String w.WriteByte(TypeTag_String); - WriteString(ref w, value.ToString() ?? string.Empty); + WriteString(ref w, value.ToString() ?? String.Empty); break; } } diff --git a/NewLife.NovaDb/Engine/KV/KvStore.Persist.cs b/NewLife.NovaDb/Engine/KV/KvStore.Persist.cs index d808b16..e83666b 100644 --- a/NewLife.NovaDb/Engine/KV/KvStore.Persist.cs +++ b/NewLife.NovaDb/Engine/KV/KvStore.Persist.cs @@ -393,7 +393,7 @@ private void LoadFromFileStream() if (_fileStream.Read(lenBuf, 0, 4) < 4) break; #endif - var totalLength = Unsafe.ReadUnaligned(ref lenBuf[0]); + var totalLength = Unsafe.ReadUnaligned(ref lenBuf[0]); if (totalLength < 5) break; var body = ArrayPool.Shared.Rent(totalLength); @@ -420,7 +420,7 @@ private void ReplayRecord(ReadOnlySpan body, Int64 recordFileOffset) if (dataLength < 0) return; // 校验 CRC32 - var expectedChecksum = Unsafe.ReadUnaligned(ref MemoryMarshal.GetReference(body.Slice(body.Length - 4))); + var expectedChecksum = Unsafe.ReadUnaligned(ref MemoryMarshal.GetReference(body.Slice(body.Length - 4))); var actualChecksum = Crc32.Compute(body.Slice(0, 1 + dataLength)); if (expectedChecksum != actualChecksum) return; @@ -548,7 +548,7 @@ private void CompactNoLock() if (kvp.Value.IsExpired()) continue; using var pk = ReadValueFromDiskNoLock(kvp.Value); - var value = pk != null ? pk.GetSpan() : ReadOnlySpan.Empty; + var value = pk != null ? pk.GetSpan() : ReadOnlySpan.Empty; var valueOffset = WriteSetRecordToStream(tempStream, kvp.Key, value, kvp.Value.ExpiresAt); newEntries[kvp.Key] = new KvEntry diff --git a/NewLife.NovaDb/Engine/KV/KvStore.cs b/NewLife.NovaDb/Engine/KV/KvStore.cs index 50fe355..3b349c2 100644 --- a/NewLife.NovaDb/Engine/KV/KvStore.cs +++ b/NewLife.NovaDb/Engine/KV/KvStore.cs @@ -400,8 +400,8 @@ public Int64 Inc(String key, Int64 delta = 1, TimeSpan? ttl = null) expiresAt = ttl != null ? DateTime.UtcNow.Add(ttl.Value) : _defaultTtl != null ? DateTime.UtcNow.Add(_defaultTtl.Value) : DateTime.MaxValue; } - const int length = sizeof(long); - Span valueBytes = stackalloc byte[length]; + const Int32 length = sizeof(Int64); + Span valueBytes = stackalloc Byte[length]; Unsafe.WriteUnaligned(ref MemoryMarshal.GetReference(valueBytes), newValue); var valueOffset = WriteSetRecordNoLock(key, valueBytes, expiresAt); @@ -446,8 +446,8 @@ public Double IncDouble(String key, Double delta, TimeSpan? ttl = null) expiresAt = ttl != null ? DateTime.UtcNow.Add(ttl.Value) : _defaultTtl != null ? DateTime.UtcNow.Add(_defaultTtl.Value) : DateTime.MaxValue; } - const int length = sizeof(double); - Span valueBytes = stackalloc byte[length]; + const Int32 length = sizeof(Double); + Span valueBytes = stackalloc Byte[length]; Unsafe.WriteUnaligned(ref MemoryMarshal.GetReference(valueBytes), newValue); var valueOffset = WriteSetRecordNoLock(key, valueBytes, expiresAt); diff --git a/NewLife.NovaDb/Server/KvPacket.cs b/NewLife.NovaDb/Server/KvPacket.cs index d15e4f5..c5999da 100644 --- a/NewLife.NovaDb/Server/KvPacket.cs +++ b/NewLife.NovaDb/Server/KvPacket.cs @@ -542,7 +542,7 @@ private static String ReadString(ref SpanReader reader) #if NET45 private static readonly byte[] EmptyBytes = new byte[0]; #else - private static readonly byte[] EmptyBytes = Array.Empty(); + private static readonly Byte[] EmptyBytes = Array.Empty(); #endif #endregion diff --git a/NewLife.NovaDb/Utilities/EncodingExtensions.cs b/NewLife.NovaDb/Utilities/EncodingExtensions.cs index 4ef01ff..43995d5 100644 --- a/NewLife.NovaDb/Utilities/EncodingExtensions.cs +++ b/NewLife.NovaDb/Utilities/EncodingExtensions.cs @@ -15,7 +15,7 @@ internal static class EncodingExtensions /// /// 要转换的字符串。 /// 返回一个 实例,包含 UTF-8 编码的字节数组。 - public static PooledBytes ToPooledUtf8Bytes(this string value) => Encoding.GetPooledEncodedBytes(value); + public static PooledBytes ToPooledUtf8Bytes(this String value) => Encoding.GetPooledEncodedBytes(value); /// /// 将字符串转换为使用对象池管理的指定编码的字节数组。 @@ -23,10 +23,10 @@ internal static class EncodingExtensions /// 要使用的编码。 /// 要转换的字符串。 /// 返回一个 实例,包含指定编码的字节数组。 - public static PooledBytes GetPooledEncodedBytes(this Encoding encoding, string value) + public static PooledBytes GetPooledEncodedBytes(this Encoding encoding, String value) { var length = encoding.GetByteCount(value); - var pooledBytes = ArrayPool.Shared.Rent(length); + var pooledBytes = ArrayPool.Shared.Rent(length); encoding.GetBytes(value, 0, value.Length, pooledBytes, 0); return new PooledBytes(pooledBytes, length); } diff --git a/NewLife.NovaDb/Utilities/HashHelper.cs b/NewLife.NovaDb/Utilities/HashHelper.cs index f751951..5539bb7 100644 --- a/NewLife.NovaDb/Utilities/HashHelper.cs +++ b/NewLife.NovaDb/Utilities/HashHelper.cs @@ -67,7 +67,7 @@ private static String ComputeHash(String str, HashAlgorithmName alg, Int32 hashS } #if NET5_0_OR_GREATER - private static bool TryHashData(HashAlgorithmName alg, ReadOnlySpan data, Span dest) + private static Boolean TryHashData(HashAlgorithmName alg, ReadOnlySpan data, Span dest) { if (alg == HashAlgorithmName.MD5) return MD5.TryHashData(data, dest, out _); diff --git a/NewLife.NovaDb/WAL/BinlogWriter.cs b/NewLife.NovaDb/WAL/BinlogWriter.cs index 7d62d33..2620358 100644 --- a/NewLife.NovaDb/WAL/BinlogWriter.cs +++ b/NewLife.NovaDb/WAL/BinlogWriter.cs @@ -236,7 +236,7 @@ private void WriteEvent(BinlogEventType eventType, String sql, Int32 affectedRow { if (_stream == null) return; - sql ??= string.Empty; + sql ??= String.Empty; var encoding = Encoding.UTF8; From 5efce97cea38fc956c8989e597abf4a3769a0ab8 Mon Sep 17 00:00:00 2001 From: Allen Cai Date: Fri, 6 Mar 2026 11:03:41 +0800 Subject: [PATCH 13/15] =?UTF-8?q?=E4=BC=98=E5=8C=96=20using=20=E6=8C=87?= =?UTF-8?q?=E4=BB=A4=E4=B8=8E=E5=91=BD=E5=90=8D=E7=A9=BA=E9=97=B4=E5=A3=B0?= =?UTF-8?q?=E6=98=8E=EF=BC=8C=E6=8F=90=E5=8D=87=E4=BB=A3=E7=A0=81=E8=A7=84?= =?UTF-8?q?=E8=8C=83=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 本次提交主要清理了多个源文件中的冗余或未使用的 using 指令,规范了 using 与命名空间声明的顺序和格式。部分文件添加了 BOM 标记,统一了命名空间声明风格,去除了无用引用。此举有助于提升代码整洁性、可维护性,并减少编译器警告。 --- NewLife.NovaDb/Core/MetadataLock.cs | 4 +--- NewLife.NovaDb/Core/NovaMetrics.cs | 4 +--- NewLife.NovaDb/Core/SlowQueryLog.cs | 3 +-- NewLife.NovaDb/Engine/ColdIndexDirectory.cs | 4 +--- NewLife.NovaDb/Engine/Flux/FluxEngine.Persist.cs | 1 - NewLife.NovaDb/Engine/NovaTable.Persist.cs | 3 +-- NewLife.NovaDb/Server/NovaController.cs | 1 - NewLife.NovaDb/Sql/SqlEngine.DML.cs | 4 +--- NewLife.NovaDb/Sql/SqlEngine.Explain.cs | 4 +--- NewLife.NovaDb/Sql/SqlEngine.Select.cs | 1 - NewLife.NovaDb/Storage/DatabaseDirectory.cs | 3 +-- NewLife.NovaDb/Tx/TransactionManager.cs | 4 +--- NewLife.NovaDb/WAL/WalCheckpointer.cs | 4 +--- 13 files changed, 10 insertions(+), 30 deletions(-) diff --git a/NewLife.NovaDb/Core/MetadataLock.cs b/NewLife.NovaDb/Core/MetadataLock.cs index 84f875e..71a7221 100644 --- a/NewLife.NovaDb/Core/MetadataLock.cs +++ b/NewLife.NovaDb/Core/MetadataLock.cs @@ -1,6 +1,4 @@ -using System.Threading; - -namespace NewLife.NovaDb.Core; +namespace NewLife.NovaDb.Core; /// 元数据读写锁,用于 DDL 与 DML/SELECT 的并发控制 /// diff --git a/NewLife.NovaDb/Core/NovaMetrics.cs b/NewLife.NovaDb/Core/NovaMetrics.cs index 54d1131..5185796 100644 --- a/NewLife.NovaDb/Core/NovaMetrics.cs +++ b/NewLife.NovaDb/Core/NovaMetrics.cs @@ -1,6 +1,4 @@ -using System.Threading; - -namespace NewLife.NovaDb.Core; +namespace NewLife.NovaDb.Core; /// NovaDb 运行时指标 /// 所有计数器均为线程安全,支持多线程并发递增 diff --git a/NewLife.NovaDb/Core/SlowQueryLog.cs b/NewLife.NovaDb/Core/SlowQueryLog.cs index ca1114a..d9c623d 100644 --- a/NewLife.NovaDb/Core/SlowQueryLog.cs +++ b/NewLife.NovaDb/Core/SlowQueryLog.cs @@ -1,5 +1,4 @@ -using System.Collections.Concurrent; -using NewLife.Log; +using NewLife.Log; namespace NewLife.NovaDb.Core; diff --git a/NewLife.NovaDb/Engine/ColdIndexDirectory.cs b/NewLife.NovaDb/Engine/ColdIndexDirectory.cs index d5b3271..ea2936a 100644 --- a/NewLife.NovaDb/Engine/ColdIndexDirectory.cs +++ b/NewLife.NovaDb/Engine/ColdIndexDirectory.cs @@ -1,6 +1,4 @@ -using System.Linq; - -namespace NewLife.NovaDb.Engine; +namespace NewLife.NovaDb.Engine; /// 冷段目录项(稀疏索引) public class ColdDirectoryEntry diff --git a/NewLife.NovaDb/Engine/Flux/FluxEngine.Persist.cs b/NewLife.NovaDb/Engine/Flux/FluxEngine.Persist.cs index 6fd7928..8ff51f6 100644 --- a/NewLife.NovaDb/Engine/Flux/FluxEngine.Persist.cs +++ b/NewLife.NovaDb/Engine/Flux/FluxEngine.Persist.cs @@ -1,6 +1,5 @@ using System.Buffers; using System.Buffers.Binary; -using System.Runtime.InteropServices.ComTypes; using System.Text; using NewLife.NovaDb.Utilities; using NewLife.Security; diff --git a/NewLife.NovaDb/Engine/NovaTable.Persist.cs b/NewLife.NovaDb/Engine/NovaTable.Persist.cs index 7a368d5..b7d35b4 100644 --- a/NewLife.NovaDb/Engine/NovaTable.Persist.cs +++ b/NewLife.NovaDb/Engine/NovaTable.Persist.cs @@ -1,5 +1,4 @@ -using System.Buffers.Binary; -using NewLife.NovaDb.Core; +using NewLife.NovaDb.Core; using NewLife.NovaDb.Utilities; using NewLife.Security; diff --git a/NewLife.NovaDb/Server/NovaController.cs b/NewLife.NovaDb/Server/NovaController.cs index cffc24a..e827bb7 100644 --- a/NewLife.NovaDb/Server/NovaController.cs +++ b/NewLife.NovaDb/Server/NovaController.cs @@ -1,7 +1,6 @@ using NewLife.NovaDb.Cluster; using NewLife.NovaDb.Sql; using NewLife.NovaDb.Tx; -using NewLife.NovaDb.WAL; using NewLife.Remoting; namespace NewLife.NovaDb.Server; diff --git a/NewLife.NovaDb/Sql/SqlEngine.DML.cs b/NewLife.NovaDb/Sql/SqlEngine.DML.cs index 0c7d189..45c78a3 100644 --- a/NewLife.NovaDb/Sql/SqlEngine.DML.cs +++ b/NewLife.NovaDb/Sql/SqlEngine.DML.cs @@ -1,6 +1,4 @@ -using NewLife.NovaDb.Core; -using NewLife.NovaDb.Engine; -using NewLife.NovaDb.Tx; +using NewLife.NovaDb.Core; namespace NewLife.NovaDb.Sql; diff --git a/NewLife.NovaDb/Sql/SqlEngine.Explain.cs b/NewLife.NovaDb/Sql/SqlEngine.Explain.cs index 0568375..5976621 100644 --- a/NewLife.NovaDb/Sql/SqlEngine.Explain.cs +++ b/NewLife.NovaDb/Sql/SqlEngine.Explain.cs @@ -1,6 +1,4 @@ -using NewLife.NovaDb.Core; - -namespace NewLife.NovaDb.Sql; +namespace NewLife.NovaDb.Sql; /// EXPLAIN 查询计划执行器 public partial class SqlEngine diff --git a/NewLife.NovaDb/Sql/SqlEngine.Select.cs b/NewLife.NovaDb/Sql/SqlEngine.Select.cs index 1ce9c4d..57dfad1 100644 --- a/NewLife.NovaDb/Sql/SqlEngine.Select.cs +++ b/NewLife.NovaDb/Sql/SqlEngine.Select.cs @@ -1,6 +1,5 @@ using NewLife.NovaDb.Core; using NewLife.NovaDb.Engine; -using NewLife.NovaDb.Tx; namespace NewLife.NovaDb.Sql; diff --git a/NewLife.NovaDb/Storage/DatabaseDirectory.cs b/NewLife.NovaDb/Storage/DatabaseDirectory.cs index bdfe8d8..4517be3 100644 --- a/NewLife.NovaDb/Storage/DatabaseDirectory.cs +++ b/NewLife.NovaDb/Storage/DatabaseDirectory.cs @@ -1,5 +1,4 @@ -using NewLife; -using NewLife.Data; +using NewLife.Data; using NewLife.NovaDb.Core; namespace NewLife.NovaDb.Storage; diff --git a/NewLife.NovaDb/Tx/TransactionManager.cs b/NewLife.NovaDb/Tx/TransactionManager.cs index debeb62..8a3b96e 100644 --- a/NewLife.NovaDb/Tx/TransactionManager.cs +++ b/NewLife.NovaDb/Tx/TransactionManager.cs @@ -1,6 +1,4 @@ -using System.Linq; - -namespace NewLife.NovaDb.Tx; +namespace NewLife.NovaDb.Tx; /// 事务管理器,负责分配事务 ID 和提交时间戳 public class TransactionManager diff --git a/NewLife.NovaDb/WAL/WalCheckpointer.cs b/NewLife.NovaDb/WAL/WalCheckpointer.cs index 212f62d..d2120cf 100644 --- a/NewLife.NovaDb/WAL/WalCheckpointer.cs +++ b/NewLife.NovaDb/WAL/WalCheckpointer.cs @@ -1,6 +1,4 @@ -using NewLife.NovaDb.Core; - -namespace NewLife.NovaDb.WAL; +namespace NewLife.NovaDb.WAL; /// WAL 检查点管理器 /// From 8614c66ef8db72bc960dbc595a7cbba5b96aa8a7 Mon Sep 17 00:00:00 2001 From: Allen Cai Date: Fri, 6 Mar 2026 11:05:59 +0800 Subject: [PATCH 14/15] =?UTF-8?q?=E5=AF=B9=20EncodingExtensions=E3=80=81Ha?= =?UTF-8?q?shHelper=E3=80=81PooledBufferWriter=E3=80=81PooledBytes=20?= =?UTF-8?q?=E5=9B=9B=E4=B8=AA=E5=B7=A5=E5=85=B7=E7=B1=BB=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E6=94=B9=E6=88=90=20file-scoped=20namespace=20=E4=BB=A5?= =?UTF-8?q?=E4=BF=9D=E6=8C=81=E7=BB=9F=E4=B8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Utilities/EncodingExtensions.cs | 49 ++- NewLife.NovaDb/Utilities/HashHelper.cs | 159 +++++----- .../Utilities/PooledBufferWriter.cs | 281 +++++++++--------- NewLife.NovaDb/Utilities/PooledBytes.cs | 95 +++--- 4 files changed, 290 insertions(+), 294 deletions(-) diff --git a/NewLife.NovaDb/Utilities/EncodingExtensions.cs b/NewLife.NovaDb/Utilities/EncodingExtensions.cs index 43995d5..1e71de7 100644 --- a/NewLife.NovaDb/Utilities/EncodingExtensions.cs +++ b/NewLife.NovaDb/Utilities/EncodingExtensions.cs @@ -1,34 +1,33 @@ using System.Buffers; using System.Text; -namespace NewLife.NovaDb.Utilities +namespace NewLife.NovaDb.Utilities; + +/// +/// 提供字符串与 UTF-8 编码字节数组之间的转换扩展方法,使用对象池管理字节数组以提高性能。 +/// +internal static class EncodingExtensions { + private static readonly Encoding Encoding = Encoding.UTF8; + /// - /// 提供字符串与 UTF-8 编码字节数组之间的转换扩展方法,使用对象池管理字节数组以提高性能。 + /// 将字符串转换为使用对象池管理的 UTF-8 编码字节数组。 /// - internal static class EncodingExtensions - { - private static readonly Encoding Encoding = Encoding.UTF8; - - /// - /// 将字符串转换为使用对象池管理的 UTF-8 编码字节数组。 - /// - /// 要转换的字符串。 - /// 返回一个 实例,包含 UTF-8 编码的字节数组。 - public static PooledBytes ToPooledUtf8Bytes(this String value) => Encoding.GetPooledEncodedBytes(value); + /// 要转换的字符串。 + /// 返回一个 实例,包含 UTF-8 编码的字节数组。 + public static PooledBytes ToPooledUtf8Bytes(this String value) => Encoding.GetPooledEncodedBytes(value); - /// - /// 将字符串转换为使用对象池管理的指定编码的字节数组。 - /// - /// 要使用的编码。 - /// 要转换的字符串。 - /// 返回一个 实例,包含指定编码的字节数组。 - public static PooledBytes GetPooledEncodedBytes(this Encoding encoding, String value) - { - var length = encoding.GetByteCount(value); - var pooledBytes = ArrayPool.Shared.Rent(length); - encoding.GetBytes(value, 0, value.Length, pooledBytes, 0); - return new PooledBytes(pooledBytes, length); - } + /// + /// 将字符串转换为使用对象池管理的指定编码的字节数组。 + /// + /// 要使用的编码。 + /// 要转换的字符串。 + /// 返回一个 实例,包含指定编码的字节数组。 + public static PooledBytes GetPooledEncodedBytes(this Encoding encoding, String value) + { + var length = encoding.GetByteCount(value); + var pooledBytes = ArrayPool.Shared.Rent(length); + encoding.GetBytes(value, 0, value.Length, pooledBytes, 0); + return new PooledBytes(pooledBytes, length); } } \ No newline at end of file diff --git a/NewLife.NovaDb/Utilities/HashHelper.cs b/NewLife.NovaDb/Utilities/HashHelper.cs index 5539bb7..ff22013 100644 --- a/NewLife.NovaDb/Utilities/HashHelper.cs +++ b/NewLife.NovaDb/Utilities/HashHelper.cs @@ -1,55 +1,55 @@ using System.Runtime.CompilerServices; using System.Security.Cryptography; -namespace NewLife.NovaDb.Utilities +namespace NewLife.NovaDb.Utilities; + +/// +/// 哈希计算辅助类,提供 MD5、SHA1、SHA256、SHA384、SHA512 等常用哈希算法的计算方法 +/// +internal static class HashHelper { - /// - /// 哈希计算辅助类,提供 MD5、SHA1、SHA256、SHA384、SHA512 等常用哈希算法的计算方法 - /// - internal static class HashHelper - { - private const String Hex = "0123456789abcdef"; + private const String Hex = "0123456789abcdef"; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static String Md5ToHex(String str) => ComputeHash(str, HashAlgorithmName.MD5, 16); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static String Md5ToHex(String str) => ComputeHash(str, HashAlgorithmName.MD5, 16); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static String Sha1ToHex(String str) => ComputeHash(str, HashAlgorithmName.SHA1, 20); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static String Sha1ToHex(String str) => ComputeHash(str, HashAlgorithmName.SHA1, 20); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static String Sha256ToHex(String str) => ComputeHash(str, HashAlgorithmName.SHA256, 32); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static String Sha256ToHex(String str) => ComputeHash(str, HashAlgorithmName.SHA256, 32); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static String Sha384ToHex(String str) => ComputeHash(str, HashAlgorithmName.SHA384, 48); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static String Sha384ToHex(String str) => ComputeHash(str, HashAlgorithmName.SHA384, 48); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static String Sha512ToHex(String str) => ComputeHash(str, HashAlgorithmName.SHA512, 64); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static String Sha512ToHex(String str) => ComputeHash(str, HashAlgorithmName.SHA512, 64); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static String Md5ToBase64(String str) => ComputeHash(str, HashAlgorithmName.MD5, 16, false); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static String Md5ToBase64(String str) => ComputeHash(str, HashAlgorithmName.MD5, 16, false); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static String Sha1ToBase64(String str) => ComputeHash(str, HashAlgorithmName.SHA1, 20, false); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static String Sha1ToBase64(String str) => ComputeHash(str, HashAlgorithmName.SHA1, 20, false); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static String Sha256ToBase64(String str) => ComputeHash(str, HashAlgorithmName.SHA256, 32, false); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static String Sha256ToBase64(String str) => ComputeHash(str, HashAlgorithmName.SHA256, 32, false); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static String Sha384ToBase64(String str) => ComputeHash(str, HashAlgorithmName.SHA384, 48, false); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static String Sha384ToBase64(String str) => ComputeHash(str, HashAlgorithmName.SHA384, 48, false); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static String Sha512ToBase64(String str) => ComputeHash(str, HashAlgorithmName.SHA512, 64, false); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static String Sha512ToBase64(String str) => ComputeHash(str, HashAlgorithmName.SHA512, 64, false); - private static String ComputeHash(String str, HashAlgorithmName alg, Int32 hashSize, Boolean hex = true) - { - using var bytes = str.ToPooledUtf8Bytes(); + private static String ComputeHash(String str, HashAlgorithmName alg, Int32 hashSize, Boolean hex = true) + { + using var bytes = str.ToPooledUtf8Bytes(); #if NET5_0_OR_GREATER - Span hash = stackalloc Byte[hashSize]; + Span hash = stackalloc Byte[hashSize]; - if (!TryHashData(alg, bytes.AsSpan(), hash)) - throw new CryptographicException(); + if (!TryHashData(alg, bytes.AsSpan(), hash)) + throw new CryptographicException(); - return hex ? ToLowerHex(hash) : Convert.ToBase64String(hash); + return hex ? ToLowerHex(hash) : Convert.ToBase64String(hash); #elif NETSTANDARD2_1_OR_GREATER using var algo = CreateAlgorithm(alg); @@ -64,68 +64,67 @@ private static String ComputeHash(String str, HashAlgorithmName alg, Int32 hashS var hash = algo.ComputeHash(bytes.Buffer, 0, bytes.Length); return hex ? ToLowerHex(hash) : Convert.ToBase64String(hash); #endif - } + } #if NET5_0_OR_GREATER - private static Boolean TryHashData(HashAlgorithmName alg, ReadOnlySpan data, Span dest) - { - if (alg == HashAlgorithmName.MD5) - return MD5.TryHashData(data, dest, out _); + private static Boolean TryHashData(HashAlgorithmName alg, ReadOnlySpan data, Span dest) + { + if (alg == HashAlgorithmName.MD5) + return MD5.TryHashData(data, dest, out _); - if (alg == HashAlgorithmName.SHA1) - return SHA1.TryHashData(data, dest, out _); + if (alg == HashAlgorithmName.SHA1) + return SHA1.TryHashData(data, dest, out _); - if (alg == HashAlgorithmName.SHA256) - return SHA256.TryHashData(data, dest, out _); + if (alg == HashAlgorithmName.SHA256) + return SHA256.TryHashData(data, dest, out _); - if (alg == HashAlgorithmName.SHA384) - return SHA384.TryHashData(data, dest, out _); + if (alg == HashAlgorithmName.SHA384) + return SHA384.TryHashData(data, dest, out _); - if (alg == HashAlgorithmName.SHA512) - return SHA512.TryHashData(data, dest, out _); + if (alg == HashAlgorithmName.SHA512) + return SHA512.TryHashData(data, dest, out _); - throw new NotSupportedException(); - } + throw new NotSupportedException(); + } #endif - private static HashAlgorithm CreateAlgorithm(HashAlgorithmName alg) - { - if (alg == HashAlgorithmName.MD5) return MD5.Create(); - if (alg == HashAlgorithmName.SHA1) return SHA1.Create(); - if (alg == HashAlgorithmName.SHA256) return SHA256.Create(); - if (alg == HashAlgorithmName.SHA384) return SHA384.Create(); - if (alg == HashAlgorithmName.SHA512) return SHA512.Create(); + private static HashAlgorithm CreateAlgorithm(HashAlgorithmName alg) + { + if (alg == HashAlgorithmName.MD5) return MD5.Create(); + if (alg == HashAlgorithmName.SHA1) return SHA1.Create(); + if (alg == HashAlgorithmName.SHA256) return SHA256.Create(); + if (alg == HashAlgorithmName.SHA384) return SHA384.Create(); + if (alg == HashAlgorithmName.SHA512) return SHA512.Create(); - throw new NotSupportedException(); - } + throw new NotSupportedException(); + } - private static String ToLowerHex(ReadOnlySpan bytes) - { + private static String ToLowerHex(ReadOnlySpan bytes) + { #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER - Span chars = stackalloc Char[bytes.Length * 2]; + Span chars = stackalloc Char[bytes.Length * 2]; #else var chars = new Char[bytes.Length * 2]; #endif - var j = 0; - - foreach (var b in bytes) - { - chars[j++] = Hex[b >> 4]; - chars[j++] = Hex[b & 0xF]; - } + var j = 0; - return new String(chars); - } - - private enum HashAlgorithmName + foreach (var b in bytes) { - // ReSharper disable InconsistentNaming - MD5, - SHA1, - SHA256, - SHA384, - SHA512 - // ReSharper restore InconsistentNaming + chars[j++] = Hex[b >> 4]; + chars[j++] = Hex[b & 0xF]; } + + return new String(chars); + } + + private enum HashAlgorithmName + { + // ReSharper disable InconsistentNaming + MD5, + SHA1, + SHA256, + SHA384, + SHA512 + // ReSharper restore InconsistentNaming } -} +} \ No newline at end of file diff --git a/NewLife.NovaDb/Utilities/PooledBufferWriter.cs b/NewLife.NovaDb/Utilities/PooledBufferWriter.cs index 1c1be10..d6f26b9 100644 --- a/NewLife.NovaDb/Utilities/PooledBufferWriter.cs +++ b/NewLife.NovaDb/Utilities/PooledBufferWriter.cs @@ -2,155 +2,154 @@ using System.Buffers.Binary; using System.Runtime.CompilerServices; -namespace NewLife.NovaDb.Utilities +namespace NewLife.NovaDb.Utilities; + +/// +/// 池化的字节数组写入器,提供高效的写入方法。
+/// 使用对象池管理字节数组,避免频繁分配和垃圾回收。
+/// 使用完毕后应调用 方法归还数组到 对象池。 +///
+internal struct PooledBufferWriter : IDisposable { - /// - /// 池化的字节数组写入器,提供高效的写入方法。
- /// 使用对象池管理字节数组,避免频繁分配和垃圾回收。
- /// 使用完毕后应调用 方法归还数组到 对象池。 - ///
- internal struct PooledBufferWriter : IDisposable - { #if NET45 private static readonly Byte[] EmptyBytes = new Byte[0]; #else - private static readonly Byte[] EmptyBytes = Array.Empty(); + private static readonly Byte[] EmptyBytes = Array.Empty(); #endif - private Byte[] _buffer; - private Int32 _pos; - - public PooledBufferWriter() - { - _buffer = ArrayPool.Shared.Rent(256); - _pos = 0; - } - - public PooledBufferWriter(Int32 initialCapacity) - { - _buffer = ArrayPool.Shared.Rent(initialCapacity); - _pos = 0; - } - - public readonly Int32 WrittenCount => _pos; - public readonly Byte[] Buffer => _buffer; - - public void Dispose() - { - var buf = _buffer; - _buffer = EmptyBytes; - _pos = 0; - if (buf.Length != 0) - ArrayPool.Shared.Return(buf); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void Ensure(Int32 sizeHint) - { - if ((UInt32)(_pos + sizeHint) <= (UInt32)_buffer.Length) return; - - var newSize = _buffer.Length * 2; - var needed = _pos + sizeHint; - if (newSize < needed) newSize = needed; - - var newBuf = ArrayPool.Shared.Rent(newSize); - System.Buffer.BlockCopy(_buffer, 0, newBuf, 0, _pos); - ArrayPool.Shared.Return(_buffer); - _buffer = newBuf; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void WriteByte(Byte value) - { - Ensure(1); - _buffer[_pos++] = value; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void WriteInt32(Int32 value) - { - Ensure(4); - BinaryPrimitives.WriteInt32LittleEndian(_buffer.AsSpan(_pos, 4), value); - _pos += 4; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void WriteUInt32(UInt32 value) - { - Ensure(4); - BinaryPrimitives.WriteUInt32LittleEndian(_buffer.AsSpan(_pos, 4), value); - _pos += 4; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void WriteInt64(Int64 value) - { - Ensure(8); - BinaryPrimitives.WriteInt64LittleEndian(_buffer.AsSpan(_pos, 8), value); - _pos += 8; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void WriteUInt64(UInt64 value) - { - Ensure(8); - BinaryPrimitives.WriteUInt64LittleEndian(_buffer.AsSpan(_pos, 8), value); - _pos += 8; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void WriteDouble(Double value) - { - // 兼容低于 .NET 6:用 bits + WriteInt64 + private Byte[] _buffer; + private Int32 _pos; + + public PooledBufferWriter() + { + _buffer = ArrayPool.Shared.Rent(256); + _pos = 0; + } + + public PooledBufferWriter(Int32 initialCapacity) + { + _buffer = ArrayPool.Shared.Rent(initialCapacity); + _pos = 0; + } + + public readonly Int32 WrittenCount => _pos; + public readonly Byte[] Buffer => _buffer; + + public void Dispose() + { + var buf = _buffer; + _buffer = EmptyBytes; + _pos = 0; + if (buf.Length != 0) + ArrayPool.Shared.Return(buf); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Ensure(Int32 sizeHint) + { + if ((UInt32)(_pos + sizeHint) <= (UInt32)_buffer.Length) return; + + var newSize = _buffer.Length * 2; + var needed = _pos + sizeHint; + if (newSize < needed) newSize = needed; + + var newBuf = ArrayPool.Shared.Rent(newSize); + System.Buffer.BlockCopy(_buffer, 0, newBuf, 0, _pos); + ArrayPool.Shared.Return(_buffer); + _buffer = newBuf; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteByte(Byte value) + { + Ensure(1); + _buffer[_pos++] = value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteInt32(Int32 value) + { + Ensure(4); + BinaryPrimitives.WriteInt32LittleEndian(_buffer.AsSpan(_pos, 4), value); + _pos += 4; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteUInt32(UInt32 value) + { + Ensure(4); + BinaryPrimitives.WriteUInt32LittleEndian(_buffer.AsSpan(_pos, 4), value); + _pos += 4; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteInt64(Int64 value) + { + Ensure(8); + BinaryPrimitives.WriteInt64LittleEndian(_buffer.AsSpan(_pos, 8), value); + _pos += 8; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteUInt64(UInt64 value) + { + Ensure(8); + BinaryPrimitives.WriteUInt64LittleEndian(_buffer.AsSpan(_pos, 8), value); + _pos += 8; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteDouble(Double value) + { + // 兼容低于 .NET 6:用 bits + WriteInt64 #if NET6_0_OR_GREATER - Ensure(8); - BinaryPrimitives.WriteDoubleLittleEndian(_buffer.AsSpan(_pos, 8), value); - _pos += 8; + Ensure(8); + BinaryPrimitives.WriteDoubleLittleEndian(_buffer.AsSpan(_pos, 8), value); + _pos += 8; #else WriteInt64(BitConverter.DoubleToInt64Bits(value)); #endif - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void WriteBool(Boolean value) => WriteByte(value ? (Byte)1 : (Byte)0); - - public void WriteBytes(ReadOnlySpan src) - { - Ensure(src.Length); - src.CopyTo(_buffer.AsSpan(_pos, src.Length)); - _pos += src.Length; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void WriteBytes(Byte[] src) => WriteBytes(src.AsSpan()); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void WriteBytes(Byte[] src, Int32 offset, Int32 count) => WriteBytes(src.AsSpan(offset, count)); - - /// - /// 获取一个可写的字节数组切片,长度由参数指定。 - /// - /// 要获取的字节数组切片的长度 - /// 返回一个可写的字节数组切片 - public Span GetWritableSpan(Int32 length) - { - Ensure(length); - var span = _buffer.AsSpan(_pos, length); - _pos += length; - return span; - } - - /// - /// 获取一个可写的字节数组切片,长度由参数指定,但返回值类型为 ,以兼容某些需要 ArraySegment 的 API。 - /// - /// 要获取的字节数组切片的长度 - /// 返回一个可写的字节数组切片 - public ArraySegment GetWritableSegment(Int32 length) - { - Ensure(length); - var segment = new ArraySegment(_buffer, _pos, length); - _pos += length; - return segment; - } } -} + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteBool(Boolean value) => WriteByte(value ? (Byte)1 : (Byte)0); + + public void WriteBytes(ReadOnlySpan src) + { + Ensure(src.Length); + src.CopyTo(_buffer.AsSpan(_pos, src.Length)); + _pos += src.Length; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteBytes(Byte[] src) => WriteBytes(src.AsSpan()); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteBytes(Byte[] src, Int32 offset, Int32 count) => WriteBytes(src.AsSpan(offset, count)); + + /// + /// 获取一个可写的字节数组切片,长度由参数指定。 + /// + /// 要获取的字节数组切片的长度 + /// 返回一个可写的字节数组切片 + public Span GetWritableSpan(Int32 length) + { + Ensure(length); + var span = _buffer.AsSpan(_pos, length); + _pos += length; + return span; + } + + /// + /// 获取一个可写的字节数组切片,长度由参数指定,但返回值类型为 ,以兼容某些需要 ArraySegment 的 API。 + /// + /// 要获取的字节数组切片的长度 + /// 返回一个可写的字节数组切片 + public ArraySegment GetWritableSegment(Int32 length) + { + Ensure(length); + var segment = new ArraySegment(_buffer, _pos, length); + _pos += length; + return segment; + } +} \ No newline at end of file diff --git a/NewLife.NovaDb/Utilities/PooledBytes.cs b/NewLife.NovaDb/Utilities/PooledBytes.cs index ac990e5..f08181f 100644 --- a/NewLife.NovaDb/Utilities/PooledBytes.cs +++ b/NewLife.NovaDb/Utilities/PooledBytes.cs @@ -1,58 +1,57 @@ using System.Buffers; using System.Runtime.CompilerServices; -namespace NewLife.NovaDb.Utilities +namespace NewLife.NovaDb.Utilities; + +/// +/// 使用对象池管理字节数组,避免频繁分配和垃圾回收。 +/// +internal struct PooledBytes : IDisposable { - /// - /// 使用对象池管理字节数组,避免频繁分配和垃圾回收。 - /// - internal struct PooledBytes : IDisposable - { #if NET45 private static readonly Byte[] EmptyBytes = new Byte[0]; #else - private static readonly Byte[] EmptyBytes = Array.Empty(); + private static readonly Byte[] EmptyBytes = Array.Empty(); #endif - public static readonly PooledBytes Empty = new(); - - /// - /// 字节数组的有效数据长度。
- /// 字节数组的长度可能大于此值,因为它是从 对象池租用的。 - ///
- public Int32 Length { get; private set; } - - /// - /// 获取字节数组。
- /// 注意:有效数据的长度由 属性决定。
- /// 使用完毕后应调用 方法归还数组到对象池。 - ///
- public Byte[] Buffer { get; private set; } - - public PooledBytes() - { - Length = 0; - Buffer = EmptyBytes; - } - - internal PooledBytes(Byte[] pooledBytes, Int32 length) - { - Length = length; - Buffer = pooledBytes; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ReadOnlySpan AsSpan() => Length == 0 ? ReadOnlySpan.Empty : Buffer.AsSpan(0, Length); - - public void Dispose() - { - if (Buffer == null || Buffer.Length == 0) return; - ArrayPool.Shared.Return(Buffer); - Buffer = EmptyBytes; - Length = 0; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static implicit operator ReadOnlySpan(PooledBytes pooled) => pooled.AsSpan(); + public static readonly PooledBytes Empty = new(); + + /// + /// 字节数组的有效数据长度。
+ /// 字节数组的长度可能大于此值,因为它是从 对象池租用的。 + ///
+ public Int32 Length { get; private set; } + + /// + /// 获取字节数组。
+ /// 注意:有效数据的长度由 属性决定。
+ /// 使用完毕后应调用 方法归还数组到对象池。 + ///
+ public Byte[] Buffer { get; private set; } + + public PooledBytes() + { + Length = 0; + Buffer = EmptyBytes; + } + + internal PooledBytes(Byte[] pooledBytes, Int32 length) + { + Length = length; + Buffer = pooledBytes; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ReadOnlySpan AsSpan() => Length == 0 ? ReadOnlySpan.Empty : Buffer.AsSpan(0, Length); + + public void Dispose() + { + if (Buffer == null || Buffer.Length == 0) return; + ArrayPool.Shared.Return(Buffer); + Buffer = EmptyBytes; + Length = 0; } -} + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator ReadOnlySpan(PooledBytes pooled) => pooled.AsSpan(); +} \ No newline at end of file From 5475185bf8d022ad794260aedd75e00cde4143d1 Mon Sep 17 00:00:00 2001 From: Allen Cai Date: Fri, 6 Mar 2026 11:12:21 +0800 Subject: [PATCH 15/15] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E4=BA=86=20valueBytesL?= =?UTF-8?q?engthTotal=20=E8=AE=A1=E7=AE=97=E4=B8=AD=E7=9A=84=E6=8B=AC?= =?UTF-8?q?=E5=8F=B7=E4=BC=98=E5=85=88=E7=BA=A7=EF=BC=8C=E7=A1=AE=E4=BF=9D?= =?UTF-8?q?=E5=BD=93=E5=80=BC=E4=B8=BA=20null=20=E6=97=B6=E8=83=BD?= =?UTF-8?q?=E6=AD=A3=E7=A1=AE=E8=BF=94=E5=9B=9E=200=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- NewLife.NovaDb/Engine/Flux/FluxEngine.Persist.cs | 2 +- NewLife.NovaDb/Server/KvPacket.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/NewLife.NovaDb/Engine/Flux/FluxEngine.Persist.cs b/NewLife.NovaDb/Engine/Flux/FluxEngine.Persist.cs index 8ff51f6..a9cd0d8 100644 --- a/NewLife.NovaDb/Engine/Flux/FluxEngine.Persist.cs +++ b/NewLife.NovaDb/Engine/Flux/FluxEngine.Persist.cs @@ -207,7 +207,7 @@ private void WriteFluxRecord(Byte recordType, Byte[] data, Int32 offset, Int32 c } finally { - ArrayPool.Shared.Return(tempBuffer); + ArrayPool.Shared.Return(tempBuffer); } } #endif diff --git a/NewLife.NovaDb/Server/KvPacket.cs b/NewLife.NovaDb/Server/KvPacket.cs index c5999da..62b6308 100644 --- a/NewLife.NovaDb/Server/KvPacket.cs +++ b/NewLife.NovaDb/Server/KvPacket.cs @@ -207,7 +207,7 @@ public static IPacket EncodeGetAll(String tableName, String[] keys) public static IPacket EncodeSetAll(String tableName, IDictionary values, Int32 ttlSeconds) { using var tableBytes = _encoding.GetPooledEncodedBytes(tableName ?? "default"); - var valueBytesLengthTotal = values.Sum(kvp => 8 + _encoding.GetByteCount(kvp.Key) + kvp.Value?.Length ?? 0); + var valueBytesLengthTotal = values.Sum(kvp => 8 + _encoding.GetByteCount(kvp.Key) + (kvp.Value?.Length ?? 0)); var bufSize = 32 + tableBytes.Length + valueBytesLengthTotal; var buf = new Byte[bufSize]; var writer = new SpanWriter(buf, 0, bufSize); @@ -540,7 +540,7 @@ private static String ReadString(ref SpanReader reader) } #if NET45 - private static readonly byte[] EmptyBytes = new byte[0]; + private static readonly Byte[] EmptyBytes = new Byte[0]; #else private static readonly Byte[] EmptyBytes = Array.Empty(); #endif