热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

通过lms.samples熟悉lms微服务框架的使用详解

这篇文章主要介绍了通过lms.samples熟悉lms微服务框架的使用,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

经过一段时间的开发与测试,终于发布了Lms框架的第一个正式版本(1.0.0版本),并给出了lms框架的样例项目lms.samples。本文通过对lms.samples的介绍,简述如何通过lms框架快速的构建一个微服务的业务框架,并进行应用开发。

lms.samples项目基本介绍

lms.sample项目由三个独立的微服务应用模块组成:account、stock、order和一个网关项目gateway构成。

业务应用模块

每个独立的微服务应用采用模块化设计,主要由如下几部分组成:

  1. 主机(Host): 主要用于托管微服务应用本身,主机通过引用应用服务项目(应用接口的实现),托管微服务应用,通过托管应用服务,在主机启动的过程中,向服务注册中心注册服务路由。
  2. 应用接口层(Application.Contracts): 用于定义应用服务接口,通过应用接口,该微服务模块与其他微服务模块或是网关进行rpc通信的能力。在该项目中,除了定义应用服务接口之前,一般还定义与该应用接口相关的DTO对象。应用接口除了被该微服务应用项目引用,并实现应用服务之前,还可以被网关或是其他微服务模块引用。网关或是其他微服务项目通过应用接口生成的代理与该微服务模块通过rpc进行通信。
  3. 应用服务层(Application): 应用服务是该微服务定义的应用接口的实现。应用服务与DDD传统分层架构的应用层的概念一致。主要负责外部通信与领域层之间的协调。一般地,应用服务进行业务流程控制,但是不包含业务逻辑的实现。
  4. 领域层(Domain): 负责表达业务概念,业务状态信息以及业务规则,是该微服务模块的业务核心。一般地,在该层可以定义聚合根、实体、领域服务等对象。
  5. 领域共享层(Domain.Shared): 该层用于定义与领域对象相关的模型、实体等相关类型。不包含任何业务实现,可以被其他微服务引用。
  6. 数据访问(DataAccess)层: 该层一般用于封装数据访问相关的对象。例如:仓库对象、 SqlHelper、或是ORM相关的类型等。在lms.samples中,通过efcore实现数据的读写操作。

服务聚合与网关

lms框架不允许服务外部与微服务主机直接通信,应用请求必须通过http请求到达网关,网关通过lms提供的中间件解析到服务条目,并通过rpc与集群内部的微服务进行通信。所以,如果服务需要与集群外部进行通信,那么,开发者定义的网关必须要引用各个微服务模块的应用接口层;以及必须要使用lms相关的中间件。

开发环境

  1. .net版本: 5.0.101
  2. lms版本: 1.0.0
  3. IDE: (1) visual studio 最新版 (2) Rider(推荐)

主机与应用托管

主机的创建步骤

通过lms框架创建一个业务模块非常方便,只需要通过如下4个步骤,就可以轻松的创建一个lms应用业务模块。

1.创建项目

创建控制台应用(Console Application)项目,并且引用Silky.Lms.NormHost包。

dotnet add package Silky.Lms.NormHost --version 1.0.0

2.应用程序入口与主机构建

main方法中,通用.net的主机Host构建并注册lms微服务。在注册lms微服务时,需要指定lms启动的依赖模块。

一般地,如果开发者不需要额外依赖其他模块,也无需在应用启动或停止时执行方法,那么您可以直接指定NormHostModule模块。

 public class Program
    {
        public static async Task Main(string[] args)
        {
            await CreateHostBuilder(args).Build().RunAsync();
        }

        private static IHostBuilder CreateHostBuilder(string[] args)
        {
            return Host.CreateDefaultBuilder(args)
                    .RegisterLmsServices()
                ;
        }
    }

3.配置文件

lms框架支持yml或是json格式作为配置文件。通过appsettings.yml对lms框架进行统一配置,通过appsettings.${Environment}.yml对不同环境变量下的配置项进行设置。

开发者如果直接通过项目的方式启动应用,那么可以通过Properties/launchSettings.jsonenvironmentVariables.DOTNET_ENVIRONMENT环境变量。如果通过docker-compose的方式启动应用,那么可以通过.env设置DOTNET_ENVIRONMENT环境变量。

