wanshenmean
7 天以前 37acb8358f5602a9013ee29c04a45e33483c2329
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using WIDESEAWCS_Common;
using WIDESEAWCS_Core.Caches;
using WIDESEAWCS_Core.LogHelper;
using WIDESEAWCS_QuartzJob;
 
namespace WIDESEAWCS_Tasks
{
    /// <summary>
    /// 机械手状态管理器 - 负责 RobotSocketState 的线程安全更新和克隆
    /// </summary>
    /// <remarks>
    /// 核心功能是通过缓存服务(ICacheService)管理 Redis 中的机械手状态。
    /// 提供乐观并发控制,通过版本号(Version)字段防止并发更新时的数据覆盖问题。
    /// </remarks>
    public class RobotStateManager
    {
        /// <summary>
        /// 缓存服务实例,用于读写 Redis 中的状态数据
        /// </summary>
        private readonly ICacheService _cache;
 
        /// <summary>
        /// 日志记录器
        /// </summary>
        private readonly ILogger _logger;
 
        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="cache">缓存服务实例(通常为 HybridCacheService)</param>
        /// <param name="logger">日志记录器</param>
        public RobotStateManager(ICacheService cache, ILogger logger)
        {
            _cache = cache;
            _logger = logger;
        }
 
        /// <summary>
        /// 安全更新 RobotSocketState 缓存,防止并发覆盖
        /// </summary>
        /// <remarks>
        /// 使用乐观并发模式:先读取当前版本号,执行更新时检查版本是否一致。
        /// 如果版本不匹配(说明有其他线程已更新),则更新失败返回 false。
        /// </remarks>
        /// <param name="ipAddress">设备 IP 地址,用于构建缓存键</param>
        /// <param name="updateAction">更新状态的委托函数,传入当前状态副本,返回修改后的新状态</param>
        /// <returns>是否更新成功;false 表示版本冲突或状态不存在</returns>
        public bool TryUpdateStateSafely(string ipAddress, Func<RobotSocketState, RobotSocketState> updateAction)
        {
            // 构建 Redis 缓存键,格式:{RedisPrefix.Code}:{RedisName.SocketDevices}:{ipAddress}
            var cacheKey = GetCacheKey(ipAddress);
 
            // 从缓存获取当前存储的状态
            var currentState = _cache.Get<RobotSocketState>(cacheKey);
 
            // 如果缓存中不存在该设备的状态,直接返回 false(应由 GetOrCreateState 先创建)
            if (currentState == null)
            {
                return false;
            }
 
            // 记录当前存储的版本号,作为更新时的期望版本
            var expectedVersion = currentState.Version;
 
            // 创建状态的深拷贝副本,避免直接修改原对象引用
            // 这样可以确保在多线程环境下,每个线程操作的是独立的状态副本
            var stateCopy = CloneState(currentState);
 
            // 执行调用者提供的更新逻辑,传入副本状态,获取新的状态对象
            var newState = updateAction(stateCopy);
 
            // 将新状态的版本号更新为最新的时间戳,表示数据已更新
            newState.Version = DateTime.UtcNow.Ticks;
 
            // 调用缓存服务的安全更新方法,传入期望版本和版本提取器
            // 如果当前版本与期望版本不一致(已被其他线程更新),则更新失败
            return _cache.TrySafeUpdate(
                cacheKey,
                newState,
                expectedVersion,
                s => s.Version  // 指定哪个字段作为版本号
            );
        }
 
