yii2 fecshop 服务组件的详细介绍

yii2 fecshop 服务组件的详细介绍

在  vendor/fancyecommerce/fecshop/services路径下就是所有的fecshop组件,组件分为组件和子组件,组件本质上是yii2的组件,子组件的原理是组件的属性指向子组件对象,譬如Yii::$app->page是组件,Yii::$app->page->theme是子组件,组件的配置是在 vendor/fancyecommerce/fecshop/config/services里面,下面是一个配置组件和子组件的例子:

<?php
/**
 * FecShop file.
 *
 * @link http://www.fecshop.com/
 * @copyright Copyright (c) 2016 FecShop Software LLC
 * @license http://www.fecshop.com/license/
 */
return [
  'page' => [
    'class' => 'fecshop\services\Page',
    //'terry' => 'xxxx',
    
    # 子服务
    'childService' => [
      'breadcrumbs' => [
        'class' 		=> 'fecshop\services\page\Breadcrumbs',
        'homeName' 		=> 'Home',  # if homeName => '', Home will not show in breadcrums.
        'ifAddHomeUrl'	=> true,  	# default true, if set false, home will not add url (a).
        //'intervalSymbol'=> ' >> '	# default value:' > '
      ],
      
      'cms' => [
        'class' 		=> 'fecshop\services\page\Cms',
      ],
      'theme' => [
        'class' 		=> 'fecshop\services\page\Theme',
      ],
      'widget' => [
        'class' 		=> 'fecshop\services\page\Widget',
        # 定义默认方法,也就是widgetConfig 里面各个部件里面的method如果没有填写
        # 则使用该配置。
        # 'defaultObMethod' => 'getLastData',
        
        'widgetConfig' => [
          'menu' =>[
            # 必填
            'class' => 'fec\block\TestMenu',
            # view 的绝对路径配置方式
            'view'  => '@fec/views/testmenu/index.php',
            # 下面为选填
            'method'=> 'getLastData',
            'terry1'=> 'My1',
            'terry2'=> 'My2',
          ],
          'love' => [
            'class' => 'fecshop\app\appfront\modules\Cms\block\widget\Test',
            # 根据多模板的优先级,依次去模板找查找该文件,直到找到这个文件。
            'view'  => 'cms/home/test.php',
            'terry' => 'II',
          ]
        ]
        
      ],
      'currency' => [
        'class' => 'fecshop\services\page\Currency',
        'currencys' => [
          'USD' => [
            'rate' 		=> 1,
            'symbol' 	=> '$',
          ],
          'RMB' => [
            'rate' 		=> 6.3,
            'symbol' 	=> '¥',
          ],
        ],
        //'defaultCurrency' => 'USD',
      ],
      
      'footer' => [
        'class' 		=> 'fecshop\services\page\Footer',
       
      ],
      
      
      'newsletter' => [
        'class' 		=> 'fecshop\services\page\Newsletter',
      ],
      
      'staticblock' => [
        'class' 		=> 'fecshop\services\page\StaticBlock',
      ],
      
      'menu' => [
        'class' => 'fecshop\services\page\Menu',
        'displayHome' => [
          'enable' => true,
          'display'=> 'Home',
        ],
        /**
         *	custom menu  in the front menu section.
         */
        'frontCustomMenu' => [
          [
            'name' 		=> 'my custom menu',
            'urlPath'	=> '/my-custom-menu.html',
            'childMenu' => [
              [
                'name' 		=> 'my custom menu 2',
                'urlPath'	=> '/my-custom-menu-2.html',
              ],
              [
                'name' 		=> 'my custom menu 2',
                'urlPath'	=> '/my-custom-menu-2.html',
                'childMenu' => [
                  [
                    'name' 		=> 'my custom menu 2',
                    'urlPath'	=> '/my-custom-menu-2.html',
                  ],
                  [
                    'name' 		=> 'my custom menu 2',
                    'urlPath'	=> '/my-custom-menu-2.html',
                  ],
                ],	
              ],
            ],	
          ],
          [
            'name' 		=> 'my custom menu 2',
            'urlPath'	=> '/my-custom-menu-2.html',
          ],
        ],
        /**
         *	custom menu  behind the menu section.
         */
        'behindCustomMenu' => [
          [
            'name' 		=> 'my behind custom menu',
            'urlPath'	=> '/my-behind-custom-menu.html',
          ],
          [
            'name' 		=> 'my behindcustom menu 2',
            'urlPath'	=> '/my-behind-custom-menu-2.html',
          ],
        ],
      ],
      
    ],
  ],
];

上面是page组件的配置,childService里面是page组件的子组件配置,配置方法和组件类似。

在fecshop的底层,组件作为整个开源商城服务的支撑,组件服务为上层的modules提供数据,为各个应用提供数据,有点类似SOA架构的服务,

下面是目前的服务的列表:

Affiliate.php: 网站联盟组件服务

Blog.php:博客服务组件

Cart.php:购物车服务组件

Category.php:分类服务组件

Coupon.php:优惠券服务组件

Customer.php:用户账号服务组件

——–Customer子组件 – 地址:Address

——–Customer子组件 – 网盟:Affiliate

——–Customer子组件 – 优惠券:Coupon

——–Customer子组件 – 线下分销:DropShip

——–Customer子组件 – 收藏:Favorite

——–Customer子组件 – 面包屑导航:Breadcrumbs

——–Customer子组件 – 消息:Message

——–Customer子组件 – 订单:Order

——–Customer子组件 – 积分:Point

——–Customer子组件 – 评论:Review

——–Customer子组件 – 批发:Wholesale

Email.php:邮件服务组件

Order.php:订单服务组件

Page.php:页面等服务组件

——–Page子组件 – 面包屑导航:Breadcrumbs

——–Page子组件 – cms Page:cms

——–Page子组件 – 货币:Currency

——–Page子组件 – 页面底部:Footer

——–Page子组件 – 菜单:menu

——–Page子组件 – 邮件订阅:newsletter

——–Page子组件 – 静态块:Static Block

——–Page子组件 – 模板配置功能:theme

——–Page子组件 – 小部件:Widget

Payment.php:支付服务组件

point.php:积分服务组件

Product.php:产品服务组件

——–Product子组件 – 产品访问历史记录:ViewLog

————————–ViewLog子组件 – Db.php:Mysql 存储

————————–ViewLog子组件 – Mongodb.php:Mongodb 存储

————————–ViewLog子组件 – Session.php:Session 存储

——–Product子组件 – 热卖产品:BestSell

——–Product子组件 -买了的还买了:BuyAlsoBuy

——–Product子组件 – 产品分类:Category

——–Product子组件 – 产品集合:Coll

——–Product子组件 – 产品图片:Image

——–Product子组件 – 产品详细:Info

——–Product子组件 – 产品价格:Price

——–Product子组件 – 相关产品:Relate

——–Product子组件 – 产品评论:Review

——–Product子组件 -看了的还看了:ViewAlsoView

Request.php:Request服务组件

Search.php:搜索功能服务组件

Shipping:货运物流服务组件

Sitemap.php:Sitemap服务组件

Store.php:Store服务组件

Url.php:Url服务组件

Wholesale.php:批发功能服务组件

 

 

 

 

 

 

 

yii2 fecshop 的扩展开发

  1. 如何做一个基于composer的插件,来扩展fecshop。
  2. 如何通过yii2插件或者二次开发修改 重写yii2的功能
  3. 如何通过插件或者二次开发修改 重写fecshop service
    1. service列表
  4. 如何通过插件或者二次开发修改 重写fecshop module
    1. fecshop module 列表
  5. 如何通过插件或者二次开发修改fecshop module controller
    1. controller
  6. 如何做fecshop的模板?