为保证配置文件有效,开发者需要显式的将配置文件拷贝到项目生成目录下。

4.引用应用服务层和数据访问层

一般地,主机项目需要引用该微服务模块的应用服务层和数据访问层。只有主机引用应用服务层,主机在启动时,才会生成服务条目的路由,并且将服务路由注册到服务注册中心。

一个典型的主机项目文件如下所示:



    
        Exe
        net5.0
    

    
      
    

    
      
        Always
      
      
        Always
      
      
        Always
      
    

    
      
      
    

配置

一般地,一个微服务模块的主机必须要配置:服务注册中心、分布式锁链接、分布式缓存地址、集群rpc通信token、数据库链接地址等。

如果使用docker-compose来启动和调试应用的话,那么,rpc配置节点下的的host和port可以缺省,因为生成的每个容器的都有自己的地址和端口号。

如果直接通过项目的方式启动和调试应用的话,那么,必须要配置rpc节点下的port,每个微服务模块的主机应用有自己的端口号。

lms框架的必要配置如下所示:

rpc:
  host: 0.0.0.0
  rpcPort: 2201
  token: ypjdYOzNd4FwENJiEARMLWwK0v7QUHPW
registrycenter:
  connectionStrings: 127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183;127.0.0.1:2184,127.0.0.1:2185,127.0.0.1:2186 # 使用分号;来区分不同的服务注册中心
  registryCenterType: Zookeeper
distributedCache:
  redis:
    isEnabled: true 
    configuration: 127.0.0.1:6379,defaultDatabase=0
lock:
  lockRedisConnection: 127.0.0.1:6379,defaultDatabase=1
connectionStrings:
    default: server=127.0.0.1;port=3306;database=account;uid=root;pwd=qwe!P4ss;

应用接口

应用接口定义

一般地,在应用接口层开发者需要安装Silky.Lms.Rpc包。如果该微服务模块还涉及到分布式事务,那么还需要安装Silky.Lms.Transaction.Tcc,当然,您也可以选择在应用接口层安装Silky.Lms.Transaction包,在应用服务层安装Silky.Lms.Transaction.Tcc包。

  1. 开发者只需要在应用接口通过ServiceRouteAttribute特性对应用接口进行直接即可。
  2. Lms约定应用接口应当以IXxxAppService命名,这样,服务条目生成的路由则会以api/xxx形式生成。当然这并不是强制的。
  3. 每个应用接口的方法都对应着一个服务条目,服务条目的Id为: 方法的完全限定名 + 参数名
  4. 您可以在应用接口层对方法的缓存、路由、服务治理、分布式事务进行相关配置。该部分内容请参考官方文档
  5. 网关或是其他模块的微服务项目需要引用服务应用接口项目或是通过nuget的方式安装服务应用接口生成的包。
  6. [Governance(ProhibitExtranet = true)]可以标识一个方法禁止与集群外部进行通信,通过网关也不会生成swagger文档。
  7. 应用接口方法生成的WebApi支持restful API风格。Lms支持通过方法的约定命名生成对应http方法请求的WebApi。您当然开发者也可以通过HttpMethodAttribute特性对某个方法进行注解。

一个典型的应用接口的定义

/// 
    /// 账号服务
    /// 
    [ServiceRoute]
    public interface IAccountAppService
    {
        /// 
        /// 新增账号
        /// 
        /// 账号信息
        /// 
        Task Create(CreateAccountInput input);

        /// 
        /// 通过账号名称获取账号
        /// 
        /// 账号名称
        /// 
        [GetCachingIntercept("Account:Name:{0}")]
        [HttpGet("{name:string}")]
        Task GetAccountByName([CacheKey(0)] string name);

        /// 
        /// 通过Id获取账号信息
        /// 
        /// 账号Id
        /// 
        [GetCachingIntercept("Account:Id:{0}")]
        [HttpGet("{id:long}")]
        Task GetAccountById([CacheKey(0)] long id);

        /// 
        /// 更新账号信息
        /// 
        /// 
        /// 
        [UpdateCachingIntercept( "Account:Id:{0}")]
        Task Update(UpdateAccountInput input);

        /// 
        /// 删除账号信息
        /// 
        /// 账号Id
        /// 
        [RemoveCachingIntercept("GetAccountOutput","Account:Id:{0}")]
        [HttpDelete("{id:long}")]
        Task Delete([CacheKey(0)]long id);

        /// 
        /// 订单扣款
        /// 
        /// 
        /// 
        [Governance(ProhibitExtranet = true)]
        [RemoveCachingIntercept("GetAccountOutput","Account:Id:{0}")]
        [Transaction]
        Task DeductBalance(DeductBalanceInput input);
    }