        /// <summary>
        /// 安全更新 RobotSocketState 缓存的重载版本(直接传入新状态)
        /// </summary>
        /// <remarks>
        /// 与上一个重载的区别:此方法直接接收完整的新状态对象,而不是更新委托。
        /// 如果设备状态不存在于缓存中,则直接添加新状态。
        /// </remarks>
        /// <param name="ipAddress">设备 IP 地址,用于构建缓存键</param>
        /// <param name="newState">新状态对象(方法内部会更新其 Version 字段)</param>
        /// <returns>是否更新成功;新建设置为 true</returns>
        public bool TryUpdateStateSafely(string ipAddress, RobotSocketState newState)
        {
            // 构建 Redis 缓存键
            var cacheKey = GetCacheKey(ipAddress);
 
            // 从缓存获取当前存储的状态
            var currentState = _cache.Get<RobotSocketState>(cacheKey);
 
            // 如果当前不存在该设备的状态
            if (currentState == null)
            {
                // 为新状态设置版本号(时间戳)
                newState.Version = DateTime.UtcNow.Ticks;
                // 直接添加到缓存
                _cache.AddObject(cacheKey, newState);
                _logger.LogDebug("TryUpdateStateSafely:创建新状态,IP: {IpAddress}", ipAddress);
                QuartzLogger.Debug($"创建新状态,IP: {ipAddress}", ipAddress);
                return true;
            }
 
            // 当前存在状态,记录期望版本号用于乐观锁检查
            var expectedVersion = currentState.Version;
 
            // 更新新状态的版本号为最新时间戳
            newState.Version = DateTime.UtcNow.Ticks;
 
            // 尝试安全更新,如果版本冲突则返回 false
            bool success = _cache.TrySafeUpdate(
                cacheKey,
                newState,
                expectedVersion,
                s => s.Version
            );
 
            if (!success)
            {
                _logger.LogWarning("TryUpdateStateSafely:版本冲突,更新失败,IP: {IpAddress},期望版本: {ExpectedVersion}", ipAddress, expectedVersion);
                QuartzLogger.Warn($"版本冲突,更新失败,IP: {ipAddress}", ipAddress);
            }
 
            return success;
        }
 
        /// <summary>
        /// 克隆 RobotSocketState 对象(深拷贝)
        /// </summary>
        /// <remarks>
        /// 使用 JSON 序列化/反序列化实现深拷贝。
        /// 这样可以确保新对象与原对象完全独立,修改新对象不会影响原对象。
        /// </remarks>
        /// <param name="source">源状态对象</param>
        /// <returns>新的状态对象,是源对象的深拷贝</returns>
        public RobotSocketState CloneState(RobotSocketState source)
        {
            // 将源对象序列化为 JSON 字符串
            var json = JsonConvert.SerializeObject(source);
            // 反序列化为新的 RobotSocketState 对象
            // 如果反序列化失败(返回 null),创建一个新对象并复制 IPAddress
            return JsonConvert.DeserializeObject<RobotSocketState>(json) ?? new RobotSocketState { IPAddress = source.IPAddress };
        }
 
        /// <summary>
        /// 获取 Redis 缓存键
        /// </summary>
        /// <remarks>
        /// 缓存键格式:{RedisPrefix.Code}:{RedisName.SocketDevices}:{ipAddress}
        /// 例如:Code:SocketDevices:192.168.1.100
        /// </remarks>
        /// <param name="ipAddress">设备 IP 地址</param>
        /// <returns>完整的 Redis 缓存键</returns>
        public static string GetCacheKey(string ipAddress)
        {
            return $"{RedisPrefix.Code}:{RedisName.SocketDevices}:{ipAddress}";
        }
 
        /// <summary>
        /// 从缓存获取机械手状态
        /// </summary>
        /// <param name="ipAddress">设备 IP 地址</param>
        /// <returns>如果存在则返回状态对象,否则返回 null</returns>
        public RobotSocketState? GetState(string ipAddress)
        {
            return _cache.Get<RobotSocketState>(GetCacheKey(ipAddress));
        }
 
        /// <summary>
        /// 获取或创建机械手状态
        /// </summary>
        /// <remarks>
        /// 如果缓存中已存在该设备的状态,直接返回。
        /// 如果不存在,则创建新的状态对象并存入缓存,然后返回。
        /// </remarks>
        /// <param name="ipAddress">设备 IP 地址</param>
        /// <param name="robotCrane">机器人设备信息,用于初始化新状态</param>
        /// <returns>该设备的状态对象</returns>
        public RobotSocketState GetOrCreateState(string ipAddress, RobotCraneDevice robotCrane)
        {
            // 使用缓存服务的 GetOrAdd 方法,工厂函数在缓存未命中时创建新状态
            return _cache.GetOrAdd(GetCacheKey(ipAddress), _ => new RobotSocketState
            {
                IPAddress = ipAddress,  // 设置 IP 地址作为标识
                RobotCrane = robotCrane  // 保存设备信息
            });
        }
    }
}