服务器之家:专注于服务器技术及软件下载分享
分类导航

Mysql|Sql Server|Oracle|Redis|MongoDB|PostgreSQL|Sqlite|DB2|mariadb|Access|数据库技术|

服务器之家 - 数据库 - Redis - Redis缓存详解

Redis缓存详解

2020-04-20 16:00神牛步行3 Redis

本文主要介绍了Redis缓存从搭建到使用的相关知识,具有一定的参考价值,下面跟着小编一起来看下吧

下面来正式分享今天的文章吧:

。搭建Redis服务端,并用客户端连接

。封装缓存父类,定义Get,Set等常用方法

。定义RedisCache缓存类,执行Redis的Get,Set方法

。构造出缓存工厂调用方法

下面一步一个脚印的来分享:

。搭建Redis服务端,并用客户端连接

首先,咋们去这个地址下载安装文件https://github.com/dmajkic/redis/downloads,我这里的版本是:redis-2.4.5-win32-win64里面有32位和64位的执行文件,我这里服务器是64位的下面给出截图和用到部分程序的说明:

Redis缓存详解

现在,咋们直接可以用鼠标双击redis-server.exe这个应用程序,这样就打开了redis服务窗体(您也可以下载一个windows服务承载器,把redis服务运行在windows的服务中,就不用担心每次关闭redis服务黑色窗体后无法访问redis了),运行起来是这样:

Redis缓存详解

有红色框的信息就表示成功了,这里redis服务监听的端口默认是6379,要修改端口或者更多的配置信息请找到redis.conf配置文件,具体配置信息介绍可以来这里http://www.shouce.ren/api/view/a/6231

再来,打开客户端连接服务端,咋们退到64bit文件夹的目录中,鼠标移到64bit文件夹上并且安装Shift键,同时点击鼠标的右键,选中"在此处打开命令窗口"这样快速进入到了该文件夹的cmd命令窗口中(当然不同的操作系统不同,这里演示的是windows的操作;还有其他进入的方式这里不做介绍,因为个人感觉这是最快的);然后,在命令窗口中录入redis-cli.exe -h localhost -p 6379回车来访问服务端,效果图:

Redis缓存详解

再来看下服务端窗体截图:

Redis缓存详解

没错这样客户端就连接上服务端了,可以简单在客户端执行下set,get命令:

Redis缓存详解

如果是客户端要访问远程的redis服务端,只需要把localhost换成可访问的ip就行了如果还需要密码等更多配置请去上面的那个地址链接;

 。封装缓存父类,定义Get,Set等常用方法

先来,上父类的代码:

?
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
public class BaseCache : IDisposable
 {
 protected string def_ip = string.Empty;
 protected int def_port = 0;
 protected string def_password = string.Empty;
 public BaseCache()
 {
 }
 public virtual void InitCache(string ip = "", int port = 0, string password = "")
 {
 }
 public virtual bool SetCache<T>(string key, T t, int timeOutMinute = 10) where T : class,new()
 {
  return false;
 }
 public virtual T GetCache<T>(string key) where T : class,new()
 {
  return default(T);
 }
 public virtual bool Remove(string key)
 {
  return false;
 }
 public virtual bool FlushAll()
 {
  return false;
 }
 public virtual bool Any(string key)
 {
  return false;
 }
 public virtual void Dispose(bool isfalse)
 {
  if (isfalse)
  {
  }
 }
 //手动释放
 public void Dispose()
 {
  this.Dispose(true);
  //不自动释放
  GC.SuppressFinalize(this);
 }
 }

 这里定义的方法没有太多的注释,更多的意思我想看方法名称就明白了,这个父类主要实现了IDisposable,实现的Dispose()中主要用来释放资源并且自定义了一个 public virtual void Dispose(bool isfalse)方法,这里面有一句是GC.SuppressFinalize(this);按照官网介绍的意思是阻塞自动释放资源,其他的没有什么了,继续看下面的

。定义RedisCache缓存类,执行Redis的Get,Set方法

首先,咋们分别定义类RedisCache,MemcachedCache(这里暂未实现对memcache缓存的操作),并且继承BaseCache,重写Set,Get方法如下代码:

