* 'master' of https://github.com/NewLifeX/NewLife.Redis:
  [feat] 新增LPOP,支持Redis7下的列表索引查找,Redis7-版本通过多次LRange遍历实现。close: https://github.com/NewLifeX/NewLife.Redis/issues/137
  Redis7支持LPOS
  添加 RemoveTest 测试方法并更新 EventInfo 类
  精确控制内存释放
  [fix] 外部调用者可能需要直接使用内部申请的OwnerPacket,所以这里不释放
This commit is contained in:
猿人易 2025-03-28 17:37:02 +08:00
commit 54933dc971
10 changed files with 160 additions and 37 deletions

View File

@ -1,5 +1,4 @@
using System.Net.Sockets;
using NewLife.Log;
using NewLife.Log;
using NewLife.Net;
using NewLife.Threading;
@ -13,7 +12,7 @@ public class RedisReplication : RedisBase, IRedisCluster, IDisposable
IList<IRedisNode> IRedisCluster.Nodes => Nodes.Select(x => (IRedisNode)x).ToList();
/// <summary>节点改变事件</summary>
public event EventHandler NodeChanged;
public event EventHandler? NodeChanged;
/// <summary>集群节点</summary>
public RedisNode[]? Nodes { get; protected set; }

View File

@ -112,7 +112,13 @@ public class RedisQueue<T> : QueueBase, IProducerConsumer<T>
if (timeout > 0 && Redis.Timeout < (timeout + 1) * 1000) Redis.Timeout = (timeout + 1) * 1000;
var rs = Execute((rc, k) => rc.Execute<IPacket[]>("BRPOP", Key, timeout), true);
return rs == null || rs.Length < 2 ? default : (T?)Redis.Encoder.Decode(rs[1], typeof(T));
if (rs == null || rs.Length < 2) return default;
var msg = (T?)Redis.Encoder.Decode(rs[1], typeof(T));
if (typeof(T) != typeof(IPacket)) rs.TryDispose();
return msg;
}
/// <summary>异步消费获取</summary>
@ -126,7 +132,13 @@ public class RedisQueue<T> : QueueBase, IProducerConsumer<T>
if (timeout > 0 && Redis.Timeout < (timeout + 1) * 1000) Redis.Timeout = (timeout + 1) * 1000;
var rs = await ExecuteAsync((rc, k) => rc.ExecuteAsync<IPacket[]>("BRPOP", [Key, timeout], cancellationToken), true).ConfigureAwait(false);
return rs == null || rs.Length < 2 ? default : (T?)Redis.Encoder.Decode(rs[1], typeof(T));
if (rs == null || rs.Length < 2) return default;
var msg = (T?)Redis.Encoder.Decode(rs[1], typeof(T));
if (typeof(T) != typeof(IPacket)) rs.TryDispose();
return msg;
}
/// <summary>异步消费获取</summary>

View File

@ -442,6 +442,7 @@ public class RedisReliableQueue<T> : QueueBase, IProducerConsumer<T>, IDisposabl
// 清理已经失去Status的Ack
foreach (var key in rds.Search($"{_Key}:Ack:*", 1000))
{
if (!acks.Contains(key))
{
var queue = rds.GetList<String>(key) as RedisList<String>;
@ -449,6 +450,7 @@ public class RedisReliableQueue<T> : QueueBase, IProducerConsumer<T>, IDisposabl
XTrace.WriteLine("全局清理死信:{0} {1}", key, msgs.ToJson());
rds.Remove(key);
}
}
return count;
}

View File