上面是一些列表,找时间做深度解答。

yii2 fecshop 功能列表

fecshop的功能列表:

  1. 多入口机制,每一个入口添加一个域名,独立分开,譬
    1. demo.appadmin.fecshop.com  解析到 appadmin 应用.
    2. demo.appfront.fecshop.com    解析到 appfront   应用.
    3. demo.apphtml5.fecshop.com  解析到 apphtml5  应用.
    4. demo.appserver.fecshop.com 解析到 appserver 应用.
  2. 单入口单index.php 多域名,可以把多个域名解析到同一个入口,譬如为了实现多语言,我们把  en.appfront.fecshop , fr.appfront.fecshop , de.appfront.fecshop 都解析到appfront应用入口,然后通过不同的域名,加载不同的配置语言,和初始默认货币等,进而实现多语言。
  3. 单入口 多文件夹index.php 单域名,譬如将  www.fecshop.com解析到  appfront/web/index.php,然后新建
    1. appfront/web/fr/index.php
    2. appfront/web/es/index.php
    3. appfront/web/de/index.php

然后访问  www/fecshop.com   ,www/fecshop.com/fr ,www/fecshop.com/es ,www/fecshop.com/de,进行语言的切换。

4. 多语言  根据不同的store,加载不同的语言,也可以进行语言的切换

5.多货币,根据不同的store,加载不同的货币,也可以进行货币的切换

6.产品属性可配置化。分类选择一个属性组,用来做分类侧栏属性过滤 ,产品选择一个属性组,用来做属性的添加。最终在这个分类做属性过滤后,就会出现对应的属性产品。

7.组件化服务可配置,可重写

8.整体系统配置化,可以在不动fecshop源代码的情况下,修改任意功能。

9.多模板系统,通过各个模板的优先级顺序,加载view文件,如果在高级别的模板中文件不存在则依次到优先级低的模板中查找,直到找到view文件,  通过这种方式,高级别的模板如果想要重写低级别模板的某个文件,只要把这个文件复制到当前模板,路径对应好即可。

 

yii2 fecshop 框架特性

框架特性:

按照程序的执行顺序,大致分为4个层次:

1.app应用入口:首先通过nginx解析到appfront,appadmin,apphtml5等app层入口index文件,然后由index.php加载各个配置传递给application,这一层做的事情是配置的收集,组合,最终生成一个大的config数据传递给yii2 application,每一个app应用的配置文件是不同的,进而后面的各个层的执行都会不一样。

2.yii2初始化层,在第一步骤的config数组传递给yii2 application,就开始了yii2的初始化层,一直到执行conroller之前,也就是在做调度之前,都是yii2的初始化层,yii2系统执行的任务,设立涉及到很多对象的初始化,有yii2系统的,也有第三方添加的bootstrap部分,其中组件,模块都是可以添加bootstrap的,这一步骤发生在执行controller之前的初始化,也就是无论执行那个controller,都必须执行的部分。(注意,这里的bootstrap指的是yii2的初始化,而不是twitter 出的css框架 bootstrap)

3.Module层:在yii2初始化完成后,执行的就是模块层,这一层,里面有controller,block,view三层结构,controller仅仅负责调度,block负责解析前台传递的数据,调用相应的组件服务,产生相应的结果,对于content部分,是由controller接收,传递到view,也有直接使用block和view生成的html部分代码,

对于controller层和view层,都不会陌生,block层是我新加入的一个层,也就是把controller中对逻辑的处理,全部放到block层来完成,block像是肌肉层,负责连接,连接前台传递的数据,调用相应的组件服务,返回数据给controller,当然block也是某个小部件的数据提供者(独立功能块)。

view接收数据,view分为3部分,layout部分和view content部分,这个是yii2本身有的,另外还有通过render函数生成的独立块,独立块由block和view文件组成,将生成的html可以通过配置的方式加到很多页面,譬如产品访问记录块,在产品页面的侧栏,分类页面的侧栏可能都要显示,那么可以做成块的方式调用即可。

这一层是加入缓存最多的一层,甚至是整页缓存(full page cache),动态数据用ajax动态异步加载。

总之,module是一个大的调度层和数据显示层,本身不涉及到复杂的数据处理,负责的数据处理都是在组件层完成,在module层是不允许访问model层的,大致的流程为:解析前端请求,到后端的组件服务层提取数据,组织数据,结果传递给view,然后画出来整个页面。

4.组件服务层:在model层之上的,就是组件服务层,这一层面提供各种服务,服务的粒子大致为描述性粒度,譬如产品加入购物车,查看购物车的产品,生 成订单,删除购物车产品等,这些功能的操作都是通过组件的一个函数直接返回,有点类似于微服务结构里面的各个服务,不过fecshop的组件服务层是有状态的,另外,该层和上面的各个层是放到一个主机上面的,因为是直接通过组件服务进行调用方法,所以不可以分割到不同的主机。通俗来讲,组件服务层提供的是 一个描述性的操作粒度,为了快速响应,根据需求,这一层可能被加入缓存功能,总之,这一层面的操作,会涉及到多个model的 操作,最终成为一个描述性粒度的功能。

在举个例子:譬如产品加入购物车,假设我们要扣除库存,那么对应着操作:

当前用户是否登录,如果登录则…

验证传递的产品参数是否存在有效(前端传递数据的有效性),然后….

将产品数据加入到cart表中…

扣除库存…

等等,上面这一系列的操作,会涉及到很多表的查询和更新插入等操作,但是我们描述的粒度为  产品加入购物车,在数据库端要执行的所有操作,都是在一个描述粒度里面,后期可能加入很多其他方面的扩展,譬如判断是否有优惠券,是否使用积分等等,都会添加到这个粒度里面,但是对访问服务组件层的module来说,是透明的,无论服务层如何更改添加新功能,module还是按照之前的方式调用(当然有可能多传递一些参数,甚至返回的数据格式有变化)。

5.Model层:数据库操作层,分为mysql和mongodb,访问数据库层,所做的事情有:数据的查找,更新和插入时的数据验证,保存,数据的删除,以及其他一部分基于数据操作的函数等,这个层面提供的功能粒度比较小,是基于表操作的粒度,module不会添加事务,事务的控制在服务层添加。由组件服务层来组织。

OK,fecshop的框架大致如此。

 

 

 

yii2 fecshop的初衷

以下为做fecshop的初衷和fecshop的简介

1.关于对magento的情感

自从2010年进入外贸电商,就开始玩magento,magento一直以来,以seo友好,容易收录,功能强大,可配置性强,灵活度大,支持度高等各个方面完胜其他开源框架,基于xml的配置和基于EAV数据表模型,让magento的插件和模板异常的丰富,magento的插件,基本上可以在不修改源文件的前提,修改任意magento代码,当然,这些都是有代价的,magento的初始化必须加载完所有的xml文件才能进行解析controller,初始化过于笨重,自身的框架过于雍容,在速度,并发方面一直处于劣势,尤其是EAV模型的数据库模型,让magento在产品数据增多的情况下,一旦超过10000个产品后,性能就会带来一定的下降,当产品超过3,4万,性能明显下降,对mysql的内存消耗非常大,当插件增多的情况下,php端对xml解析对服务器的资源吃的也比较厉害。另外在产品很多,store很多的情况下,indexer索引也是硬伤,关于magneto的一些问题,我在知乎上面回答了一个问题:https://www.zhihu.com/question/19813425#zh-question-answer-wrap