?
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
/// <summary>
 /// Redis缓存
 /// </summary>
 public class RedisCache : BaseCache
 {
 public RedisClient redis = null;
 public RedisCache()
 {
  //这里去读取默认配置文件数据
  def_ip = "172.0.0.1";
  def_port = 6379;
  def_password = "";
 }
 #region Redis缓存
 public override void InitCache(string ip = "", int port = 0, string password = "")
 {
  if (redis == null)
  {
  ip = string.IsNullOrEmpty(ip) ? def_ip : ip;
  port = port == 0 ? def_port : port;
  password = string.IsNullOrEmpty(password) ? def_password : password;
  redis = new RedisClient(ip, port, password);
  }
 }
 public override bool SetCache<T>(string key, T t, int timeOutMinute = 10)
 {
  var isfalse = false;
  try
  {
  if (string.IsNullOrEmpty(key)) { return isfalse; }
  InitCache();
  isfalse = redis.Set<T>(key, t, TimeSpan.FromMinutes(timeOutMinute));
  }
  catch (Exception ex)
  {
  }
  finally { this.Dispose(); }
  return isfalse;
 }
 public override T GetCache<T>(string key)
 {
  var t = default(T);
  try
  {
  if (string.IsNullOrEmpty(key)) { return t; }
  InitCache();
  t = redis.Get<T>(key);
  }
  catch (Exception ex)
  {
  }
  finally { this.Dispose(); }
  return t;
 }
 public override bool Remove(string key)
 {
  var isfalse = false;
  try
  {
  if (string.IsNullOrEmpty(key)) { return isfalse; }
  InitCache();
  isfalse = redis.Remove(key);
  }
  catch (Exception ex)
  {
  }
  finally { this.Dispose(); }
  return isfalse;
 }
 public override void Dispose(bool isfalse)
 {
  if (isfalse && redis != null)
  {
  redis.Dispose();
  redis = null;
  }
 }
 #endregion
 }
 /// <summary>
 /// Memcached缓存
 /// </summary>
 public class MemcachedCache : BaseCache
 {
 }

这里,用到的RedisClient类是来自nuget包引用的,这里nuget包是:

Redis缓存详解

然后,来看下重写的InitCache方法,这里面有一些ip,port(端口),password(密码)参数,这里直接写入在cs文件中没有从配置文件读取,大家可以扩展下;这些参数通过RedisClient构造函数传递给底层Socket访问需要的信息,下面简单展示下RedisClient几个的构造函数:

?
1
2
3
4
5
6
public RedisClient();
 public RedisClient(RedisEndpoint config);
 public RedisClient(string host);
 public RedisClient(Uri uri);
 public RedisClient(string host, int port);
 public RedisClient(string host, int port, string password = null, long db = 0);

至于Get,Set方法最终都是使用RedisClient对象访问的,个人觉得需要注意的是Set方法里面的过期时间参数,目前还没有试验这种情况的效果:

?通过这几种方法设置过期时间后,快到过期时间的时候如果此时有使用这个缓存key那么过期时间是否会往后自动增加过期时间有效期,这里暂时没有试验(这里是由于前面项目中的.net core框架中的memecache缓存都有这种设置,想来redis应该也有吧)

这里,需要重写下public override void Dispose(bool isfalse)方法,因为调用完RedisClient后需要释放,我们通过Dispose统一来手动释放,而不是直接在调用的时候使用using()

。构造出缓存工厂调用方法

接下来,咋们需要定义一个缓存工厂,因为上面刚才定义了一个RedisCache和MemcachedCache明显这里会有多个不同缓存的方法调用,所用咋们来定义个工厂模式来调用对应的缓存;这里的工厂模式没有使用直接显示创建new RedisCache(),new MemcachedCache()对象的方法,而是使用了反射的原理,创建对应的缓存对象;

先来,定义个枚举,枚举里面的声明的名字要和咋们缓存类的名称相同,代码如下:

?
1
2
3
4
5
public enum CacheType
 {
 RedisCache,
 MemcachedCache
 }

再来,定义个工厂来CacheRepository(缓存工厂),并且定义方法Current如下代码:

?
1
2
3
4
5
6
7
public static BaseCache Current(CacheType cacheType = CacheType.RedisCache)
 {
 var nspace = typeof(BaseCache);
 var fullName = nspace.FullName;
 var nowspace = fullName.Substring(0, fullName.LastIndexOf('.') + 1);
 return Assembly.GetExecutingAssembly().CreateInstance(nowspace + cacheType.ToString(), true) as BaseCache;
 }

*:通过传递枚举参数,来确定反射CreateInstance()方法需要用到的typeName参数,从而来定义需要访问的那个缓存对象,这里要注意的是加上了一个命名空间nowspace,因为缓存类可能和工厂类不是同一个命名空间,但是通常会和缓存基类是同命名空间所以在方法最开始的时候截取获取了缓存类需要的命名空间(这里看自身项目来定吧);

*:Assembly.GetExecutingAssembly()这个是用来获取当前应用程序集的路径,这里就避免了咋们使用Assembly.Load()方法还需要传递程序集的路径地址了