@ -1,6 +1,5 @@
using System.Buffers;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Authentication;
@ -679,19 +678,22 @@ public class RedisClient : DisposeBase
try
{
// 管道模式
var type = typeof(TResult);
if (_ps != null)
{
_ps.Add(new Command(cmd, args, typeof(TResult)));
_ps.Add(new Command(cmd, args, type));
return default;
}
var rs = ExecuteCommand(cmd, args);
if (rs == null) return default;
if (rs is TResult rs2) return rs2;
if (TryChangeType(rs, typeof(TResult), out var target))
if (TryChangeType(rs, type, out var target))
{
//!!! 外部调用者可能需要直接使用内部申请的OwnerPacket所以这里不释放
// 释放内部申请的OwnerPacket
rs.TryDispose();
if (type != typeof(IPacket) && type != typeof(IPacket[])) rs.TryDispose();
return (TResult?)target;
}
@ -725,10 +727,13 @@ public class RedisClient : DisposeBase
value = default;
if (rs == null) return false;
if (TryChangeType(rs, typeof(TResult), out var target))
var type = typeof(TResult);
if (TryChangeType(rs, type, out var target))
{
//!!! 外部调用者可能需要直接使用内部申请的OwnerPacket所以这里不释放
// 释放内部申请的OwnerPacket
rs.TryDispose();
if (type != typeof(IPacket) && type != typeof(IPacket[])) rs.TryDispose();
value = (TResult?)target;
return true;
}
@ -777,19 +782,22 @@ public class RedisClient : DisposeBase
public virtual async Task<TResult?> ExecuteAsync<TResult>(String cmd, Object?[] args, CancellationToken cancellationToken)
{
// 管道模式
var type = typeof(TResult);
if (_ps != null)
{
_ps.Add(new Command(cmd, args, typeof(TResult)));
_ps.Add(new Command(cmd, args, type));
return default;
}
var rs = await ExecuteAsync(cmd, args, cancellationToken).ConfigureAwait(false);
if (rs == null) return default;
if (rs is TResult rs2) return rs2;
if (TryChangeType(rs, typeof(TResult), out var target))
if (TryChangeType(rs, type, out var target))
{
//!!! 外部调用者可能需要直接使用内部申请的OwnerPacket所以这里不释放
// 释放内部申请的OwnerPacket
rs.TryDispose();
if (type != typeof(IPacket) && type != typeof(IPacket[])) rs.TryDispose();
return (TResult?)target;
}
@ -810,7 +818,14 @@ public class RedisClient : DisposeBase
//var rs = ExecuteCommand(null, null, null);
if (rs == null) return default;
if (rs is TResult rs2) return rs2;
if (TryChangeType(rs, typeof(TResult), out var target)) return (TResult?)target;
var type = typeof(TResult);
if (TryChangeType(rs, type, out var target))
{
// 释放内部申请的OwnerPacket
if (type != typeof(IPacket) && type != typeof(IPacket[])) rs.TryDispose();
return (TResult?)target;
}
return default;
}
@ -939,8 +954,9 @@ public class RedisClient : DisposeBase
var rs = list[i];
if (rs != null && TryChangeType(rs, ps[i].Type, out var target) && target != null)
{
//!!! 外部调用者可能需要直接使用内部申请的OwnerPacket所以这里不释放
// 释放内部申请的OwnerPacket
rs.TryDispose();
if (ps[i].Type != typeof(IPacket) && ps[i].Type != typeof(IPacket[])) rs.TryDispose();
list[i] = target;
}
}

View File

@ -68,6 +68,8 @@ public class RedisHash<TKey, TValue> : RedisBase, IDictionary<TKey, TValue>
value = Redis.Encoder.Decode<TValue>(pk)!;
//value = (TValue?)Redis.Encoder.Decode(pk, typeof(TValue))!;
if (typeof(TValue) != typeof(IPacket)) pk.TryDispose();
return true;
}
@ -164,6 +166,8 @@ public class RedisHash<TKey, TValue> : RedisBase, IDictionary<TKey, TValue>
dic[key] = value;
}
if (typeof(TKey) != typeof(IPacket) && typeof(TValue) != typeof(IPacket)) rs.TryDispose();
return dic;
}

View File

@ -47,14 +47,7 @@ public class RedisList<T> : RedisBase, IList<T>
/// <summary>是否包含指定元素</summary>
/// <param name="item"></param>
/// <returns></returns>
public Boolean Contains(T item)
{
var count = Count;
if (count > 1000) throw new NotSupportedException($"[{Key}]的元素个数过多,不支持!");
var list = GetAll();
return list.Contains(item);
}
public Boolean Contains(T item) => IndexOf(item) >= 0;
/// <summary>复制到目标数组</summary>
/// <param name="array"></param>
@ -72,11 +65,31 @@ public class RedisList<T> : RedisBase, IList<T>
/// <returns></returns>
public Int32 IndexOf(T item)
{
var count = Count;
if (count > 1000) throw new NotSupportedException($"[{Key}]的元素个数过多,不支持!");
// Redis7支持LPOS
if (Redis.Version.Major >= 7) return LPOS(item);
var arr = GetAll();
return Array.IndexOf(arr, item);
var p = 0;
var batch = 100;
while (true)
{
var arr = LRange(p, p + batch - 1);
if (arr == null || arr.Length == 0) break;
var idx = Array.IndexOf(arr, item);
if (idx >= 0) return p + idx;
if (p >= 1_000_000) throw new NotSupportedException($"[{Key}]的元素个数过多,不支持遍历!");
p += batch;
}
return -1;
//var count = Count;
//if (count > 1000)
//var arr = GetAll();
//return Array.IndexOf(arr, item);
}
/// <summary>在指定位置插入</summary>
@ -217,5 +230,9 @@ public class RedisList<T> : RedisBase, IList<T>
/// <param name="value"></param>
/// <returns></returns>
public Int32 LRem(Int32 count, T value) => Execute((r, k) => r.Execute<Int32>("LREM", Key, count, value), true);
/// <summary>获取元素位置</summary>
/// <returns></returns>
public Int32 LPOS(T item) => Execute((rc, k) => rc.Execute<Int32>("LPOS", Key, item), false);
#endregion
}