总之,magento的灵活度高,解决了自身版本升级,第三方插件升级,和用户二次开发之间冲突的问题,通过配置的方式来协调使用那一块文件代码,magento除了自身功能完善,还有大量的第三方插件和模板,让magento这个闭环更加的完善,但是magento的并发性能较差,日ip超过2万的情况下,没有varnish,2台独立主机都很难平稳的抗住,另外,重构magento的某个底层也是比较困难,这些都是magento的不足。

2.对yii2的情感。

从2014年接触yii2,用yii2做了3个项目,一个电商网站,一个erp系统,一个业务数据分析系统,对yii2框架的认知加深后,越来越喜欢这个php框架,在架构方面非常的优秀,全程基于配置,用户可以在不修改yii2文件代码的前提下修改和扩展yii2框架的功能,单例模式的组件,等基于控制反转的思想设计而成,还有model的role验证,命令行模式的批处理console,类似于一个小系统的模块,多入口应用,可以新建一堆类似frontend,backend这样的入口应用,安全性高,可以通过bootstrap配置加入初始化代码,数据库升级migrate等,yii2是一个非常适合做产品的框架。

3.fecshop的想法

作为一个电商系统产品,我认为有两个大问题比较关键:

3.1 产品自身功能升级,和第三方插件扩展升级,和用户自身二次开发的矛盾的解决, 总体来说就是:各自在各自的文件系统中开发,通过配置的方式覆盖,yii2的配置是基于数据,而不是magento那类笨笨的xml,资源消耗小

3.2 对底层功能的重构和选择,对于一个底层的功能,譬如购物车,我前期用的是基于session,后期我发现不行,我需要吧购物车数据放到mysql中,后来我又想放到redis中(假设),如果我是标注的mvc模式,重构将非常的困难,因为对购物车的操作散步在各个地方,不知道那个地方会漏掉,因此,我们需要更改我们的模式,我想到的是用组件的形式提供服务,各个模块不允许调用数据库,只能通过对服务的调用来完成功能,如果我想重构某个服务,我只需要把这个服务组件里面所有的public方法实现即可,数据的流向为: model –>service component –>module->block->view

这样,我甚至可以对同一个功能做多个服务组件,譬如购物车,我做了session cart,mysql cart让用户选择。

糅合yii2的新型思维,mongodb的多维数组,mysql的事务性,mongodb的强大配置和灵活度,我想基于mysql和mongodb数据库,用yii2框架,参膜magento的灵活,实现一套电商开源系统,我给予的名字为fancy ecommerce shop,简称fecshop。

yii2 fecshop 功能特性添加

fecshop,全程: Fancy E-Commerce Shop

今天进一步整理了fecshop的需求。

一:view部分

在yii2的view中,大致分两部分,一个是layout.php文件,先通过render画出这个文件的html,然后具体的view文件在通过render函数画出来,内容放到layout.php文件的$content中替代。

对于view部分的路径查找,一共有两个部分,一个是所在的namespace,也就是绝对根目录,另外一个就是相对文件路径,这两个拼出来view文件和layout.php文件的路径。

1.对于决定根目录,这个是由模板决定,优先级最高的是当前的   模板包/模板名称  路径,然后是中间层模板路径,这个是第三方插件在初始化的时候设置的,最后是fecshop的模板路径,fecshop的模板路径是优先级最低的,但是是文件最全的,也就是说,如果当前的模板想修改fecshop的某个view,可以在当前模板下面把这个文件复制过来,然后修改。

2.对于layout.php文件,首先由全体设定,也就是yii2整体的application中读取,如果module设置了,那么以module为准,如果controller设置了,那么以controller为准,也就是层层覆盖,优先级最高的是controller,然后是module,然后是全体设置,然后得出当前的layout文件的相对路径

3.现在我们有了多个模板的根路径,以及优先级,和view,layout的相对路径,那么我们先到当前模板下面找view文件和layout文件,如果不存在,那么我们去第三方模板或者插件的路径下面找,如果还找不到,我们就去fecshop的文件路径下面找,如果还找不到就报错,

4.到这里,我们的多模板系统的优先级找文件,和各个页面的layout文件的设置就完成了,这里可以让第三方和当前模板通过覆盖的方式,重写fecshop的任何一个view文件。

5.对于第三方的模板或者插件可能有多个,可以通过全局配置的方式添加,譬如模板,也可以通过某个controller文件下面动态设置,譬如插件只对某个页面进行了view的修改,

6.目前想的是通过Yii::$app->page->theme进行,  theme->currentTheme设置当前的模板,譬如:   [ ‘name’ => ‘terry/theme01’,’path’=>’@app/theme’]。

当前模板路径是为了给用户二次开发的。

theme->middleTheme = [

[ ‘name’ => ‘fec/theme01’,’path’=>’@vendor/fancyecommerce/fecshoptheme’],

[ ‘name’ => ‘mage/theme01’,’path’=>’@app/fancyecommerce/fecshoptheme’],

],

前面的优先级最高。如果您有多个模板,请不要一定都设置,这样叠加后可能会出问题,

对于插件,一般在某几个controller中做view的更改,我们建议您通过动态加载的方式,在controller中设置middleTheme的值,进行更高。

theme->baseTheme ,这个是固定值,由fecshop决定。这个是最基础最全面的theme。

7.重写yii\web\controller 的render方法,在生成view路径前,先通过优先级去找view文件,找到后返回,然后画出来页面。

总之,通过配置的方式,各个模板进行优先级的查找view文件,找到后就加载之。

二:第三方开发,插件和模板

1.配置,配置通过在index.php加载的方式加载插件或者模板的配置,通过顺序进行优先级的调整,顺序为:common的配置,fecshop的配置,第三方插件和模板的配置,本地local的配置,其中后面的会覆盖前面的,

2.扩展可以重写yii2框架的文件,可以重写fecshop的services,modules,module controller,block,view文件等等,也就是说可以重写所有的fecshop和 yii2框架的文件,对于重写,后面的优先级最高,如果对同一个service进行重写,在入口文件中,配置在最后面和前面的取他们的并集,如果某个key的value都存在,则会覆盖。

3.对于配置型数据和页面   cms 和statici block

要可以加入配置变量,可以配置布局文件layout,可以加入配置块等

4.review 通过配置的方式决定是全语言显示还是单语言显示,全语言显示,标注store

5.对于同一个服务,可能后期会重构另外一种,那么通过扩展的方式让用户自己选择加载,譬如购物车,订单,优惠券,积分,网站联盟,要改成分库分表的方式,则通过一个插件,通过重构service的方式,来重构,上面的modules则不必更改

6.为了尽量的避免冲突,view文件要尽可能的碎小。

7.多语言采用,翻译文件,数据库数据,配置数据来实现多语言。

8.controller,集成于fecshop的controller,然后fecshop controller集成yii\web\controller. 这样后期可以加入统计等一些功能,然后通过队列的方式,进行一些统计的事情等等,方便后期扩展。

9.对于ajax,后期是否考虑开启不同的入口,譬如在app/web下面新建一个文件夹ajax

app/web/ajax/index.php,这个文件中只有很少的加载文件,更快的速度执行,

10.对于货币,基于session或者cookie,没有url的改变

11.产品购物车通过配置的方式决定跳转还是不跳转

12.类似jd的购物车产品选择下单的方式

13.登录和游客下面的功能的开启和关闭

14,积分分为可用和不可用积分。

15.思考以后分库分表的变迁,如何解决。这些的解决方式,是通过扩展的方式重构service的方式,而不是修改fecshop的代码的方式,这样让客户根据自己的需要,选定不同的插件,根据公司的方式状况进行不同的重构方式。