应用服务--应用接口的实现

  1. 应用服务层只需要引用应用服务接口层以及领域服务层,并实现应用接口相关的方法。
  2. 确保该微服务模块的主机引用了该模块的应用服务层,这样主机才能够托管该应用本身。
  3. 应用服务层可以通过引用其他微服务模块的应用接口层项目(或是安装nuget包,取决于开发团队的项目管理方法),与其他微服务模块进行rpc通信。
  4. 应用服务层需要依赖领域服务,通过调用领域服务的相关接口,实现该模块的核心业务逻辑。
  5. DTO到实体对象或是实体对DTO对象的映射关系可以在该层指定映射关系。

一个典型的应用服务的实现如下所示:

public class AccountAppService : IAccountAppService
    {
        private readonly IAccountDomainService _accountDomainService;

        public AccountAppService(IAccountDomainService accountDomainService)
        {
            _accountDomainService = accountDomainService;
        }

        public async Task Create(CreateAccountInput input)
        {
            var account = input.MapTo();
            account = await _accountDomainService.Create(account);
            return account.MapTo();
        }

        public async Task GetAccountByName(string name)
        {
            var account = await _accountDomainService.GetAccountByName(name);
            return account.MapTo();
        }

        public async Task GetAccountById(long id)
        {
            var account = await _accountDomainService.GetAccountById(id);
            return account.MapTo();
        }

        public async Task Update(UpdateAccountInput input)
        {
            var account = await _accountDomainService.Update(input);
            return account.MapTo();
        }

        public Task Delete(long id)
        {
            return _accountDomainService.Delete(id);
        }

        [TccTransaction(COnfirmMethod= "DeductBalanceConfirm", CancelMethod = "DeductBalanceCancel")]
        public async Task DeductBalance(DeductBalanceInput input)
        {
            var account = await _accountDomainService.GetAccountById(input.AccountId);
            if (input.OrderBalance > account.Balance)
            {
                throw new BusinessException("账号余额不足");
            }
            return await _accountDomainService.DeductBalance(input, TccMethodType.Try);
        }

        public Task DeductBalanceConfirm(DeductBalanceInput input)
        {
            return _accountDomainService.DeductBalance(input, TccMethodType.Confirm);
        }

        public Task DeductBalanceCancel(DeductBalanceInput input)
        {
            return _accountDomainService.DeductBalance(input, TccMethodType.Cancel);
        }
    }

领域层--微服务的核心业务实现

  1. 领域层是该微服务模块核心业务处理的模块,一般用于定于聚合根、实体、领域服务、仓储等业务对象。
  2. 领域层引用该微服务模块的应用接口层,方便使用dto对象。
  3. 领域层可以通过引用其他微服务模块的应用接口层项目(或是安装nuget包,取决于开发团队的项目管理方法),与其他微服务模块进行rpc通信。
  4. 领域服务必须要直接或间接继承ITransientDependency接口,这样,该领域服务才会被注入到ioc容器。
  5. lms.samples 项目使用TanvirArjel.EFCore.GenericRepository包实现数据的读写操作。

一个典型的领域服务的实现如下所示:

public class AccountDomainService : IAccountDomainService
    {
        private readonly IRepository _repository;
        private readonly IDistributedCache _accountCache;

        public AccountDomainService(IRepository repository,
            IDistributedCache accountCache)
        {
            _repository = repository;
            _accountCache = accountCache;
        }

        public async Task Create(Account account)
        {
            var exsitAccountCount = await _repository.GetCountAsync(p => p.Name == account.Name);
            if (exsitAccountCount > 0)
            {
                throw new BusinessException($"已经存在{account.Name}名称的账号");
            }

            exsitAccountCount = await _repository.GetCountAsync(p => p.Email == account.Email);
            if (exsitAccountCount > 0)
            {
                throw new BusinessException($"已经存在{account.Email}Email的账号");
            }

            await _repository.InsertAsync(account);
            return account;
        }

        public async Task GetAccountByName(string name)
        {
            var accountEntry = _repository.GetQueryable().FirstOrDefault(p => p.Name == name);
            if (accountEntry == null)
            {
                throw new BusinessException($"不存在名称为{name}的账号");
            }

            return accountEntry;
        }

        public async Task GetAccountById(long id)
        {
            var accountEntry = _repository.GetQueryable().FirstOrDefault(p => p.Id == id);
            if (accountEntry == null)
            {
                throw new BusinessException($"不存在Id为{id}的账号");
            }

            return accountEntry;
        }

        public async Task Update(UpdateAccountInput input)
        {
            var account = await GetAccountById(input.Id);
            if (!account.Email.Equals(input.Email))
            {
                var exsitAccountCount = await _repository.GetCountAsync(p => p.Email == input.Email);
                if (exsitAccountCount > 0)
                {
                    throw new BusinessException($"系统中已经存在Email为{input.Email}的账号");
                }
            }

            if (!account.Name.Equals(input.Name))
            {
                var exsitAccountCount = await _repository.GetCountAsync(p => p.Name == input.Name);
                if (exsitAccountCount > 0)
                {
                    throw new BusinessException($"系统中已经存在Name为{input.Name}的账号");
                }
            }

            await _accountCache.RemoveAsync($"Account:Name:{account.Name}");
            account = input.MapTo(account);
            await _repository.UpdateAsync(account);
            return account;
        }

        public async Task Delete(long id)
        {
            var account = await GetAccountById(id);
            await _accountCache.RemoveAsync($"Account:Name:{account.Name}");
            await _repository.DeleteAsync(account);
        }

        public async Task DeductBalance(DeductBalanceInput input, TccMethodType tccMethodType)
        {
            var account = await GetAccountById(input.AccountId);
            var trans = await _repository.BeginTransactionAsync();
            BalanceRecord balanceRecord = null;
            switch (tccMethodType)
            {
                case TccMethodType.Try:
                    account.Balance -= input.OrderBalance;
                    account.LockBalance += input.OrderBalance;
                    balanceRecord = new BalanceRecord()
                    {
                        OrderBalance = input.OrderBalance,
                        OrderId = input.OrderId,
                        PayStatus = PayStatus.NoPay
                    };
                    await _repository.InsertAsync(balanceRecord);
                    RpcContext.GetContext().SetAttachment("balanceRecordId",balanceRecord.Id);
                    break;
                case TccMethodType.Confirm:
                    account.LockBalance -= input.OrderBalance;
                    var balanceRecordId1 = RpcContext.GetContext().GetAttachment("orderBalanceId")?.To();
                    if (balanceRecordId1.HasValue)
                    {
                        balanceRecord = await _repository.GetByIdAsync(balanceRecordId1.Value);
                        balanceRecord.PayStatus = PayStatus.Payed;
                        await _repository.UpdateAsync(balanceRecord);
                    }
                    break;
                case TccMethodType.Cancel:
                    account.Balance += input.OrderBalance;
                    account.LockBalance -= input.OrderBalance;
                    var balanceRecordId2 = RpcContext.GetContext().GetAttachment("orderBalanceId")?.To();
                    if (balanceRecordId2.HasValue)
                    {
                        balanceRecord = await _repository.GetByIdAsync(balanceRecordId2.Value);
                        balanceRecord.PayStatus = PayStatus.Cancel;
                        await _repository.UpdateAsync(balanceRecord);
                    }
                    break;
            }

           
            await _repository.UpdateAsync(account);
            await trans.CommitAsync();
            await _accountCache.RemoveAsync($"Account:Name:{account.Name}");
            return balanceRecord?.Id;
        }
    }

数据访问(EntityFrameworkCore)--通过efcore实现数据读写

  • lms.samples项目使用orm框架efcore进行数据读写。
  • lms提供了IConfigureService,通过继承该接口即可使用IServiceCollection的实例指定数据上下文对象和注册仓库服务。
public class EfCoreConfigureService : IConfigureService
    {
        public void ConfigureServices(IServiceCollection services, IConfiguration configuration)
        {
            services.AddDbContext(opt =>
                    opt.UseMySql(configuration.GetConnectionString("Default"),
                        ServerVersion.AutoDetect(configuration.GetConnectionString("Default"))))
                .AddGenericRepository(ServiceLifetime.Transient)
                ;
        }

        public int Order { get; } = 1;
    }

