zoukankan      html  css  js  c++  java
  • .NetCore+Jexus代理+Redis模拟秒杀商品活动

    开篇叙

    本篇将和大家分享一下秒杀商品活动架构,采用的架构方案正如标题名称.NetCore+Jexus代理+Redis,由于精力有限所以这里只设计到商品添加,抢购,订单查询,处理队列抢购订单的功能;有不足或者不够详细的还请见谅,顺手点个推荐也不错;

    a. 秒杀流程

    b. 封装StackExchange.Redis的使用类

    c. Ubuntu16.04上使用Jexus搭建代理完成分布式部署

    d. NetCore写实时监控队列服务

    秒杀架构设计图︿( ̄︶ ̄)︿三幅

    1. 一般业务性架构

     

    2. 后端分布式架构

     

    3. 整站分布式

     

    项目工程结构描述

    a. 该项目git开源地址: https://github.com/shenniubuxing3/SeckillPro ,线上效果地址: http://www.lovexins.com:3333/

    b. SeckillPro.Web:面向用户的web站点,主要提供商品展示,秒杀抢购,抢购结果,订单列表等功能;

    c. SeckillPro.Api:主要处理秒杀活动的请求,然后加入到秒杀队列中,以及订单状态的查询接口;

    d. SeckillPro.Server:处理秒杀队列的服务;根据Redis模糊匹配key的方式,开启多个商品秒杀的任务,并处理秒杀请求和改变订单抢购状态;

    e. SeckillPro.Com:集成公共的方法;这里面前有操作Redis的list,hash,string的封装类;

    SeckillPro.Web商品后台管理

    对于商品活动来说,商品维护是必不可少的,由于这里商品维护的信息比较少,并且这里只加入到了RedisDb中,所以就不直接上代码了;一个列表,一个添加仅此而已;这里就不再贴代码了,如果你感兴趣可以去我的git上面看源码: https://github.com/shenniubuxing3/SeckillPro/blob/master/SeckillPro/SeckillPro.Web/Controllers/HomeController.cs 

    SeckillPro.Web用户端商品列表+秒杀请求+用户订单列表

    商品列表和订单列表没有可以太多说的,一般订单系统都有这两个列表;关键点在于订单秒杀流程中,咋们来简单分析下面向客户秒杀的流程需要注意的事项:

    a. 限制秒杀开始时间和结束时间(测试未限制)

    b. 未开始活动限制提交按钮不可点(测试未限制)

    c. 获取真实剩余库存限制秒杀提交(获取redis中商品hash存储的真实剩余量)

    d. 把客户的秒杀请求转移到另外的api集群,以此提高面向客户端的web站点并发承载率(测试项目中我直接指定4545端口的api测试)

    这里就不再贴代码了,如果你感兴趣可以去我的git上面看看这部分源码: https://github.com/shenniubuxing3/SeckillPro/blob/master/SeckillPro/SeckillPro.Web/Controllers/HomeController.cs 

    .NetCore写处理秒杀活动队列的服务

    这个处理队列服务处理流程:模糊匹配Redis中每种商品的队列key-》开启不同商品的处理队列任务-》处理秒杀订单-》更新库存和秒杀订单状态;

    a. 模糊匹配Redis中每种商品的队列key:这里采用的是StackExchange.Redis中指定redis原生命令的方法来获取匹配队列key,设计的代码如下:

     1 /// <summary>
     2         /// 模糊匹配redis中的key
     3         /// </summary>
     4         /// <param name="paramArr"></param>
     5         /// <returns></returns>
     6         public async Task<List<string>> MatchKeys(params string[] paramArr)
     7         {
     8             var list = new List<string>();
     9             try
    10             {
    11                 var result = await this.ExecuteAsync("keys", paramArr);
    12 
    13                 var valArr = ((RedisValue[])result);
    14                 foreach (var item in valArr)
    15                 {
    16                     list.Add(item);
    17                 }
    18             }
    19             catch (Exception ex) { }
    20             return list;
    21         }
    22 
    23         /// <summary>
    24         /// 执行redis原生命令
    25         /// </summary>
    26         /// <param name="cmd"></param>
    27         /// <param name="paramArr"></param>
    28         /// <returns></returns>
    29         public async Task<RedisResult> ExecuteAsync(string cmd, params string[] paramArr)
    30         {
    31             try
    32             {
    33                 var db = this.GetDb();
    34                 return await db.ExecuteAsync(cmd, paramArr);
    35             }
    36             catch (Exception ex) { }
    37             return default(RedisResult);
    38         }

    b. 开启不同商品的处理队列任务:通过Task.Factory.StartNew(action,object)方法开启不同商品的处理秒杀订单的任务;

    c. 更新库存和秒杀订单状态:由于抢购商品要求库存剩余实时性,所以每处理一个抢购订单,需要对该商品减去相应的库存和修改秒杀订单的状态方便用户查看秒杀结果;

    d. 处理队列具体的实现代码可以去git看下,个人觉得还是有用的:https://github.com/shenniubuxing3/SeckillPro/blob/master/SeckillPro/SeckillPro.Server/Program.cs

    使用Jexus代理部署分布式站点和接口

    这里部署的代理采用的是Jexus代理;作为在linux和unix上部署.net程序实用的工具,真的很感谢jexus作者;首先本篇讲解的部署环境是ubunt16.04x64(至于这么安装jexus可以参考上一篇分享文章),为了更直观的看出来效果我在服务器上拷贝了两份SeckillPro.Web发布的站点,他们代码都是一样的只是分别把_Layout.cshtml试图模板中加入了端口7777和8888,我就用这两个端口来测试jexus的代理效果;

    测试方便直接分别在两个复制站点中执行如下终端命令:dotnet SeckillPro.Web.dll http://ip:端口 ;一个监听7777端口一个监听8888;执行命令效果图:

    监听7777和8888端口成功后,我们就可以直接在浏览器输入:http://172.16.9.66:7777 访问,正常情况下能够看到如下图示例:

    单个站点访问没问题了,下面开始配置jexus代理;只需要在jexus/siteconf的配置文件中(我这里是default配置文件),增加如下设置:

    注意reproxy参数:

    a. 第一个/表示根目录,一般不变

    b. 多个被代理地址使用‘,’隔开;

    c. 被代理地址后面也同样需要加/

    此时我们配置完后,只需要启动jexus就行了:./jws start (怎么启动可以参考上一篇文章);当启动jws成功后,我们就能通过配置的80端口,来访问SeckillPro.Web站点了,效果图:

    至于代理分发的策略暂不在本章的讨论范围内,如果可以建议去jexus官网了解下;同样对于Seckill.Api我们也可以这样部署,这里部署了个秒杀线上地址,有兴趣的朋友可以点击试试:http://www.lovexins.com:3333/ (注:这里没有使用代理)

    封装StackExchange.Redis的使用类StackRedis.cs

    其实这个在之前已经分享过了,只不过只有操作string和list的分装;本篇测试涉及到订单查询和商品查询等功能,所以这里我又扩展了对hash的操作方法,可以说更丰富了吧,如果您正打算使用redis或许直接用我这个封装类是个不错的打算;

      1 public class StackRedis : IDisposable
      2     {
      3         #region 配置属性   基于 StackExchange.Redis 封装
      4         //连接串 (注:IP:端口,属性=,属性=)
      5         public string _ConnectionString = "127.0.0.1:6377,password=shenniubuxing3";
      6         //操作的库(注:默认0库)
      7         public int _Db = 0;
      8         #endregion
      9 
     10         #region 管理器对象
     11 
     12         /// <summary>
     13         /// 获取redis操作类对象
     14         /// </summary>
     15         private static StackRedis _StackRedis;
     16         private static object _locker_StackRedis = new object();
     17         public static StackRedis Current
     18         {
     19             get
     20             {
     21                 if (_StackRedis == null)
     22                 {
     23                     lock (_locker_StackRedis)
     24                     {
     25                         _StackRedis = _StackRedis ?? new StackRedis();
     26                         return _StackRedis;
     27                     }
     28                 }
     29 
     30                 return _StackRedis;
     31             }
     32         }
     33 
     34         /// <summary>
     35         /// 获取并发链接管理器对象
     36         /// </summary>
     37         private static ConnectionMultiplexer _redis;
     38         private static object _locker = new object();
     39         public ConnectionMultiplexer Manager
     40         {
     41             get
     42             {
     43                 if (_redis == null)
     44                 {
     45                     lock (_locker)
     46                     {
     47                         _redis = _redis ?? GetManager(this._ConnectionString);
     48                         return _redis;
     49                     }
     50                 }
     51 
     52                 return _redis;
     53             }
     54         }
     55 
     56         /// <summary>
     57         /// 获取链接管理器
     58         /// </summary>
     59         /// <param name="connectionString"></param>
     60         /// <returns></returns>
     61         public ConnectionMultiplexer GetManager(string connectionString)
     62         {
     63             return ConnectionMultiplexer.Connect(connectionString);
     64         }
     65 
     66         /// <summary>
     67         /// 获取操作数据库对象
     68         /// </summary>
     69         /// <returns></returns>
     70         public IDatabase GetDb()
     71         {
     72             return Manager.GetDatabase(_Db);
     73         }
     74         #endregion
     75 
     76         #region 操作方法
     77 
     78         #region string 操作
     79 
     80         /// <summary>
     81         /// 根据Key移除
     82         /// </summary>
     83         /// <param name="key"></param>
     84         /// <returns></returns>
     85         public async Task<bool> Remove(string key)
     86         {
     87             var db = this.GetDb();
     88 
     89             return await db.KeyDeleteAsync(key);
     90         }
     91 
     92         /// <summary>
     93         /// 根据key获取string结果
     94         /// </summary>
     95         /// <param name="key"></param>
     96         /// <returns></returns>
     97         public async Task<string> Get(string key)
     98         {
     99             var db = this.GetDb();
    100             return await db.StringGetAsync(key);
    101         }
    102 
    103         /// <summary>
    104         /// 根据key获取string中的对象
    105         /// </summary>
    106         /// <typeparam name="T"></typeparam>
    107         /// <param name="key"></param>
    108         /// <returns></returns>
    109         public async Task<T> Get<T>(string key)
    110         {
    111             var t = default(T);
    112             try
    113             {
    114                 var _str = await this.Get(key);
    115                 if (string.IsNullOrWhiteSpace(_str)) { return t; }
    116 
    117                 t = JsonConvert.DeserializeObject<T>(_str);
    118             }
    119             catch (Exception ex) { }
    120             return t;
    121         }
    122 
    123         /// <summary>
    124         /// 存储string数据
    125         /// </summary>
    126         /// <param name="key"></param>
    127         /// <param name="value"></param>
    128         /// <param name="expireMinutes"></param>
    129         /// <returns></returns>
    130         public async Task<bool> Set(string key, string value, int expireMinutes = 0)
    131         {
    132             var db = this.GetDb();
    133             if (expireMinutes > 0)
    134             {
    135                 return db.StringSet(key, value, TimeSpan.FromMinutes(expireMinutes));
    136             }
    137             return await db.StringSetAsync(key, value);
    138         }
    139 
    140         /// <summary>
    141         /// 存储对象数据到string
    142         /// </summary>
    143         /// <typeparam name="T"></typeparam>
    144         /// <param name="key"></param>
    145         /// <param name="value"></param>
    146         /// <param name="expireMinutes"></param>
    147         /// <returns></returns>
    148         public async Task<bool> Set<T>(string key, T value, int expireMinutes = 0)
    149         {
    150             try
    151             {
    152                 var jsonOption = new JsonSerializerSettings()
    153                 {
    154                     ReferenceLoopHandling = ReferenceLoopHandling.Ignore
    155                 };
    156                 var _str = JsonConvert.SerializeObject(value, jsonOption);
    157                 if (string.IsNullOrWhiteSpace(_str)) { return false; }
    158 
    159                 return await this.Set(key, _str, expireMinutes);
    160             }
    161             catch (Exception ex) { }
    162             return false;
    163         }
    164 
    165         /// <summary>
    166         /// 是否存在key
    167         /// </summary>
    168         /// <typeparam name="T"></typeparam>
    169         /// <param name="key"></param>
    170         /// <returns></returns>
    171         public async Task<bool> KeyExists(string key)
    172         {
    173             try
    174             {
    175                 var db = this.GetDb();
    176                 return await db.KeyExistsAsync(key);
    177             }
    178             catch (Exception ex) { }
    179             return false;
    180         }
    181 
    182         #endregion
    183 
    184         #region hash操作
    185 
    186         /// <summary>
    187         /// 是否存在hash的列
    188         /// </summary>
    189         /// <param name="key"></param>
    190         /// <param name="filedKey"></param>
    191         /// <returns></returns>
    192         public async Task<bool> HashFieldExists(string key, string filedKey)
    193         {
    194             try
    195             {
    196                 if (string.IsNullOrWhiteSpace(key) || string.IsNullOrWhiteSpace(filedKey)) { return false; }
    197 
    198                 var result = await this.HashFieldsExists(key, new Dictionary<string, bool> { { filedKey, false } });
    199                 return result[filedKey];
    200             }
    201             catch (Exception ex) { }
    202             return false;
    203         }
    204 
    205         /// <summary>
    206         /// 是否存在hash的列集合
    207         /// </summary>
    208         /// <param name="key"></param>
    209         /// <param name="dics"></param>
    210         /// <returns></returns>
    211         public async Task<Dictionary<string, bool>> HashFieldsExists(string key, Dictionary<string, bool> dics)
    212         {
    213             try
    214             {
    215                 if (dics.Count <= 0) { return dics; }
    216 
    217                 var db = this.GetDb();
    218                 foreach (var fieldKey in dics.Keys)
    219                 {
    220                     dics[fieldKey] = await db.HashExistsAsync(key, fieldKey);
    221                 }
    222             }
    223             catch (Exception ex) { }
    224             return dics;
    225         }
    226 
    227         /// <summary>
    228         /// 设置hash
    229         /// </summary>
    230         /// <typeparam name="T"></typeparam>
    231         /// <param name="key"></param>
    232         /// <param name="filedKey"></param>
    233         /// <param name="t"></param>
    234         /// <returns></returns>
    235         public async Task<long> SetOrUpdateHashsField<T>(string key, string filedKey, T t, bool isAdd = true)
    236         {
    237             var result = 0L;
    238             try
    239             {
    240                 return await this.SetOrUpdateHashsFields<T>(key, new Dictionary<string, T> { { filedKey, t } }, isAdd);
    241             }
    242             catch (Exception ex) { }
    243             return result;
    244         }
    245 
    246         /// <summary>
    247         /// 设置hash集合,添加和更新操作
    248         /// </summary>
    249         /// <typeparam name="T"></typeparam>
    250         /// <param name="key"></param>
    251         /// <param name="dics"></param>
    252         /// <returns></returns>
    253         public async Task<long> SetOrUpdateHashsFields<T>(string key, Dictionary<string, T> dics, bool isAdd = true)
    254         {
    255             var result = 0L;
    256             try
    257             {
    258                 var jsonOption = new JsonSerializerSettings()
    259                 {
    260                     ReferenceLoopHandling = ReferenceLoopHandling.Ignore
    261                 };
    262                 var db = this.GetDb();
    263                 foreach (var fieldKey in dics.Keys)
    264                 {
    265                     var item = dics[fieldKey];
    266                     var _str = JsonConvert.SerializeObject(item, jsonOption);
    267                     result += await db.HashSetAsync(key, fieldKey, _str) ? 1 : 0;
    268                     if (!isAdd) { result++; }
    269                 }
    270                 return result;
    271             }
    272             catch (Exception ex) { }
    273             return result;
    274         }
    275 
    276         /// <summary>
    277         /// 移除hash的列
    278         /// </summary>
    279         /// <param name="key"></param>
    280         /// <param name="filedKey"></param>
    281         /// <returns></returns>
    282         public async Task<bool> RemoveHashField(string key, string filedKey)
    283         {
    284             try
    285             {
    286                 if (string.IsNullOrWhiteSpace(key) || string.IsNullOrWhiteSpace(filedKey)) { return false; }
    287 
    288                 var result = await this.RemoveHashFields(key, new Dictionary<string, bool> { { filedKey, false } });
    289                 return result[filedKey];
    290             }
    291             catch (Exception ex) { }
    292             return false;
    293         }
    294 
    295         /// <summary>
    296         /// 异常hash的列集合
    297         /// </summary>
    298         /// <param name="key"></param>
    299         /// <param name="dics"></param>
    300         /// <returns></returns>
    301         public async Task<Dictionary<string, bool>> RemoveHashFields(string key, Dictionary<string, bool> dics)
    302         {
    303 
    304             try
    305             {
    306                 var jsonOption = new JsonSerializerSettings()
    307                 {
    308                     ReferenceLoopHandling = ReferenceLoopHandling.Ignore
    309                 };
    310                 var db = this.GetDb();
    311                 foreach (var fieldKey in dics.Keys)
    312                 {
    313                     dics[fieldKey] = await db.HashDeleteAsync(key, fieldKey);
    314                 }
    315                 return dics;
    316             }
    317             catch (Exception ex) { }
    318             return dics;
    319         }
    320 
    321         /// <summary>
    322         /// 设置hash
    323         /// </summary>
    324         /// <typeparam name="T"></typeparam>
    325         /// <param name="key"></param>
    326         /// <param name="filedKey"></param>
    327         /// <param name="t"></param>
    328         /// <returns></returns>
    329         public async Task<T> GetHashField<T>(string key, string filedKey)
    330         {
    331             var t = default(T);
    332             try
    333             {
    334                 var dics = await this.GetHashFields<T>(key, new Dictionary<string, T> { { filedKey, t } });
    335                 return dics[filedKey];
    336             }
    337             catch (Exception ex) { }
    338             return t;
    339         }
    340 
    341         /// <summary>
    342         /// 获取hash的列值集合
    343         /// </summary>
    344         /// <typeparam name="T"></typeparam>
    345         /// <param name="key"></param>
    346         /// <param name="dics"></param>
    347         /// <returns></returns>
    348         public async Task<Dictionary<string, T>> GetHashFields<T>(string key, Dictionary<string, T> dics)
    349         {
    350             try
    351             {
    352                 var db = this.GetDb();
    353                 foreach (var fieldKey in dics.Keys)
    354                 {
    355                     var str = await db.HashGetAsync(key, fieldKey);
    356                     if (string.IsNullOrWhiteSpace(str)) { continue; }
    357 
    358                     dics[fieldKey] = JsonConvert.DeserializeObject<T>(str);
    359                 }
    360                 return dics;
    361             }
    362             catch (Exception ex) { }
    363             return dics;
    364         }
    365 
    366         /// <summary>
    367         /// 获取hash的key的所有列的值
    368         /// </summary>
    369         /// <typeparam name="T"></typeparam>
    370         /// <param name="key"></param>
    371         /// <returns></returns>
    372         public async Task<Dictionary<string, T>> GetHashs<T>(string key)
    373         {
    374             var dic = new Dictionary<string, T>();
    375             try
    376             {
    377                 var db = this.GetDb();
    378 
    379                 var hashFiles = await db.HashGetAllAsync(key);
    380                 foreach (var field in hashFiles)
    381                 {
    382                     dic[field.Name] = JsonConvert.DeserializeObject<T>(field.Value);
    383                 }
    384                 return dic;
    385             }
    386             catch (Exception ex) { }
    387             return dic;
    388         }
    389 
    390         /// <summary>
    391         /// 获取hash的Key的所有列的值的list集合
    392         /// </summary>
    393         /// <typeparam name="T"></typeparam>
    394         /// <param name="key"></param>
    395         /// <returns></returns>
    396         public async Task<List<T>> GetHashsToList<T>(string key)
    397         {
    398             var list = new List<T>();
    399             try
    400             {
    401                 var db = this.GetDb();
    402 
    403                 var hashFiles = await db.HashGetAllAsync(key);
    404                 foreach (var field in hashFiles)
    405                 {
    406                     var item = JsonConvert.DeserializeObject<T>(field.Value);
    407                     if (item == null) { continue; }
    408                     list.Add(item);
    409                 }
    410             }
    411             catch (Exception ex) { }
    412             return list;
    413         }
    414 
    415         #endregion
    416 
    417         #region List操作(注:可以当做队列使用)
    418 
    419         /// <summary>
    420         /// list长度
    421         /// </summary>
    422         /// <typeparam name="T"></typeparam>
    423         /// <param name="key"></param>
    424         /// <returns></returns>
    425         public async Task<long> GetListLen<T>(string key)
    426         {
    427             try
    428             {
    429                 var db = this.GetDb();
    430                 return await db.ListLengthAsync(key);
    431             }
    432             catch (Exception ex) { }
    433             return 0;
    434         }
    435 
    436         /// <summary>
    437         /// 获取List数据
    438         /// </summary>
    439         /// <typeparam name="T"></typeparam>
    440         /// <param name="key"></param>
    441         /// <returns></returns>
    442         public async Task<List<T>> GetList<T>(string key)
    443         {
    444             var t = new List<T>();
    445             try
    446             {
    447                 var db = this.GetDb();
    448                 var _values = await db.ListRangeAsync(key);
    449                 foreach (var item in _values)
    450                 {
    451                     if (string.IsNullOrWhiteSpace(item)) { continue; }
    452                     t.Add(JsonConvert.DeserializeObject<T>(item));
    453                 }
    454             }
    455             catch (Exception ex) { }
    456             return t;
    457         }
    458 
    459         /// <summary>
    460         /// 获取队列出口数据并移除
    461         /// </summary>
    462         /// <typeparam name="T"></typeparam>
    463         /// <param name="key"></param>
    464         /// <returns></returns>
    465         public async Task<T> GetListAndPop<T>(string key)
    466         {
    467             var t = default(T);
    468             try
    469             {
    470                 var db = this.GetDb();
    471                 var _str = await db.ListRightPopAsync(key);
    472                 if (string.IsNullOrWhiteSpace(_str)) { return t; }
    473                 t = JsonConvert.DeserializeObject<T>(_str);
    474             }
    475             catch (Exception ex) { }
    476             return t;
    477         }
    478 
    479         /// <summary>
    480         /// 集合对象添加到list左边
    481         /// </summary>
    482         /// <typeparam name="T"></typeparam>
    483         /// <param name="key"></param>
    484         /// <param name="values"></param>
    485         /// <returns></returns>
    486         public async Task<long> SetLists<T>(string key, List<T> values)
    487         {
    488             var result = 0L;
    489             try
    490             {
    491                 var jsonOption = new JsonSerializerSettings()
    492                 {
    493                     ReferenceLoopHandling = ReferenceLoopHandling.Ignore
    494                 };
    495                 var db = this.GetDb();
    496                 foreach (var item in values)
    497                 {
    498                     var _str = JsonConvert.SerializeObject(item, jsonOption);
    499                     result += await db.ListLeftPushAsync(key, _str);
    500                 }
    501                 return result;
    502             }
    503             catch (Exception ex) { }
    504             return result;
    505         }
    506 
    507         /// <summary>
    508         /// 单个对象添加到list左边
    509         /// </summary>
    510         /// <typeparam name="T"></typeparam>
    511         /// <param name="key"></param>
    512         /// <param name="value"></param>
    513         /// <returns></returns>
    514         public async Task<long> SetList<T>(string key, T value)
    515         {
    516             var result = 0L;
    517             try
    518             {
    519                 result = await this.SetLists(key, new List<T> { value });
    520             }
    521             catch (Exception ex) { }
    522             return result;
    523         }
    524 
    525 
    526         #endregion
    527 
    528         #region 额外扩展
    529 
    530         public async Task<List<string>> MatchKeys(params string[] paramArr)
    531         {
    532             var list = new List<string>();
    533             try
    534             {
    535                 var result = await this.ExecuteAsync("keys", paramArr);
    536 
    537                 var valArr = ((RedisValue[])result);
    538                 foreach (var item in valArr)
    539                 {
    540                     list.Add(item);
    541                 }
    542             }
    543             catch (Exception ex) { }
    544             return list;
    545         }
    546 
    547         /// <summary>
    548         /// 执行redis原生命令
    549         /// </summary>
    550         /// <param name="cmd"></param>
    551         /// <param name="paramArr"></param>
    552         /// <returns></returns>
    553         public async Task<RedisResult> ExecuteAsync(string cmd, params string[] paramArr)
    554         {
    555             try
    556             {
    557                 var db = this.GetDb();
    558                 return await db.ExecuteAsync(cmd, paramArr);
    559             }
    560             catch (Exception ex) { }
    561             return default(RedisResult);
    562         }
    563 
    564         /// <summary>
    565         /// 手动回收管理器对象
    566         /// </summary>
    567         public void Dispose()
    568         {
    569             this.Dispose(_redis);
    570         }
    571 
    572         public void Dispose(ConnectionMultiplexer con)
    573         {
    574             if (con != null)
    575             {
    576                 con.Close();
    577                 con.Dispose();
    578             }
    579         }
    580 
    581         #endregion
    582 
    583         #endregion
    584     }
  • 相关阅读:
    Class StatusesTableSeeder does not exist 如何解决
    Heroku 如何上重置 PostgreSQL 数据库
    git强制push
    Heroku登录失败
    将手机替换为*号
    使用TP自带缓存时。出现第一次拿不到数据。
    PHP实现查看邮件是否被阅读
    使用file_get_contents下载图片
    介绍几款开源好用的产品
    查看光纤卡wwn号【转载】
  • 原文地址:https://www.cnblogs.com/wangrudong003/p/7111789.html
Copyright © 2011-2022 走看看