16.网站联盟affiliate,积分points,团购,线下分销dropship,批发wholesale等等后续的功能,可以通过插件的方式,也可以在fecshop升级的方式加载。来让fecshop的功能越来越强大。

yii2 在域名后面加一个路径作为首页

对于多语言网站,我们可以用 en.fecshop.com,fr.fecshop.com,es.fecshop.com, 这种子域名的方式表现做多语言
也可以用 www.fecshop.com , www.fecshop.com/fr , www.fecshop.com/es , 这种方式做多语言
yii2对第一种支持还是不错,对于第二种支持不好,我们搞一种自己的方式来支持这种。

1.在app/web/下面新建文件夹

app/web/fr

app/web/es

在上面的文件夹下面新建index.php文件,assets文件夹设置可写。

/app/web/fr/index.php

1.nginx设置:

在nginx的server配置中加入:

location /fr/ {
    index index.php;
    if (!-e $request_filename){
      rewrite . /fr/index.php last;
    }
    }

 
2.入口/app/web/fr/index.php文件开始加入:

$secure = 0;
$http = $secure ? 'https' : 'http';
$homeUrl = $http.'://'.$_SERVER['HTTP_HOST'].rtrim(dirname($_SERVER['SCRIPT_NAME']), '\\/');

 

$application = new yii\web\Application($config);
的上面加入:

$config['homeUrl'] = $homeUrl;

加完之后的样子:

<?php
//$secure = isset($_SERVER['HTTPS']) && (strcasecmp($_SERVER['HTTPS'], 'on') === 0 || $_SERVER['HTTPS'] == 1) || isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && strcasecmp($_SERVER['HTTP_X_FORWARDED_PROTO'], 'https') === 0;
$secure = 0;
$http = $secure ? 'https' : 'http';
$homeUrl = $http.'://'.$_SERVER['HTTP_HOST'].rtrim(dirname($_SERVER['SCRIPT_NAME']), '\\/');

defined('YII_DEBUG') or define('YII_DEBUG', true);
defined('YII_ENV') or define('YII_ENV', 'dev');
require(__DIR__ . '/../../../vendor/autoload.php');
require(__DIR__ . '/../../../vendor/yiisoft/yii2/Yii.php');
require(__DIR__ . '/../../../common/config/bootstrap.php');
require(__DIR__ . '/../../config/bootstrap.php');

$config = yii\helpers\ArrayHelper::merge(
    require(__DIR__ . '/../../../common/config/main.php'),
    require(__DIR__ . '/../../../common/config/main-local.php'),
    require(__DIR__ . '/../../config/main.php'),
  require(__DIR__ . '/../../config/main-local.php')
    

);
$config['homeUrl'] = $homeUrl;
$application = new yii\web\Application($config);
$application->run();

3.添加store组件:

'store' => [
    'class' => 'fecshop\services\Store',
    'stores' => [
      'fecshop.appadmin.fancyecommerce.com/fr' => [
        
        'language' 		=> 'fr',
        'themePackage'	=> 'default',
        'theme'	=> 'default',
        'currency' => 'USD',
      ],
      'fecshop.appadmin.fancyecommerce.com/es' => [
        'language' 		=> 'es',
        'themePackage'	=> 'default',
        'theme'	=> 'default',
        'currency' => 'USD',
      ],
       'fecshop.appadmin.fancyecommerce.com' => [
        'language'  => 'en',
        'themePackage' => 'default',
        'theme' => 'default',
        'currency' => 'USD',
      ],
    ],
    'languages' => [
      //'en','fr','it','de','es','nl','pt','ru',
    ],
  ],

组件:

<?php
/**
 * FecShop file.
 *
 * @link http://www.fecshop.com/
 * @copyright Copyright (c) 2016 FecShop Software LLC
 * @license http://www.fecshop.com/license/
 */
namespace fecshop\services;
use Yii;
use yii\base\InvalidValueException;
use yii\base\InvalidConfigException;
use yii\base\BootstrapInterface;
/**
 * @author Terry Zhao <2358269014@qq.com>
 * @since 1.0
 */
class Store extends Service implements BootstrapInterface
{
  /**
   * init by config file.
   * all stores config . include : domain,language,theme,themePackage
   */
  public $stores; 
  /**
   * init by config file.
   * all store Language.
   */	
  public $languages;
  
  /**
   * current store language
   */
  public $currentLanguage = 'en';
  
  /**
   * current store theme package
   */
  public $currentThemePackage = 'default';
  /**
   * current store theme
   */
  public $currentTheme = 'default';
  /**
   * current store name , this property will  init value with domain.
   */
  public $currentStore;
  
  
  /**
   *	Bootstrap:init website,  class property $currentLanguage ,$currentTheme and $currentStore.
   *  if you not config this ,default class property will be set.
   *  if current domain is not config , InvalidValueException will be throw. 
   *	class property $currentStore will be set value $domain.
   */
  public function bootstrap($app){
    $host = explode('://' ,$app->getHomeUrl());
    $stores = $this->stores;
    $init_compelte = 0;
    if(is_array($stores) && !empty($stores)){
      foreach($stores as $domain => $lang){
        if($host[1] == $domain){
          Yii::$app->store->currentStore = $domain;
          if(isset($lang['language']) && !empty($lang['language'])){
            Yii::$app->store->currentLanguage = $lang['language'];
          }
          if(isset($lang['theme']) && !empty($lang['theme'])){
            Yii::$app->store->currentTheme = $lang['theme'];
          }
          if(isset($lang['themePackage']) && !empty($lang['themePackage'])){
            Yii::$app->store->currentThemePackage = $lang['themePackage'];
          }
          /**
           * init store currency.
           */
          if(isset($lang['currency']) && !empty($lang['currency'])){
            $currency = $lang['currency'];
          }else{
            $currency = '';
          }
          
          Yii::$app->page->currency->initCurrency($currency);
          /**
           * current domian is config is store config.
           */
          $init_compelte = 1;
        }
      }
    }
    if(!$init_compelte){
      throw new InvalidValueException('this domain is not config in store component');
    }
    
    }
  
  /**
   * if a object or array  attribute is a store attribute, you can get current 
   * language value by this function.
   */
  public function getLangVal($attr,$attrName){
    return $attr[$this->currentLanguage."_".$attrName];
  }
  
  
  public function getAllLanguage(){
    
    
  }
  
  
}

上面通过相应的域名设置不同的语言,货币,模板等。

4.yii的yii\helpers\Url已经对我们不适合,我们需要自己写一个url的生成:

<?php
namespace fec\helpers;
use Yii; 
class CUrl
{
  public static $_baseHttpUrl;
  public static $_baseHttpsUrl;
  # 1.获取首页地址。
  public static function getHomeUrl(){
    return Yii::$app->getHomeUrl();
    //return Yii::$app->getBaseUrl(true);
  }
  # 2. 获取首页地址。同上
  public static function getBaseUrl($isHttps=false){
    if($isHttps){
      if(!self::$_baseHttpsUrl){
        self::$_baseHttpsUrl = str_replace('http','https',self::getHomeUrl());
      }
      return self::$_baseHttpsUrl;
    }else{
      if(!self::$_baseHttpUrl){
        self::$_baseHttpUrl = str_replace('https','http',self::getHomeUrl());
      }
      return self::$_baseHttpUrl;
    }
  }
  