好了满上上面要求后,咋们可以在测试页面调用代码如:CacheRepository.Current(CacheType.RedisCache).SetCache<MoFlightSearchResponse>(keyData, value);就如此简单,咋们使用redis-cli.exe客户端来看下缓存起来的数据:

Redis缓存详解

怎么样,您们的是什么效果呢,下面给出整体代码

?
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
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
#region CacheRepository 缓存工厂(默认存储Session中)
 /// <summary>
 /// 缓存枚举
 /// </summary>
 public enum CacheType
 {
 BaseCache,
 RedisCache,
 MemcachedCache
 }
 /// <summary>
 /// 缓存工厂(默认存储Session中)
 /// </summary>
 public class CacheRepository
 {
 /// <summary>
 /// 缓存工厂(默认存储Session中, CacheKey = "SeesionKey")
 /// </summary>
 /// <param name="cacheType">缓存类型</param>
 /// <returns></returns>
 public static BaseCache Current(CacheType cacheType = CacheType.RedisCache)
 {
 var nspace = typeof(BaseCache);
 var fullName = nspace.FullName;
 var nowspace = fullName.Substring(0, fullName.LastIndexOf('.') + 1);
 
 return Assembly.GetExecutingAssembly().CreateInstance(nowspace + cacheType.ToString(), true) as BaseCache;
 }
 }
 /// <summary>
 /// 缓存基类(默认存储Session中)
 /// </summary>
 public class BaseCache : IDisposable
 {
 protected string def_ip = string.Empty;
 protected int def_port = 0;
 protected string def_password = string.Empty;
 protected string CacheKey = "SeesionKey";
 public BaseCache()
 {
 }
 /// <summary>
 /// 获取自定义SessionId值
 /// </summary>
 /// <param name="key">key:使用唯一的登陆账号</param>
 /// <returns>hash值的SessionId</returns>
 public virtual string GetSessionId(string key)
 {
 return Md5Extend.GetSidMd5Hash(key);
 }
 public virtual void InitCache(bool isReadAndWriter = true, string ip = "", int port = 0, string password = "")
 {
 }
 public virtual bool SetCache<T>(string key, T t, int timeOutMinute = 10, bool isSerilize = false) where T : class,new()
 {
 var isfalse = false;
 try
 {
 key = key ?? CacheKey;
 if (t == null) { return isfalse; }
 
 var session_json = JsonConvert.SerializeObject(t);
 HttpContext.Current.Session.Timeout = timeOutMinute;
 HttpContext.Current.Session.Add(key, session_json);
 isfalse = true;
 }
 catch (Exception ex)
 {
 throw new Exception(ex.Message);
 }
 return isfalse;
 }
 public virtual T GetCache<T>(string key = null, bool isSerilize = false) where T : class,new()
 {
 var t = default(T);
 try
 {
 key = key ?? CacheKey;
 var session = HttpContext.Current.Session[key];
 if (session == null) { return t; }
 
 t = JsonConvert.DeserializeObject<T>(session.ToString());
 }
 catch (Exception ex)
 {
 throw new Exception(ex.Message);
 }
 return t;
 }
 public virtual bool Remove(string key = null)
 {
 var isfalse = false;
 try
 {
 key = key ?? CacheKey;
 HttpContext.Current.Session.Remove(key);
 isfalse = true;
 }
 catch (Exception ex)
 {
 throw new Exception(ex.Message);
 }
 return isfalse;
 }
 /// <summary>
 /// 增加缓存时间
 /// </summary>
 /// <returns></returns>
 public virtual bool AddExpire(string key, int nTimeMinute = 10)
 {
 return true;
 }
 public virtual bool FlushAll()
 {
 return false;
 }
 public virtual bool Any(string key)
 {
 return false;
 }
 public virtual bool SetHashCache<T>(string hashId, string key, T t, int nTimeMinute = 10) where T : class,new()
 {
 return false;
 }
 public virtual List<string> GetHashKeys(string hashId)
 {
 return null;
 }
 public virtual List<string> GetHashValues(string hashId)
 {
 return null;
 }
 public virtual T GetHashValue<T>(string hashId, string key) where T : class,new()
 {
 var t = default(T);
 return t;
 }
 public virtual bool RemoveHashByKey(string hashId, string key)
 {
 return false;
 }
 public virtual void Dispose(bool isfalse)
 {
 if (isfalse)
 {
 }
 }
 //手动释放
 public void Dispose()
 {
 this.Dispose(true);
 //不自动释放
 GC.SuppressFinalize(this);
 }
 }
 /// <summary>
 /// Redis缓存
 /// </summary>
 public class RedisCache : BaseCache
 {
 public IRedisClient redis = null;
 public RedisCache()
 {
 //这里去读取默认配置文件数据
 def_ip = "127.0.0.1";
 def_port = 6379;
 def_password = "";
 }
 #region Redis缓存
 public static object _lockCache = new object();
 public override void InitCache(bool isReadAndWriter = true, string ip = "", int port = 0, string password = "")
 {
 if (redis == null)
 {
 ip = string.IsNullOrEmpty(ip) ? def_ip : ip;
 port = port == 0 ? def_port : port;
 password = string.IsNullOrEmpty(password) ? def_password : password;
 //单个redis服务
 //redis = new RedisClient(ip, port, password);
 //集群服务 如果密码,格式如:pwd@ip:port
 var readAndWritePorts = new List<string> { "shenniubuxing3@127.0.0.1:6379" };
 var onlyReadPorts = new List<string> {
  "shenniubuxing3@127.0.0.1:6378",
  "shenniubuxing3@127.0.0.1:6377"
 };
 var redisPool = new PooledRedisClientManager(
  readAndWritePorts,
  onlyReadPorts,
  new RedisClientManagerConfig
  {
  AutoStart = true,
  //最大读取链接
  MaxReadPoolSize = 20,
  //最大写入链接
  MaxWritePoolSize = 10
  })
 {
  //每个链接超时时间
  ConnectTimeout = 20,
  //连接池超时时间
  PoolTimeout = 60
 };
 lock (_lockCache)
 {
  redis = isReadAndWriter ? redisPool.GetClient() : redisPool.GetReadOnlyClient();
 }
 }
 }
 public override bool AddExpire(string key, int nTimeMinute = 10)
 {
 var isfalse = false;
 try
 {
 if (string.IsNullOrEmpty(key)) { return isfalse; }
 InitCache();
 //isfalse = redis.ExpireEntryIn(key, TimeSpan.FromMinutes(nTimeMinute));
 isfalse = redis.ExpireEntryAt(key, DateTime.Now.AddMinutes(nTimeMinute));
 }
 catch (Exception ex)
 {
 }
 finally { this.Dispose(); }
 return isfalse;
 }
 public override bool SetCache<T>(string key, T t, int timeOutMinute = 10, bool isSerilize = false)
 {
 var isfalse = false;
 try
 {
 if (string.IsNullOrEmpty(key)) { return isfalse; }
 InitCache();
 if (isSerilize)
 {
  var data = JsonConvert.SerializeObject(t);
  var bb = System.Text.Encoding.UTF8.GetBytes(data);
  isfalse = redis.Set<byte[]>(key, bb, TimeSpan.FromMinutes(timeOutMinute));
 }
 else { isfalse = redis.Set<T>(key, t, TimeSpan.FromMinutes(timeOutMinute)); }
 }
 catch (Exception ex)
 {
 }
 finally { this.Dispose(); }
 return isfalse;
 }
 public override T GetCache<T>(string key, bool isSerilize = false)
 {
 var t = default(T);
 try
 {
 if (string.IsNullOrEmpty(key)) { return t; }
 InitCache(false);
 if (isSerilize)
 {
  var bb = redis.Get<byte[]>(key);
  if (bb.Length <= 0) { return t; }
  var data = System.Text.Encoding.UTF8.GetString(bb);
  t = JsonConvert.DeserializeObject<T>(data);
 }
 else { t = redis.Get<T>(key); }
 }
 catch (Exception ex)
 {
 }
 finally { this.Dispose(); }
 return t;
 }
 public override bool Remove(string key)
 {
 var isfalse = false;
 try
 {
 if (string.IsNullOrEmpty(key)) { return isfalse; }
 InitCache();
 isfalse = redis.Remove(key);
 }
 catch (Exception ex)
 {
 }
 finally { this.Dispose(); }
 return isfalse;
 }
 public override bool SetHashCache<T>(string hashId, string key, T t, int nTimeMinute = 10)
 {
 var isfalse = false;
 try
 {
 if (string.IsNullOrEmpty(hashId) || string.IsNullOrEmpty(key) || t == null) { return isfalse; }
 InitCache();
 var result = JsonConvert.SerializeObject(t);
 if (string.IsNullOrEmpty(result)) { return isfalse; }
 isfalse = redis.SetEntryInHash(hashId, key, result);
 if (isfalse) { AddExpire(key, nTimeMinute); }
 }
 catch (Exception ex)
 {
 }
 finally { this.Dispose(); }
 return isfalse;
 }
 public override List<string> GetHashKeys(string hashId)
 {
 var hashKeys = new List<string>();
 try
 {
 if (string.IsNullOrEmpty(hashId)) { return hashKeys; }
 InitCache();
 hashKeys = redis.GetHashKeys(hashId);
 }
 catch (Exception ex)
 {
 }
 finally { this.Dispose(); }
 return hashKeys;
 }
 public override List<string> GetHashValues(string hashId)
 {
 var hashValues = new List<string>();
 try
 {
 if (string.IsNullOrEmpty(hashId)) { return hashValues; }
 
 InitCache();
 hashValues = redis.GetHashValues(hashId);
 }
 catch (Exception ex)
 {
 }
 finally { this.Dispose(); }
 return hashValues;
 }
 public override T GetHashValue<T>(string hashId, string key)
 {
 var t = default(T);
 try
 {
 if (string.IsNullOrEmpty(hashId) || string.IsNullOrEmpty(key)) { return t; }
 InitCache();
 var result = redis.GetValueFromHash(hashId, key);
 if (string.IsNullOrEmpty(result)) { return t; }
 t = JsonConvert.DeserializeObject<T>(result);
 }
 catch (Exception ex)
 {
 }
 finally { this.Dispose(); }
 return t;
 }
 public override bool RemoveHashByKey(string hashId, string key)
 {
 var isfalse = false;
 try
 {
 if (string.IsNullOrEmpty(hashId) || string.IsNullOrEmpty(key)) { return isfalse; }
 InitCache();
 isfalse = redis.RemoveEntryFromHash(hashId, key);
 }
 catch (Exception ex)
 {
 }
 finally { this.Dispose(); }
 return isfalse;
 }
 public override void Dispose(bool isfalse)
 {
 if (isfalse && redis != null)
 {
 redis.Dispose();
 redis = null;
 }
 }
 #endregion
 }
 /// <summary>
 /// Memcached缓存
 /// </summary>
 public class MemcachedCache : BaseCache
 {
 }
 #endregion

 这次分享的Redis缓存从搭建到使用希望给您们有帮助,还请多多支持点赞,谢谢。

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,同时也希望多多支持服务器之家!