3.主机项目需要显式的引用该项目,只有这样,该项目的ConfigureServices才会被调用。

4.数据迁移,请参考

应用启动与调试

获取源码

1.使用git 克隆lms项目源代码,lms.samples存放在samples目录下

# github
git clone https://github.com/liuhll/lms.git

# gitee
git clone https://gitee.com/liuhll2/lms.git

必要的前提

  1. 服务注册中心zookeeper
  2. 缓存服务redis
  3. mysql数据库

如果您电脑已经安装了docker以及docker-compose命令,那么您只需要进入samples\docker-compose\infrastr目录下,打开命令行工作,执行如下命令就可以自动安装zookeeperredismysql等服务:

docker-compose -f .\docker-compose.mysql.yml -f .\docker-compose.redis.yml -f .\docker-compose.zookeeper.yml up -d

数据库迁移

需要分别进入到各个微服务模块下的EntityFrameworkCore项目(例如:),执行如下命令:

dotnet ef database update

例如: 需要迁移account模块的数据库如下所示:

order模块和stock模块与account模块一致,在服务运行前都需要通过数据库迁移命令生成相关数据库。

  1. 数据库迁移指定数据库连接地址默认指定的是appsettings.Development.yml中配置的,您可以通过修改该配置文件中的connectionStrings.default配置项来指定自己的数据库服务地址。
  2. 如果没有dotnet ef命令,则需要通过dotnet tool install --global dotnet-ef安装ef工具,请[参考] (https://docs.microsoft.com/zh-cn/ef/core/get-started/overview/install)

以项目的方式启动和调试

使用visual studio作为开发工具

进入到samples目录下,使用visual studio打开lms.samples.sln解决方案,将项目设置为多启动项目,并将网关和各个模块的微服务主机设置为启动项目,如下图:

设置完成后直接启动即可。

使用rider作为开发工具进入到samples目录下,使用rider打开lms.samples.sln解决方案,打开各个微服务模块下的Properties/launchSettings.json,点击图中绿色的箭头即可启动项目。

启动网关项目后,可以看到应用接口的服务条目生成的swagger api文档 http://localhost:5000/swagger。

默认的环境变量为: Development,如果需要修改环境变量的话,可以通过Properties/launchSettings.json下的environmentVariables节点修改相关环境变量,请参考在 ASP.NET Core 中使用多个环境。

数据库连接、服务注册中心地址、以及redis缓存地址和分布式锁连接等配置项可以通过修改appsettings.Development.yml配置项自定义指定。

以docker-compose的方式启动和调试

进入到samples目录下,使用visual studio打开lms.samples.dockercompose.sln解决方案,将docker-compose设置为启动项目,即可启动和调式。

应用启动成功后,打开: http://127.0.0.1/swagger,即可看到swagger api文档

以docker-compose的方式启动和调试,则指定的环境变量为:ContainerDev

数据库连接、服务注册中心地址、以及redis缓存地址和分布式锁连接等配置项可以通过修改appsettings.ContainerDev.yml配置项自定义指定,配置的服务连接地址不允许为: 127.0.0.1或是localhost

测试和调式

服务启动成功后,您可以通过写入/api/account-post接口和/api/product-post新增账号和产品,然后通过/api/order-post接口进行测试和调式。

开源地址

github: https://github.com/liuhll/lms

gitee: https://gitee.com/liuhll2/lms

到此这篇关于通过lms.samples熟悉lms微服务框架的使用的文章就介绍到这了,更多相关lms微服务框架内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!


推荐阅读
  • 有意向可以发简历到邮箱内推.简历直达组内Leader.能做同事的话,内推奖励全给你. ... [详细]
  • 本文介绍了在开发Android新闻App时,搭建本地服务器的步骤。通过使用XAMPP软件,可以一键式搭建起开发环境,包括Apache、MySQL、PHP、PERL。在本地服务器上新建数据库和表,并设置相应的属性。最后,给出了创建new表的SQL语句。这个教程适合初学者参考。 ... [详细]
  • 在Docker中,将主机目录挂载到容器中作为volume使用时,常常会遇到文件权限问题。这是因为容器内外的UID不同所导致的。本文介绍了解决这个问题的方法,包括使用gosu和suexec工具以及在Dockerfile中配置volume的权限。通过这些方法,可以避免在使用Docker时出现无写权限的情况。 ... [详细]
  • 推荐一个ASP的内容管理框架(ASP Nuke)的优势和适用场景
    本文推荐了一个ASP的内容管理框架ASP Nuke,并介绍了其主要功能和特点。ASP Nuke支持文章新闻管理、投票、论坛等主要内容,并可以自定义模块。最新版本为0.8,虽然目前仍处于Alpha状态,但作者表示会继续更新完善。文章还分析了使用ASP的原因,包括ASP相对较小、易于部署和较简单等优势,适用于建立门户、网站的组织和小公司等场景。 ... [详细]
  • 在说Hibernate映射前,我们先来了解下对象关系映射ORM。ORM的实现思想就是将关系数据库中表的数据映射成对象,以对象的形式展现。这样开发人员就可以把对数据库的操作转化为对 ... [详细]
  • 本文介绍了Hyperledger Fabric外部链码构建与运行的相关知识,包括在Hyperledger Fabric 2.0版本之前链码构建和运行的困难性,外部构建模式的实现原理以及外部构建和运行API的使用方法。通过本文的介绍,读者可以了解到如何利用外部构建和运行的方式来实现链码的构建和运行,并且不再受限于特定的语言和部署环境。 ... [详细]
  • 本文详细介绍了在ASP.NET中获取插入记录的ID的几种方法,包括使用SCOPE_IDENTITY()和IDENT_CURRENT()函数,以及通过ExecuteReader方法执行SQL语句获取ID的步骤。同时,还提供了使用这些方法的示例代码和注意事项。对于需要获取表中最后一个插入操作所产生的ID或马上使用刚插入的新记录ID的开发者来说,本文提供了一些有用的技巧和建议。 ... [详细]
  • DockerDataCenter系列(四)-离线安装UCP和DTR,Go语言社区,Golang程序员人脉社 ... [详细]
  • 随着我司的应用都开始容器化,相应的ETL流程也需要迁移到容器中。常规的SQL和shell脚本迁移之后执行基本没有问题,主要的问题在于数据接入使用kettle的场景下,kettle启 ... [详细]
  • mysql自动打开文件_让docker中的mysql启动时自动执行sql文件
    本文提要本文目的不仅仅是创建一个MySQL的镜像,而是在其基础上再实现启动过程中自动导入数据及数据库用户的权限设置,并且在新创建出来的容器里自动启动My ... [详细]
  • 本文主要介绍关于linux文件描述符设置,centos7设置文件句柄数,centos7查看进程数的知识点,对【Linux之进程数和句柄数】和【linux句柄数含义】有兴趣的朋友可以看下由【东城绝神】投 ... [详细]
  • java布尔字段用is前缀_POJO类中布尔类型的变量都不要加is前缀详解
    前言对应阿里巴巴开发手册第一章的命名风格的第八条。【强制】POJO类中布尔类型的变量都不要加is前缀,否则部分框架解析会引起序列化错误。反例:定义为基本 ... [详细]
  • SpringMVC接收请求参数的方式总结
    本文总结了在SpringMVC开发中处理控制器参数的各种方式,包括处理使用@RequestParam注解的参数、MultipartFile类型参数和Simple类型参数的RequestParamMethodArgumentResolver,处理@RequestBody注解的参数的RequestResponseBodyMethodProcessor,以及PathVariableMapMethodArgumentResol等子类。 ... [详细]
  • 在Android中解析Gson解析json数据是很方便快捷的,可以直接将json数据解析成java对象或者集合。使用Gson解析json成对象时,默认将json里对应字段的值解析到java对象里对应字段的属性里面。然而,当我们自己定义的java对象里的属性名与json里的字段名不一样时,我们可以使用@SerializedName注解来将对象里的属性跟json里字段对应值匹配起来。本文介绍了使用@SerializedName注解解析json数据的方法,并给出了具体的使用示例。 ... [详细]
  • mapreduce源码分析总结
    这篇文章总结的非常到位,故而转之一MapReduce概述MapReduce是一个用于大规模数据处理的分布式计算模型,它最初是由Google工程师设计并实现的ÿ ... [详细]
author-avatar
PearlLisa_Shanghai_901
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有