  # 3.立即跳转  和 yii2的跳转还是不同
  public static function redirect($url,$isHttps=false){
    if($url){
      if(substr($url,0,4) != "http"){
        $url = self::getUrl($url,[],$isHttps);	
      }
      header("Location: $url");
      exit;
    }
  }
  
  # 4.通过模板name,得到对应文件路径。
  # 默认是 domain.com/skin/theme/下面的绝对URL
  public static function getSkinUrl($dir = '',$relative_path=false){
    $currentTheme = CConfig::getCurrentTheme();
    $url = '';
    if(!$relative_path){
      $url = self::getHomeUrl(). DIRECTORY_SEPARATOR;
    }
    return  $url.'skin'.DIRECTORY_SEPARATOR
        .$currentTheme.DIRECTORY_SEPARATOR
        .$dir;
  }
  
  #5. 通过url path 和参数  得到当前网站下的完整url路径。
  public static function getUrl($url_path,$params=array(),$isHttps=false){
    $url_path = trim($url_path,DIRECTORY_SEPARATOR);
    $url =  self::getBaseUrl($isHttps). DIRECTORY_SEPARATOR .$url_path;
    $str = "";
    if(!empty($params) && is_array($params)){
      $str .= "?";
      foreach($params as $k=>$v){
        $str .= $k."=".$v."&";
      }
      $str = substr($str,0,strlen($str)-1);
    }
    return $url.$str;
  } 
  
  # 6.得到当前的完整url
  public static function getCurrentUrl(){
    //$s =  self::getHomeUrl();
    //return $s.$_SERVER["REQUEST_URI"];
    return \yii\helpers\Url::current();
  }
  # 7.得到当前的完整url  no param
  public static function getCurrentUrlNoParam(){
    $url = self::getCurrentUrl();
    if(strstr($url,"#")){
      $url = substr($url,0,strpos($url,"#"));
    }
    
    if(strstr($url,"?")){
      $url = substr($url,0,strpos($url,"?"));
    }
    return $url;
    
  }
  
  # 8、得到url key   ,譬如  http://www.x.com/ss/dd/aa?aaaa=ddddd   返回 /ss/dd/aa
  public static function getUrlKey(){
    
    return Yii::$app->request->getPathInfo();
  }
  # 9.得到url    ,譬如  http://www.x.com/ss/dd/aa?aaaa=ddddd   返回 /ss/dd/aa?aaaa=ddddd   
  public static function getUrlKeyWithParam(){
    return Yii::$app->getRequest()->url;
  }
  
}

 

得到url:

CUrl::getUrl('/x/x/x',['p'=>'2]);

#将生成

http://fecshop.appadmin.fancyecommerce.com/fr/x/x/x?p=2

CUrl::getUrl('/x/x/x',['p'=>'2],rue);

#将生成

https://fecshop.appadmin.fancyecommerce.com/fr/x/x/x?p=2

OK,到这里就完成整个过程了。

 

 

 

 

 

 

Redis 分区实现原理

摘要

Redis Partitioning即Redis分区,简单的说就是将数据分布到不同的redis实例中,因此对于每个redis实例所存储的内容仅仅是所有内容的一个子集。分区(Partitioning)不仅仅是Redis中的概念,几乎是所有数据存储系统都会涉及到的概念,这篇文章将会在理解分区基本概念的基础之上进一步了解Redis对分区的支持。

我们为什么要分区

我们为什么要分区?分区的动机是什么?通常来说,Redis分区的好处大致有如下两个方面:

  1. 性能的提升,单机Redis的网络I/O能力和计算资源是有限的,将请求分散到多台机器,充分利用多台机器的计算能力可网络带宽,有助于提高Redis总体的服务能力。
  2. 存储的横向扩展,即使Redis的服务能力能够满足应用需求,但是随着存储数据的增加,单台机器受限于机器本身的存储容量,将数据分散到多台机器上存储使得Redis服务可以横向扩展。

总的来说,分区使得我们本来受限于单台计算机硬件资源的问题不再是问题,存储不够?计算资源不够?带宽不够?我们都可以通过增加机器来解决这些问题。

Redis分区基础

实际应用中有很多分区的具体策略,举个例子,假设我们已经有了一组四个Redis实例分别为R0、R1、R2、R3,另外我们有一批代表用户的键,如:user:1,user:2,……等等,其中“user:”后面的数字代表的是用户的ID,我们要做的事情是把这些键分散存储在这四个不同的Redis实例上。怎么做呢?最简单的一种方式是范围分区(range partitioning),下面我们来看看基于范围分区怎么做。

范围分区

所谓范围分区,就是将一个范围内的key都映射到同一个Redis实例中,加入数据集还是上面提到的用户数据,具体做法如下:

我们可以将用户ID从0到10000的用户数据映射到R0实例,而将用户ID从10001到20000的对象映射到R1实例,依次类推。

这种方法虽然简单,但是在实际应用中是很有效的,不过还是有问题:

  • 我们需要一张表,这张表用来存储用户ID范围到Redis实例的映射关系,比如用户ID0-10000的是映射到R0实例……。
  • 我们不仅需要对这张表进行维护,而且对于每种对象类型我们都需要一个这样的表,比如我们当前存储的是用户信息,如果存储的是订单信息,我们就需要再建一张映射关系表。
  • 如果我们想要存储的数据的key并不能按照范围划分怎么办,比如我们的key是一组uuid,这个时候就不好用范围分区了。

因此,在实际应用中,范围分区并不是很好的选择,不用担心,我们还有更好的方法,接下来认识下哈希分区。

哈希分区

哈希分区跟范围分区相比一个明显的优点是哈希分区适合任何形式的key,而不像范围分区一样需要key的形式为object_name:<id>,而且分区方法也很简单,一个公式就可以表达:

id=hash(key)%N

其中id代表Redis实例的编号,公式描述的是首先根据key和一个hash函数(如crc32函数)计算出一个数值型的值。接着上面的例子,我们的第一个要处理的key是user:1,hash(user:1)的结果是93024922。

然后哈希结果进行取模,取模的目的是计算出一个介于0到3之间的值,因此这个值才可以被映射到我们的一台Redis实例上面。比如93024922%4结果是2,我们就会知道foobar将要被存储在R2上面。

当然除了上面提到的两种分区方法,还有很多其他的方法。比如一种从哈希分区演进而来的consistent hashing分区,相信信息可以参考我的另一篇文章《memcached分布式实现原理》,其已经被redis client和proxies实现了。

不同的分区实现

分区可以在redis软件栈的不同部分被实现,我们来看看下面几种:

客户端实现

客户端实现即key在redis客户端就决定了要被存储在那台Redis实例中,见下图:

客户端实现分区示意图

上面为客户端实现Redis分区的示意图。

代理实现

代理实现即客户端将请求发往代理服务器,代理服务器实现了Redis协议,因此代理服务器可以代理客户端和Redis服务器通信。代理服务器通过配置的分区schema来将客户端的请求转发到正确的Redis实例中,同时将反馈消息返回给客户端。代理实现Redis分区示意图如下:

代理实现Redis分区示意图

Redis和Memcached代理Twemoroxy都实现了代理分区。

查询路由

查询路由是Redis Cluster实现的一种Redis分区方式:

查询路由Redis分区示意图

查询路由的过程中,我们可以将查询请求随机的发送到任意一个Redis实例,这个Redis实例负责将请求转发至正确的Redis实例中。Redis集群实现了一个通过和客户端协作的hybrid来做查询路由。

Redis分区的缺点

尽管Redis分区到现在为止,so far so good,但是Redis分区有一些致命的缺点,这导致一些Redis功能在分区的环境下并不能很好地工作,我们来看看:

  • 多键操作是不被支持的,比如我们将要批量操作的键被映射到了不同的Redis实例中。
  • 多键的Redis事务是不被支持的。
  • 分区的最小粒度是键,因此我们不能将关联到一个键的很大的数据集映射到不同的实例。
  • 当应用分区的时候,数据的处理是非常复杂的,比如我们需要处理多个rdb/aof文件,将分布在不同实例的文件聚集到一起备份。
  • 添加和删除机器是很复杂的,例如Redis集群支持几乎运行时透明的因为增加或减少机器而需要做的rebalancing,然而像客户端和代理分区这种方式是不支持这种功能的。

既然有问题,那么就需要解决方案,这个时候Pre-sharding来了,后面我们会介绍Pre-Sharding。

持久存储用还是缓存

尽管数据分区对于Redis来说无论是数据持久化存储还是缓存,在概念上都是一样的,然而对于数据持久化存储还是有一个很大的限制。当我们使用Redis来作为持久化存储的时候,每一个key必须一直被映射到同一个Redis实例。而当Redis被当做缓存使用的时候,对于这个key,如果一个实例不能用了,这个key还可以被映射到其他的实例中。

Consistent hashing实现通常使得当一个key被映射到的实例不能用的时候将这个key映射到其他实例成为可能。类似,如果增加了一台机器,一部分的key将会被映射到这台新的机器上,我们需要了解的两点如下:

  1. 如果Redis被用来当做缓存,且要求容易增加或删除机器,使用consistent hashing是非常简单的。
  2. 如果Redis被用来当做(持久)存储,一个固定的key到实例的映射是需要的,因此我们不能够再灵活的添加或删除机器。否则,我们需要在增加或删除机器的时候系统能够rebalace,当前Redis Cluster已经支持。

Pre-Sharding

通过上面的介绍,我们知道Redis分区应用起来是有问题的,除非我们只是使用Redis当做缓存,否则对于增加机器或删除机器是非常麻烦的。

然而,通常我们Redis容量变动在实际应用中是非常常见的,比如今天我需要10台Redis机器,明天可能就需要50台机器了。

鉴于Redis是很轻量级的服务(每个实例仅仅占用1M),对于上面的问题一种简单的解决办法是:

我们可以开启多个Redis实例,尽管是一台物理机器,我们在刚开始的时候也可以开启多个实例。我们可以从中选择一些实例,比如32或64个实例来作为我们的工作集群。当一台物理机器存储不够的时候,我们可以将一般的实例移动到我们的第二台物理机上,依次类对,我们可以保证集群中Redis的实例数不变,又可以达到扩充机器的目的。

怎么移动Redis实例呢?当需要将Redis实例移动到独立的机器上的时候,我们可以通过下面步骤实现:

  1. 在新的物理机上启动一个新的Redis实例。
  2. 将新的物理机作为要移动的那台的slave机器。
  3. 停止客户端。
  4. 更新将要被移动的那台Redis实例的IP地址。
  5. 对于slave机器发送SLAVEOF ON ONE命令。
  6. 使用新的IP启动Redis客户端。
  7. 关闭不再使用的那个Redis实例。

总结

这篇文章在理解Redis分区概念的基础之上又介绍了Redis分区常见的几种实现方式及原理,最后根据实现中遇到的问题引入了Pre-Sharding解决方案。

缓存穿透、并发和失效,来自一线架构师的解决方案

本文作者是易宝支付的架构师程超,他介绍了我们在使用缓存过程中经常会出现的几个问题以及解决方案,包括缓存穿透、缓存并发和缓存失效,内容比较基础,但也很实在,推荐阅读。另外,如果你有其它更好的解决方案,欢迎留言讨论。

我们在用缓存的时候,不管是Redis或者Memcached,基本上会通用遇到以下三个问题:

  • 缓存穿透
  • 缓存并发
  • 缓存失效
缓存穿透

注:上面三个图会有什么问题呢?

我们在项目中使用缓存通常都是先检查缓存中是否存在,如果存在直接返回缓存内容,如果不存在就直接查询数据库然后再缓存查询结果返回。这个时候如果我们查询的某一个数据在缓存中一直不存在,就会造成每一次请求都查询DB,这样缓存就失去了意义,在流量大时,可能DB就挂掉了。

那这种问题有什么好办法解决呢?

要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。有一个比较巧妙的作法是,可以将这个不存在的key预先设定一个值。比如,”key” , “&&”。

在返回这个&&值的时候,我们的应用就可以认为这是不存在的key,那我们的应用就可以决定是否继续等待继续访问,还是放弃掉这次操作。如果继续等待访问,过一个时间轮询点后,再次请求这个key,如果取到的值不再是&&,则可以认为这时候key有值了,从而避免了透传到数据库,从而把大量的类似请求挡在了缓存之中。

缓存并发

有时候如果网站并发访问高,一个缓存如果失效,可能出现多个进程同时查询DB,同时设置缓存的情况,如果并发确实很大,这也可能造成DB压力过大,还有缓存频繁更新的问题。

我现在的想法是对缓存查询加锁,如果KEY不存在,就加锁,然后查DB入缓存,然后解锁;其他进程如果发现有锁就等待,然后等解锁后返回数据或者进入DB查询。

这种情况和刚才说的预先设定值问题有些类似,只不过利用锁的方式,会造成部分请求等待。

缓存失效

引起这个问题的主要原因还是高并发的时候,平时我们设定一个缓存的过期时间时,可能有一些会设置1分钟啊,5分钟这些,并发很高时可能会出在某一个时间同时生成了很多的缓存,并且过期时间都一样,这个时候就可能引发一当过期时间到后,这些缓存同时失效,请求全部转发到DB,DB可能会压力过重。

那如何解决这些问题呢?

其中的一个简单方案就时讲缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

我们讨论的第二个问题时针对同一个缓存,第三个问题时针对很多缓存。

总结

1、缓存穿透:查询一个必然不存在的数据。比如文章表,查询一个不存在的id,每次都会访问DB,如果有人恶意破坏,很可能直接对DB造成影响。

2、缓存失效:如果缓存集中在一段时间内失效,DB的压力凸显。这个没有完美解决办法,但可以分析用户行为,尽量让失效时间点均匀分布。

当发生大量的缓存穿透,例如对某个失效的缓存的大并发访问就造成了缓存雪崩。

精彩问答

问题:如何解决DB和缓存一致性问题?

当修改了数据库后,有没有及时修改缓存。这种问题,以前有过实践,修改数据库成功,而修改缓存失败的情况,最主要就是缓存服务器挂了。而因为网络问题引起的没有及时更新,可以通过重试机制来解决。而缓存服务器挂了,请求首先自然也就无法到达,从而直接访问到数据库。那么我们在修改数据库后,无法修改缓存,这时候可以将这条数据放到数据库中,同时启动一个异步任务定时去检测缓存服务器是否连接成功,一旦连接成功则从数据库中按顺序取出修改数据,依次进行缓存最新值的修改。

问题:问下缓存穿透那块!例如,一个用户查询文章,通过ID查询,按照之前说的,是将缓存的KEY预先设置一个值,,如果通过ID插过来,发现是预先设定的一个值,比如说是“&&”,那之后的继续等待访问是什么意思,这个ID什么时候会真正被附上用户所需要的值呢?

我刚说的主要是咱们常用的后面配置,前台获取的场景。前台无法获取相应的key,则等待,或者放弃。当在后台配置界面上配置了相关key和value之后,那么以前的key &&也自然会被替换掉。你说的那种情况,自然也应该会有一个进程会在某一个时刻,在缓存中设置这个ID,再有新的请求到达的时候,就会获取到最新的ID和value。

问题:其实用Redis的话,那天看到一个不错的例子,双key,有一个当时生成的一个附属key来标识数据修改到期时间,然后快到的时候去重新加载数据,如果觉得key多可以把结束时间放到主key中,附属key起到锁的功能。

这种方案,之前我们实践过。这种方案会产生双份数据,而且需要同时控制附属key与key之间的关系,操作上有一定复杂度。

问题:多级缓存是什么概念呢?

多级缓存就像我今天之前给大家发的文章里面提到了,将Ehcache与Redis做二级缓存,就像我之前写的文章 http://www.jianshu.com/p/2cd6ad416a5a 提到过的。但同样会存在一致性问题,如果我们需要强一致性的话,缓存与数据库同步是会存在时间差的,所以我们在具体开发的过程中,一定要根据场景来具体分析,二级缓存更多的解决是,缓存穿透与程序的健壮性,当集中式缓存出现问题的时候,我们的应用能够继续运行。

配置mongodb 复制集3.2

我们这里讲解的是如何配置mongodb复制集,首先在每一个机器上面安装mongodb,我们是通过yum的方式安装mongodb3.2

1.在文章中添加yum安装的配置文件:

touch /etc/yum.repos.d/mongodb-org-3.2.repo
vim /etc/yum.repos.d/mongodb-org-3.2.repo

打开文件后里面添加内容:

[mongodb-org-3.2]  
name=MongoDB Repository  
baseurl=https://repo.mongodb.org/yum/redhat/$releasever/mongodb-org/3.2/x86_64/  
gpgcheck=1  
enabled=1  
gpgkey=https://www.mongodb.org/static/pgp/server-3.2.asc

2.yum安装

sudo yum install -y mongodb-org

其他节点也进行进行上面的步骤,将其他的节点按照上面,进行安装mongodb,安装完成后,都关闭mongodb:

sudo service mongod stop

3. 在每一个mongodb的节点,修改hosts

节点:192.168.220.105  mongo0.example.com

节点:192.168.220.106 mongo1.example.com

vim /etc/hosts

192.168.220.105:在上面打开的文件中添加或修改:

127.0.0.1   localhost mongo0.example.com  
192.168.220.105 mongo0.example.com  
192.168.220.106 mongo1.example.com

192.168.220.106:在上面文件中添加或者修改:

127.0.0.1   localhost mongo1.example.com  
192.168.220.105 mongo0.example.com  
192.168.220.106 mongo1.example.com

 

4. 在每一个mongodb的节点,修改hostname

节点:192.168.220.105  mongo0.example.com

节点:192.168.220.106 mongo1.example.com

192.168.220.105:

sudo hostname mongo0.example.com  
vim   /etc/hostname
#在打开的文件中添加: 
mongo0.example.com

192.168.220.106:

sudo hostname mongo1.example.com  
vim   /etc/hostname 
mongo1.example.com

5.修改mongodb的配置  vim /etc/mongod.conf

进行的修改为: 把bingIp注释掉,因为这一行只让本地访问:

net:  
  port: 27017  
#  bindIp: 127.0.0.1  # Listen to local interface only, comment to listen on all interfaces.

添加复制集配置:

replication:  
   replSetName: rs0

编辑完的内容为:

# mongod.conf

# for documentation of all options, see:
#   http://docs.mongodb.org/manual/reference/configuration-options/

# where to write logging data.
systemLog:
  destination: file
  logAppend: true
  path: /var/log/mongodb/mongod.log

# Where and how to store data.
storage:
  dbPath: /var/lib/mongo
  journal:
    enabled: true
#  engine:
#  mmapv1:
#  wiredTiger:

# how the process runs
processManagement:
  fork: true  # fork and run in background
  pidFilePath: /var/run/mongodb/mongod.pid  # location of pidfile

# network interfaces
net:
  port: 27017
#  bindIp: 127.0.0.1  # Listen to local interface only, comment to listen on all interfaces.

#security:

#operationProfiling:

replication:
   replSetName: rs0
#sharding:

## Enterprise-Only Options

#auditLog:

#snmp:

保存文件后,重启linxu。

添加开机启动

chkconfig mongod on

启动mongodb:

/etc/init.d/mongod restart

 

8.现在有两台linux,现在开始配置,注意,只能进入一台mongodb,其他的mongdb不要配置,启动就好。

在mongo0中:(这个作为主节点)

mongo

执行log如下:

[root@mongo0 ~]# mongo  
MongoDB shell version: 3.2.7  
connecting to: test  
Server has startup warnings:   
2016-06-20T14:10:48.876+0800 I CONTROL  [initandlisten]   
2016-06-20T14:10:48.876+0800 I CONTROL  [initandlisten] ** WARNING: /sys/kernel/mm/transparent_hugepage/enabled is 'always'.  
2016-06-20T14:10:48.876+0800 I CONTROL  [initandlisten] **        We suggest setting it to 'never'  
2016-06-20T14:10:48.876+0800 I CONTROL  [initandlisten]   
2016-06-20T14:10:48.876+0800 I CONTROL  [initandlisten] ** WARNING: /sys/kernel/mm/transparent_hugepage/defrag is 'always'.  
2016-06-20T14:10:48.876+0800 I CONTROL  [initandlisten] **        We suggest setting it to 'never'  
2016-06-20T14:10:48.876+0800 I CONTROL  [initandlisten]   
2016-06-20T14:10:48.876+0800 I CONTROL  [initandlisten] ** WARNING: soft rlimits too low. rlimits set to 1024 processes, 64000 files. Number of processes should be at least 32000 : 0.5 times number of files.  
2016-06-20T14:10:48.876+0800 I CONTROL  [initandlisten]

然后初始化:(注意,从节点不要执行rs.initiate()  , 只有主节点执行这个)

rs.initiate()

log如下:

rs.initiate()  
{  
    "info2" : "no configuration specified. Using a default configuration for the set",  
    "me" : "mongo0.example.com:27017",  
    "ok" : 1  
}  
rs0:OTHER>

增加节点:(在主节点中执行,添加从节点,主要从节点的iptables要添加信任,如果是本地,则可以直接关掉iptables,最好是添加ip信任,添加ip信任的方式

/sbin/iptables -I INPUT -p tcp --dport 27017 -j ACCEPT
/etc/rc.d/init.d/iptables save

rs0:OTHER> rs.add("mongo1.example.com:27017");  
{ "ok" : 1 }

rs.config() 查看配置,log如下:

rs0:PRIMARY> rs.config();  
{  
    "_id" : "rs0",  
    "version" : 2,  
    "protocolVersion" : NumberLong(1),  
    "members" : [  
        {  
            "_id" : 0,  
            "host" : "mongo0.example.com:27017",  
            "arbiterOnly" : false,  
            "buildIndexes" : true,  
            "hidden" : false,  
            "priority" : 1,  
            "tags" : {  
                  
            },  
            "slaveDelay" : NumberLong(0),  
            "votes" : 1  
        },  
        {  
            "_id" : 1,  
            "host" : "mongo1.example.com:27017",  
            "arbiterOnly" : false,  
            "buildIndexes" : true,  
            "hidden" : false,  
            "priority" : 1,  
            "tags" : {  
                  
            },  
            "slaveDelay" : NumberLong(0),  
            "votes" : 1  
        }  
    ],  
    "settings" : {  
        "chainingAllowed" : true,  
        "heartbeatIntervalMillis" : 2000,  
        "heartbeatTimeoutSecs" : 10,  
        "electionTimeoutMillis" : 10000,  
        "getLastErrorModes" : {  
              
        },  
        "getLastErrorDefaults" : {  
            "w" : 1,  
            "wtimeout" : 0  
        },  
        "replicaSetId" : ObjectId("5767896cd1595b8f30b7cd4a")  
    }  
}

查看状态:

rs0:PRIMARY> rs.status()

如果后续添加节点,那么,安装完mongodb,设置完成mongodb后,启动mongodb后,设置完成hosts后,直接在主节点中执行添加:

执行log如下:

rs0:PRIMARY> rs.add("mongo2.example.com:27017");
{ "ok" : 1 }
rs0:PRIMARY> rs.status
function () {
    return db._adminCommand("replSetGetStatus");
}
rs0:PRIMARY> rs.status();
{
  "set" : "rs0",
  "date" : ISODate("2016-10-08T02:45:54.068Z"),
  "myState" : 1,
  "term" : NumberLong(19),
  "heartbeatIntervalMillis" : NumberLong(2000),
  "members" : [
    {
      "_id" : 0,
      "name" : "mongo0.example.com:27017",
      "health" : 1,
      "state" : 1,
      "stateStr" : "PRIMARY",
      "uptime" : 1126,
      "optime" : {
        "ts" : Timestamp(1475894744, 1),
        "t" : NumberLong(19)
      },
      "optimeDate" : ISODate("2016-10-08T02:45:44Z"),
      "electionTime" : Timestamp(1475894303, 1),
      "electionDate" : ISODate("2016-10-08T02:38:23Z"),
      "configVersion" : 3,
      "self" : true
    },
    {
      "_id" : 1,
      "name" : "mongo1.example.com:27017",
      "health" : 1,
      "state" : 2,
      "stateStr" : "SECONDARY",
      "uptime" : 617,
      "optime" : {
        "ts" : Timestamp(1475894744, 1),
        "t" : NumberLong(19)
      },
      "optimeDate" : ISODate("2016-10-08T02:45:44Z"),
      "lastHeartbeat" : ISODate("2016-10-08T02:45:52.842Z"),
      "lastHeartbeatRecv" : ISODate("2016-10-08T02:45:52.847Z"),
      "pingMs" : NumberLong(0),
      "syncingTo" : "mongo0.example.com:27017",
      "configVersion" : 3
    },
    {
      "_id" : 2,
      "name" : "mongo2.example.com:27017",
      "health" : 0,
      "state" : 8,
      "stateStr" : "(not reachable/healthy)",
      "uptime" : 0,
      "optime" : {
        "ts" : Timestamp(0, 0),
        "t" : NumberLong(-1)
      },
      "optimeDate" : ISODate("1970-01-01T00:00:00Z"),
      "lastHeartbeat" : ISODate("2016-10-08T02:45:52.842Z"),
      "lastHeartbeatRecv" : ISODate("1970-01-01T00:00:00Z"),
      "pingMs" : NumberLong(0),
      "lastHeartbeatMessage" : "No route to host",
      "configVersion" : -1
    }
  ],
  "ok" : 1
}
rs0:PRIMARY> 

 

 

日志:

rs0:PRIMARY> rs.status()  
{  
    "set" : "rs0",  
    "date" : ISODate("2016-06-20T06:14:17.377Z"),  
    "myState" : 1,  
    "term" : NumberLong(1),  
    "heartbeatIntervalMillis" : NumberLong(2000),  
    "members" : [  
        {  
            "_id" : 0,  
            "name" : "mongo0.example.com:27017",  
            "health" : 1,  
            "state" : 1,  
            "stateStr" : "PRIMARY",  
            "uptime" : 92,  
            "optime" : {  
                "ts" : Timestamp(1466403201, 1),  
                "t" : NumberLong(1)  
            },  
            "optimeDate" : ISODate("2016-06-20T06:13:21Z"),  
            "infoMessage" : "could not find member to sync from",  
            "electionTime" : Timestamp(1466403181, 1),  
            "electionDate" : ISODate("2016-06-20T06:13:01Z"),  
            "configVersion" : 2,  
            "self" : true  
        },  
        {  
            "_id" : 1,  
            "name" : "mongo1.example.com:27017",  
            "health" : 1,  
            "state" : 2,  
            "stateStr" : "SECONDARY",  
            "uptime" : 55,  
            "optime" : {  
                "ts" : Timestamp(1466403201, 1),  
                "t" : NumberLong(1)  
            },  
            "optimeDate" : ISODate("2016-06-20T06:13:21Z"),  
            "lastHeartbeat" : ISODate("2016-06-20T06:14:15.784Z"),  
            "lastHeartbeatRecv" : ISODate("2016-06-20T06:14:14.010Z"),  
            "pingMs" : NumberLong(0),  
            "configVersion" : 2  
        }  
    ],  
    "ok" : 1  
}

如果连接的机器是另外一个机器,也就说不是mongodb复制集中的机器,需要添加host(此处需要说明的是php,如果是和mongodb分开的那么需要在php机器的hosts中添加hosts)

vim  /etc/hosts

192.168.220.105 mongo0.example.com
192.168.220.106 mongo1.example.com

 

在shell 下面链接:(test是数据库的名字,rs0是复制集的名字)

mongo --host rs0/mongo0.example.com:27017,mongo1.example.com:27017 test

在php中连接:

'dsn' => 'mongodb://192.168.220.105:27017/erp,192.168.220.106:27017/erp?replicaSet=rs0&readPreference=primaryPreferred',

备份:

mongodump  -h 192.168.220.60:27500 -d erp -o /terry/erp

恢复:

mongorestore  -h 192.168.220.105:27017 -d erp  /terry/erp

如果在配置复制集过程中出现了问题,无法进行,一般是想重新配置,但有一种比较好的办法,就是删除掉dbPath,在配置文件中dbPath的配置如下:

dbPath: /var/lib/mongo
rm -rf /var/lib/mongo/*

 

其他:

1. 删除mongodb的库,重新启动

mv /var/lib/mongo /var/lib/mongo1  
mkdir /var/lib/mongo  
chown mongod:mongod /var/lib/mongo

2.删除复制集节点:

rs.remove("mongo0.example.com:27017")

3.在副本集的副节点读数据:

db.setSlaveOk()

4.主从切换

进入主mongo  执行:

rs.stepDown(15);

log如下:

rs0:PRIMARY> rs.stepDown(15);
2016-10-08T10:37:25.511+0800 E QUERY    [thread1] Error: error doing query: failed: network error while attempting to run command 'replSetStepDown' on host '127.0.0.1:27017'  :
DB.prototype.runCommand@src/mongo/shell/db.js:135:1
DB.prototype.adminCommand@src/mongo/shell/db.js:153:16
rs.stepDown@src/mongo/shell/utils.js:1181:12
@(shell):1:1

2016-10-08T10:37:25.513+0800 I NETWORK  [thread1] trying reconnect to 127.0.0.1:27017 (127.0.0.1) failed
2016-10-08T10:37:25.525+0800 I NETWORK  [thread1] reconnect 127.0.0.1:27017 (127.0.0.1) ok
rs0:SECONDARY> 
rs0:SECONDARY> 
rs0:SECONDARY> 
rs0:SECONDARY> 
rs0:SECONDARY> 

 

 

参考:

https://gist.github.com/leommoore/309de7c0042ed697ee84

https://docs.mongodb.com/manual/tutorial/install-mongodb-on-red-hat/

https://docs.mongodb.com/manual/tutorial/deploy-replica-set/

https://docs.mongodb.com/manual/reference/configuration-options/