原文链接:http://www.cnblogs.com/wangrudong003/p/5785116.html

延伸 · 阅读

精彩推荐
  • Redis关于Redis数据库入门详细介绍

    关于Redis数据库入门详细介绍

    大家好,本篇文章主要讲的是关于Redis数据库入门详细介绍,感兴趣的同学赶快来看一看吧,对你有帮助的话记得收藏一下,方便下次浏览...

    沃尔码6982022-01-24
  • RedisRedis 6.X Cluster 集群搭建

    Redis 6.X Cluster 集群搭建

    码哥带大家完成在 CentOS 7 中安装 Redis 6.x 教程。在学习 Redis Cluster 集群之前,我们需要先搭建一套集群环境。机器有限,实现目标是一台机器上搭建 6 个节...

    码哥字节15752021-04-07
  • RedisRedis集群的5种使用方式,各自优缺点分析

    Redis集群的5种使用方式,各自优缺点分析

    Redis 多副本,采用主从(replication)部署结构,相较于单副本而言最大的特点就是主从实例间数据实时同步,并且提供数据持久化和备份策略。...

    优知学院4082021-08-10
  • Redis《面试八股文》之 Redis十六卷

    《面试八股文》之 Redis十六卷

    redis 作为我们最常用的内存数据库,很多地方你都能够发现它的身影,比如说登录信息的存储,分布式锁的使用,其经常被我们当做缓存去使用。...

    moon聊技术8182021-07-26
  • Redis详解三分钟快速搭建分布式高可用的Redis集群

    详解三分钟快速搭建分布式高可用的Redis集群

    这篇文章主要介绍了详解三分钟快速搭建分布式高可用的Redis集群,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,...

    万猫学社4502021-07-25
  • Redis如何使用Redis锁处理并发问题详解

    如何使用Redis锁处理并发问题详解

    这篇文章主要给大家介绍了关于如何使用Redis锁处理并发问题的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用Redis具有一定的参考学习...

    haofly4522019-11-26
  • RedisRedis Template实现分布式锁的实例代码

    Redis Template实现分布式锁的实例代码

    这篇文章主要介绍了Redis Template实现分布式锁,需要的朋友可以参考下 ...

    晴天小哥哥2592019-11-18
  • Redisredis缓存存储Session原理机制

    redis缓存存储Session原理机制

    这篇文章主要为大家介绍了redis缓存存储Session原理机制详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪...

    程序媛张小妍9252021-11-25