View File

@ -83,7 +83,13 @@ public class RedisStack<T> : RedisBase, IProducerConsumer<T>
if (timeout < 0) return Execute((rc, k) => rc.Execute<T>("RPOP", Key), true);
var rs = Execute((rc, k) => rc.Execute<IPacket[]>("BRPOP", Key, timeout), true);
return rs == null || rs.Length < 2 ? default : (T?)Redis.Encoder.Decode(rs[1], typeof(T));
if (rs == null || rs.Length < 2) return default;
var msg = (T?)Redis.Encoder.Decode(rs[1], typeof(T));
if (typeof(T) != typeof(IPacket)) rs.TryDispose();
return msg;
}
/// <summary>异步消费获取</summary>
@ -95,7 +101,13 @@ public class RedisStack<T> : RedisBase, IProducerConsumer<T>
if (timeout < 0) return await ExecuteAsync((rc, k) => rc.ExecuteAsync<T>("RPOP", Key), true).ConfigureAwait(false);
var rs = await ExecuteAsync((rc, k) => rc.ExecuteAsync<IPacket[]>("BRPOP", [Key, timeout], cancellationToken), true).ConfigureAwait(false);
return rs == null || rs.Length < 2 ? default : (T?)Redis.Encoder.Decode(rs[1], typeof(T));
if (rs == null || rs.Length < 2) return default;
var msg = (T?)Redis.Encoder.Decode(rs[1], typeof(T));
if (typeof(T) != typeof(IPacket)) rs.TryDispose();
return msg;
}
/// <summary>异步消费获取</summary>

View File

@ -90,7 +90,7 @@ public class RedisStat : DisposeBase
if (!_redis.Rename(key, newKey, false)) return;
_redis.Remove($"exists:{key}");
var rs = _redis.Execute(newKey, (r,k) => r.Execute<IPacket[]>("HGETALL", k));
var rs = _redis.Execute(newKey, (r, k) => r.Execute<IPacket[]>("HGETALL", k));
if (rs != null)
{
var dic = new Dictionary<String, Int32>();
@ -104,6 +104,8 @@ public class RedisStat : DisposeBase
OnSave(key, dic);
}
rs.TryDispose();
_redis.Remove(newKey);
}
#endregion

View File

@ -1,9 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.Intrinsics.Arm;
using NewLife.Caching;
using NewLife.Log;
using Xunit;
@ -105,14 +102,43 @@ public class HashTest
var hash = _redis.GetDictionary<EventInfo>(key);
Assert.NotNull(hash);
var l = hash as RedisHash<String, String>;
var rh = hash as RedisHash<String, EventInfo>;
foreach(var item in l.GetAll())
foreach (var item in rh.GetAll())
{
XTrace.WriteLine(item.Key);
}
l["0"] = "0";
rh["0"] = new EventInfo { EventId = "1234", EventName = "Stone" };
}
[Fact]
public void RemoveTest()
{
var key = $"NewLife:eventinfo:adsfasdfasdfdsaf";
var hash = _redis.GetDictionary<EventInfo>(key);
Assert.NotNull(hash);
var rh = hash as RedisHash<String, EventInfo>;
foreach (var item in rh.GetAll())
{
XTrace.WriteLine(item.Key);
}
rh["0"] = new EventInfo { EventId = "1234", EventName = "Stone" };
rh["1"] = new EventInfo { EventId = "12345", EventName = "Stone" };
rh["2"] = new EventInfo { EventId = "123456", EventName = "Stone" };
rh.Remove("0");
Assert.Equal(2, rh.Count);
}
class EventInfo
{
public String EventId { get; set; }
public String EventName { get; set; }
}
}

View File

@ -1,5 +1,6 @@
using NewLife.Caching;
using NewLife.Log;
using NewLife.Security;
using System;
using System.Diagnostics;
using System.Linq;
@ -164,6 +165,38 @@ public class ListTests
Assert.Equal(vs3[1], item2);
}
[Fact]
public void List_IndexOf()
{
var key = "lkey_indexof";
// 删除已有
_redis.Remove(key);
var rlist = _redis.GetList<String>(key) as RedisList<String>;
Assert.NotNull(rlist);
// 添加
var vs = Enumerable.Range(0, 1000).Select(e => Rand.NextString(8)).ToArray();
rlist.AddRange(vs);
_redis.SetExpire(key, TimeSpan.FromSeconds(60));
// 索引
var idx = rlist.IndexOf(vs[1]);
Assert.Equal(1, idx);
idx = rlist.IndexOf("abcd2");
Assert.Equal(-1, idx);
idx = rlist.IndexOf(vs[321]);
Assert.Equal(321, idx);
var rs = rlist.Contains(vs[456]);
Assert.True(rs);
_redis.Remove(key);
}
[Fact]
public void RPOPLPUSH_Test()
{