Ubuntu 16.0.4 x64安装mongodb - TGZ安装包方式

从mongodb官网获取程序的TGZ安装包下载链接如https://fastdl.mongodb.org/li…登录服务器,在服务器执行下载命令,下载程序安装包wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1604-4.0.8.tgz解压安装包sudo tar -zxvf mongodb-linux-x86_64-ubuntu1604-4.0.8.tgz创建develop、conf、文件夹,并把解压后的程序文件夹移动到develop这一步可以不做,我做这个主要是我想把开发用到的软件和配置文件全部放到一起,便于个人管理,以下步骤皆以创建了这个两个文件夹为前提,如果跳过此步骤,后续步骤的命令只需要修改路径即可>mkdir develop>cd develop>mkdir conf>cd ~>mv mongodb-linux-x86_64-ubuntu1604-4.0.8 develop/创建db和log>cd ~>cd />mkdir data>cd l>mkdir db>mkdir log>cd log>vim mongodb.log>保存 mongodb.log在develop/config下创建配置文件mongodb.conf>cd ~/develop/conf>vim mongodb.conf#mongodb.conf#开启权限认证auth=trueport=27017#开启远程连接bind_ip=0.0.0.0dbpath=/data/dblogpath=/data/log/mongodb.loglogappend=truefork=true进入程序目录启动数据库第一次启动不用配置文件,因为要配置数据的主管理员账号和密码如果报错执行./mongod 报错: error while loading shared libraries: libcurl.so.4: cannot open shared object file: No such file or directory 执行apt-get install libcurl4-openssl-dev之后即可解决>cd ~/develop/mongodb-linux-x86_64-ubuntu1604-4.0.8/bin/>./mongod第一次启动之后,新开一个命令窗口,去创建数据库管理员账号密码创建数据库超级管理员账号>cd ~/develop/mongodb-linux-x86_64-ubuntu1604-4.0.8/bin/>./mongo >use admin>db.createUser({user:‘bymm’,pwd:‘pxh52017981314’,roles:[{role:‘root’,db:‘admin’}]})>exit已配置文件启动数据库,以开启远程连接>cd ~/develop/mongodb-linux-x86_64-ubuntu1604-4.0.8/bin/>./mongod –config ~/develop/conf/mongodb.conf安装过程就完成了,并且数据库已启动,远程连接也ok。

April 12, 2019 · 1 min · jiezi

Centos7.6安装4.0.8MongoDb教程

本博客 猫叔的博客,转载请申明出处本系列教程为HMStrange项目附带。历史文章如何在VMware12安装Centos7.6最新版Centos7.6安装Java8Centos7.6安装MySQL+Redis(最新版)SpringBoot+MySQL+MyBatis的入门教程SpringBoot+Redis的入门教程安装流程1、下载MongoDB的最新资源包,大家也可以关注我的公众号“Java猫说”,回复“工具包”,获取全部资源工具。或者直接到官网下载,地址:https://www.mongodb.com/downl…下载完成,使用xftp上传到自己的文件夹下2、解压# tar zxvf mongodb-linux-x86_64-4.0.8.tgz3、安装完成,我们选择使用命令+配置启动的方式,所以我们要准备一下配置信息,以下是我的目录datamongodb/ conf #配置文件目录 mongod.conf #配置文件 db #数据库目录 log #日志文件目录 mongodb.log #日志文件4、编写配置信息mongod.confvi mongodb.conf# 内容如下dbpath=/home/myself/datamongodb/dblogpath=/home/myself/datamongodb/log/mongodb.loglogappend=true #启动日志fork=false # 不保护进程port=27017bind_ip=0.0.0.0 #允许所有IP访问5、接下来就可以启动了./mongod -f /home/myself/datamongodb/conf/mongodb.conf启动成功!6、然后我们window的本地环境推荐安装两款软件:Robo 3T、Studio 3T,大家也可以关注我的公众号“Java猫说”,回复“工具包”,获取全部资源工具。然后测试远程连接连接成功!7、使用Studio 3T,可以点击 IntelliShell 使用命令行来创建数据库,插入数据use demo # 新建数据库demo,没有数据的时候还看不到db # 查看数据库信息db.demo.install({“id”:“a”,“msg”:“欢迎学习HMStrange项目!”}) # 插入数据8、使用 Robo 3T 查看数据信息公众号:Java猫说学习交流群:728698035现架构设计(码农)兼创业技术顾问,不羁平庸,热爱开源,杂谈程序人生与不定期干货。

April 12, 2019 · 1 min · jiezi

SpringBoot 2.X Kotlin 系列之Reactive Mongodb 与 JPA

一、本节目标前两章主要讲了SpringBoot Kotlin的基本操作,这一章我们将学习使用Kotlin访问MongoDB,并通过JPA完成(Create,Read,Update,Delete)简单操作。这里有一个问题什么不选用MySQL数据库呢?答案是 Spring Data Reactive Repositories 目前支持 Mongo、Cassandra、Redis、Couchbase。不支持 MySQL,那究竟为啥呢?那就说明下 JDBC 和 Spring Data 的关系。Spring Data Reactive Repositories 突出点是 Reactive,即非阻塞的。区别如下:基于 JDBC 实现的 Spring Data,比如 Spring Data JPA 是阻塞的。原理是基于阻塞 IO 模型 消耗每个调用数据库的线程(Connection)。事务只能在一个 java.sql.Connection 使用,即一个事务一个操作。二、构建项目及配置本章不在讲解如何构建项目了,大家可以参考第一章。这里我们主要引入了mongodb-reactive框架,在pom文件加入下列内容即可。<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-mongodb-reactive</artifactId></dependency>如何jar包后我们需要配置一下MongoDB数据库,在application.properties文件中加入一下配置即可,密码和用户名需要替换自己的,不然会报错的。spring.data.mongodb.host=localhostspring.data.mongodb.port=27017spring.data.mongodb.password=student2018.Docker_spring.data.mongodb.database=studentspring.data.mongodb.username=student三、创建实体及具体实现3.1实体创建package io.intodream.kotlin03.entityimport org.springframework.data.annotation.Idimport org.springframework.data.mongodb.core.mapping.Document/** * @description * * @author Jwenk * @copyright intoDream.io 筑梦科技 * @email xmsjgzs@163.com * @date 2019-03-25,18:05 /@Documentclass Student { @Id var id :String? = null var name :String? = null var age :Int? = 0 var gender :String? = null var sClass :String ?= null override fun toString(): String { return ObjectMapper().writeValueAsString(this) }}3.2Repositorypackage io.intodream.kotlin03.daoimport io.intodream.kotlin03.entity.Studentimport org.springframework.data.mongodb.repository.ReactiveMongoRepository/* * @description * * @author Jwenk * @copyright intoDream.io 筑梦科技 * @email xmsjgzs@163.com * @date 2019-03-25,18:04 /interface StudentRepository : ReactiveMongoRepository<Student, String>{}3.3Servicepackage io.intodream.kotlin03.serviceimport io.intodream.kotlin03.entity.Studentimport reactor.core.publisher.Fluximport reactor.core.publisher.Mono/* * @description * * @author Jwenk * @copyright intoDream.io 筑梦科技 * @email xmsjgzs@163.com * @date 2019-03-25,18:04 /interface StudentService { /* * 通过学生编号获取学生信息 / fun find(id : String): Mono<Student> /* * 查找所有学生信息 / fun list(): Flux<Student> /* * 创建一个学生信息 / fun create(student: Student): Mono<Student> /* * 通过学生编号删除学生信息 / fun delete(id: String): Mono<Void>}// 接口实现类package io.intodream.kotlin03.service.implimport io.intodream.kotlin03.dao.StudentRepositoryimport io.intodream.kotlin03.entity.Studentimport io.intodream.kotlin03.service.StudentServiceimport org.springframework.beans.factory.annotation.Autowiredimport org.springframework.stereotype.Serviceimport reactor.core.publisher.Fluximport reactor.core.publisher.Mono/* * @description * * @author Jwenk * @copyright intoDream.io 筑梦科技 * @email xmsjgzs@163.com * @date 2019-03-25,18:23 /@Serviceclass StudentServiceImpl : StudentService{ @Autowired lateinit var studentRepository: StudentRepository override fun find(id: String): Mono<Student> { return studentRepository.findById(id) } override fun list(): Flux<Student> { return studentRepository.findAll() } override fun create(student: Student): Mono<Student> { return studentRepository.save(student) } override fun delete(id: String): Mono<Void> { return studentRepository.deleteById(id) }}3.4 Controller实现package io.intodream.kotlin03.webimport io.intodream.kotlin03.entity.Studentimport io.intodream.kotlin03.service.StudentServiceimport org.slf4j.LoggerFactoryimport org.springframework.beans.factory.annotation.Autowiredimport org.springframework.web.bind.annotation.import reactor.core.publisher.Fluximport reactor.core.publisher.Mono/ * @description * * @author Jwenk * @copyright intoDream.io 筑梦科技 * @email xmsjgzs@163.com * @date 2019-03-25,18:03 /@RestController@RequestMapping("/api/student")class StudentController { @Autowired lateinit var studentService: StudentService val logger = LoggerFactory.getLogger(this.javaClass) /* * 保存或新增学生信息 / @PostMapping("/") fun create(@RequestBody student: Student): Mono<Student> { logger.info("【保存学生信息】请求参数:{}", student) return studentService.create(student) } /* * 更新学生信息 / @PutMapping("/") fun update(@RequestBody student: Student): Mono<Student> { logger.info("【更新学生信息】请求参数:{}", student) return studentService.create(student) } /* * 查找所有学生信息 / @GetMapping("/list") fun listStudent(): Flux<Student> { return studentService.list() } /* * 通过学生编号查找学生信息 / @GetMapping("/id") fun student(@RequestParam id : String): Mono<Student> { logger.info(“查询学生编号:{}”, id) return studentService.find(id) } /* * 通过学生编号删除学生信息 */ @DeleteMapping("/") fun delete(@RequestParam id: String): Mono<Void> { logger.info(“删除学生编号:{}”, id) return studentService.delete(id) }}四、接口测试这里我们使用Postman来对接口进行测试,关于Postman这里接不用做过多的介绍了,不懂可以自行百度。控制台打印如下:2019-03-25 18:57:04.333 INFO 2705 — [ctor-http-nio-3] i.i.kotlin03.web.StudentController : 【保存学生信息】请求参数:{“id”:“1”,“name”:“Tom”,“age”:18,“gender”:“Boy”,“sclass”:“First class”}我们看一下数据库是否存储了其他接口测试情况如果大家觉得文章有用麻烦点一下赞,有问题的地方欢迎大家指出来。 ...

April 10, 2019 · 2 min · jiezi

mongodb中的添加用户操作

mongodb添加用户本教程介绍mongodb中添加用户的一些操作mongodb中的用户是什么在mongodb中通过用户来管理每个数据库的权限,想要控制数据库的使用权,就需要添加用户,给指定的用户分配权限,让特定用户来做特定的操作。添加用户有什么用细分权限,限制数据库的访问和使用,提高mongodb的安全性。为什么要添加用户防止被人非法使用,做一些非法操作,导致一些严重后果。比如删库跑路─=≡(((つ••)つ怎么添加用户首先,在mongod启动时是不会启动校验的mongod启动mongod后,连接到mongodroot@e444205572bd:/# mongoMongoDB shell version v4.1.9connecting to: mongodb://127.0.0.1:27017/?compressors=disabled&gssapiServiceName=mongodbImplicit session: session { “id” : UUID(“e3fd959c-db96-4853-a306-9edcc8c5baa7”) }MongoDB server version: 4.1.9……指定到admin数据库下> use adminswitched to db admin通过指定的函数创建用户> db.createUser({user:“user”, pwd:“123123”, roles:[“root”]})Successfully added user: { “user” : “user”, “roles” : [ “root” ] }通过show查看该数据库的用户> show users{ “_id” : “admin.user”, “userId” : UUID(“95e02aca-49c2-4852-b2bc-7dc4f2738175”), “user” : “user”, “db” : “admin”, “roles” : [ { “role” : “root”, “db” : “admin” } ], “mechanisms” : [ “SCRAM-SHA-1”, “SCRAM-SHA-256” ]}创建用户成功添加用户之后如何连接mongodb使用mongo连接root@1410aa527d51:/# mongo -u user -p 123123 MongoDB shell version v4.1.9connecting to: mongodb://127.0.0.1:27017/?compressors=disabled&gssapiServiceName=mongodbImplicit session: session { “id” : UUID(“8e9a9173-8263-40ea-b814-39090f0c90b7”) }MongoDB server version: 4.1.9……..在mgo中进行连接"mongodb://user:123123@localhost:27017"info := &mgo.DialInfo{ Addrs:[]string{ “localhost:27017”, }, Direct:false, Timeout:30 * time.Second, Database: “user”, Source:“admin”, Username:“user”, Password:“123123”, } session, err := mgo.DialWithInfo(info) //session, err := mgo.Dial(url) if err != nil { logs.Error(err) }注:通过docker部署的mongo,在启动时添加参数MONGO_INITDB_ROOT_USERNAME和MONGO_INITDB_ROOT_PASSWORD是可以创建用户的。但是如果之前的mongo并没有创建用户,并且你挂载了volume,把容器中的db数据映射到了宿主机,那你就要进入容器中手动创建用户了。 ...

April 4, 2019 · 1 min · jiezi

两年了,我写了这些干货!

开公众号差不多两年了,有不少原创教程,当原创越来越多时,大家搜索起来就很不方便,因此做了一个索引帮助大家快速找到需要的文章!Spring Boot系列SpringBoot+SpringSecurity处理Ajax登录请求SpringBoot+Vue前后端分离(一):使用SpringSecurity完美处理权限问题1SpringBoot+Vue前后端分离(二):使用SpringSecurity完美处理权限问题2SpringBoot+Vue前后端分离(三):SpringSecurity中密码加盐与SpringBoot中异常统一处理SpringBoot+Vue前后端分离(四):axios请求封装和异常统一处理SpringBoot+Vue前后端分离(五):权限管理模块中动态加载Vue组件SpringBoot+Vue前后端分离(六):使用SpringSecurity完美处理权限问题SpringBoot中自定义参数绑定SpringBoot中使用POI,快速实现Excel导入导出SpringBoot中发送QQ邮件SpringBoot中使用Freemarker构建邮件模板SpringBoot+WebSocket实现在线聊天(一)SpringBoot+WebSocket实现在线聊天(二)SpringSecurity登录使用JSON格式数据SpringSecurity登录添加验证码SpringSecurity中的角色继承问题Spring Boot中通过CORS解决跨域问题Spring Boot数据持久化之JdbcTemplateSpring Boot多数据源配置之JdbcTemplate最简单的SpringBoot整合MyBatis教程极简Spring Boot整合MyBatis多数据源Spring Boot中的yaml配置简介SpringBoot整合Swagger2,再也不用维护接口文档了Spring Boot中,Redis缓存还能这么用!干货|一文读懂 Spring Data Jpa!Spring基础配置Spring常用配置Spring常用配置(二)SpringMVC基础配置SpringMVC常用配置JavaWeb之最简洁的配置实现文件上传初识Spring Boot框架DIY一个Spring Boot的自动配置使用Spring Boot开发Web项目为我们的Web添加HTTPS支持在Spring Boot框架下使用WebSocket实现消息推送一个JavaWeb搭建的开源Blog系统,整合SSM框架Spring Cloud系列1.使用Spring Cloud搭建服务注册中心2.使用Spring Cloud搭建高可用服务注册中心3.Spring Cloud中服务的发现与消费4.Eureka中的核心概念5.什么是客户端负载均衡6.Spring RestTemplate中几种常见的请求方式7.RestTemplate的逆袭之路,从发送请求到负载均衡8.Spring Cloud中负载均衡器概览9.Spring Cloud中的负载均衡策略10.Spring Cloud中的断路器Hystrix11.Spring Cloud自定义Hystrix请求命令12.Spring Cloud中Hystrix的服务降级与异常处理13.Spring Cloud中Hystrix的请求缓存14.Spring Cloud中Hystrix的请求合并15.Spring Cloud中Hystrix仪表盘与Turbine集群监控16.Spring Cloud中声明式服务调用Feign17.Spring Cloud中Feign的继承特性18.Spring Cloud中Feign配置详解19.Spring Cloud中的API网关服务Zuul20.Spring Cloud Zuul中路由配置细节21.Spring Cloud Zuul中异常处理细节22.分布式配置中心Spring Cloud Config初窥23.Spring Cloud Config服务端配置细节(一)24.Spring Cloud Config服务端配置细节(二)之加密解密25.Spring Cloud Config客户端配置细节26.Spring Cloud Bus之RabbitMQ初窥27.Spring Cloud Bus整合RabbitMQ28.Spring Cloud Bus整合Kafka29.Spring Cloud Stream初窥30.Spring Cloud Stream使用细节31.Spring Cloud系列勘误Docker系列Docker教程合集MongoDB系列1.Linux上安装MongoDB2.MongoDB基本操作3.MongoDB数据类型4.MongoDB文档更新操作5.MongoDB文档查询操作(一)6.MongoDB文档查询操作(二)7.MongoDB文档查询操作(三)8.MongoDB查看执行计划9.初识MongoDB中的索引10.MongoDB中各种类型的索引11.MongoDB固定集合12.MongoDB管道操作符(一)13.MongoDB管道操作符(二)14.MongoDB中MapReduce使用15.MongoDB副本集搭建16.MongoDB副本集配置17.MongoDB副本集其他细节18.初识MongoDB分片19.Java操作MongoDBRedis系列教程1.Linux上安装Redis2.Redis中的五种数据类型简介3.Redis字符串(STRING)介绍4.Redis字符串(STRING)中BIT相关命令5.Redis列表与集合6.Redis散列与有序集合7.Redis中的发布订阅和事务8.Redis快照持久化9.Redis之AOF持久化10.Redis主从复制(一)11.Redis主从复制(二)12.Redis集群搭建13.Jedis使用14.Spring Data Redis使用Git系列1.Git概述2.Git基本操作3.Git中的各种后悔药4.Git分支管理5.Git关联远程仓库6.Git工作区储藏兼谈分支管理中的一个小问题7.Git标签管理Elasticsearch系列引言elasticsearch安装与配置初识elasticsearch中的REST接口elasticsearch修改数据elasticsearch文档操作elasticsearch API约定(一)elasticsearch API约定(二)elasticsearch文档读写模型elasticsearch文档索引API(一)elasticsearch文档索引API(二)elasticsearch文档Get APIelasticsearch文档Delete APIelasticsearch文档Delete By Query API(一)elasticsearch文档Delete By Query API(二)elasticsearch文档Update API我的Github开源项目开源项目(一): SpringBoot+Vue前后端分离开源项目-微人事开源项目(二): SpringBoot+Vue前后端分离开源项目-V部落开源项目(三): 一个开源的五子棋大战送给各位小伙伴!开源项目(四):一个开源的会议管理系统献给给位小伙伴!开源项目(五):一个JavaWeb搭建的开源Blog系统,整合SSM框架杂谈从高考到程序员之毕业流水帐从高考到现在起早贪黑几个月,我写完了人生第一本书!当公司倒闭时,你在干什么?华为云 open day,带你看看别人家的公司其他小程序开发框架WePY和mpvue使用感受两步解决maven依赖导入失败问题干货|6个牛逼的基于Vue.js的后台控制面板,接私活必备Ajax上传图片以及上传之前先预览一个简单的案例带你入门Dubbo分布式框架WebSocket刨根问底(一)WebSocket刨根问底(二)WebSocket刨根问底(三)之群聊Nginx+Tomcat搭建集群,Spring Session+Redis实现Session共享IntelliJ IDEA中创建Web聚合项目(Maven多模块项目)Linux上安装Zookeeper以及一些注意事项初识ShiroShiro中的授权问题Shiro中的授权问题(二)更多资料,请关注公众号牧码小子,回复 Java, 获取松哥为你精心准备的Java干货! ...

April 3, 2019 · 1 min · jiezi

php mongodb模糊搜索

以前一直使用mysql数据库,模糊搜索like关键字就能搞定。最近接入了mongodb平台,一时无法适应 ,踩了一些坑,在此记录下来,希望对其他人能够有用。1.mongodb对于普通非文本所有字段如何进行模糊搜索答案:使用正则表达式。对于过去经常使用mysql的同学可能不太适合,因为一想到正则表达式,我们就会想到对性能的影响,通常是能不用就不用,但是mongodb除了文本索引只能使用正则表达式进行模糊搜索。2.如何在php中使用正则表达式答案:MongoDBBSONRegex(php7使用了mongodb扩展),mongoregex(php7以前使用mongo扩展)3.两者在使用细节上的区别答案:MongoDBBSONRegex在构造regex对象时传入的字符串不需要前后的斜线,选项通过第二个参数传入$regex = new MongoRegex("/^$search/");$regex = new \MongoDB\BSON\Regex("^{$search}", ‘i’);当然了,如果要使用全文本索引mongodb也是支持的

April 2, 2019 · 1 min · jiezi

mongDB修改用户及密码踩坑

如mongo的上篇帖子,本地使用navicat连接mongo可以成功,而且命令行查看用户也是存在的,但是使用指定用户登录命令行却报错了,如图于是开始尝试修改用户密码在连接cmd!重要!1、进入admin2、验证root3、修改密码!如图,已经修改成功了,navicat和代码中已经可以使用新密码进行连接测试BUT!容我空了再来搞他!

March 27, 2019 · 1 min · jiezi

mongodb账户授权管理

最近mongodb出现了较多的权限事故,远的有国外的信用卡信息泄露,近的有国内用户人脸识别数据库被脱裤,原因都是使用了未加权限管理的mongodb数据库,导致在公网可以直接通过ip加端口的方式访问。最近我们新开的一个项目也是用了mongodb,为了避免自己成为背锅侠,我也不得已实践一遍mongodb的用户权限管理。要想对db授权, 首先要搞清楚mongodb的角色种类:Read:允许用户读取指定数据库readWrite:允许用户读写指定数据库dbAdmin:允许用户在指定数据库中执行管理函数,如索引创建、删除,查看统计或访问system.profileuserAdmin:允许用户向system.users集合写入,可以找指定数据库里创建、删除和管理用户clusterAdmin:只在admin数据库中可用,赋予用户所有分片和复制集相关函数的管理权限。readAnyDatabase:只在admin数据库中可用,赋予用户所有数据库的读权限readWriteAnyDatabase:只在admin数据库中可用,赋予用户所有数据库的读写权限userAdminAnyDatabase:只在admin数据库中可用,赋予用户所有数据库的userAdmin权限dbAdminAnyDatabase:只在admin数据库中可用,赋予用户所有数据库的dbAdmin权限。root:只在admin数据库中可用。超级账号,超级权限mongodb给用户授权有两种方式1.在添加用户的时候授权use order;db.createUser( { user: “devweb-test3”, pwd: “mmt-devweb”, roles: [ { role: “dbAdmin”, db: “order” },{ role: “readWrite”, db: “order” } ] });2.创建用户后给用户添加权限use order;db.grantRolesToUser( “devweb-test3” , [ { role: “dbOwner”, db: “order” } ])删除用户use order;db.dropUser(‘devweb-test3’);回收用户权限use order;db.revokeRolesFromUser( “devweb-test3” , [ { role: “readWrite”, db: “order” } ])以上是我实践中成功的案例,下面是实践中踩到的一些坑授权必须在被授权的db中进行,否则授权无效(我的mongo版本是4.0.7)use admin;switched to db admin> db.createUser(… {… user: “devweb-test3”,… pwd: “mmt-devweb”,… roles: [ { role: “dbAdmin”, db: “order” },{ role: “readWrite”, db: “order” } ]… }… );Successfully added user: { “user” : “devweb-test3”, “roles” : [ { “role” : “dbAdmin”, “db” : “order” }, { “role” : “readWrite”, “db” : “order” } ]}然后在另外一个终端登录进行认证db.logout();{ “ok” : 1 }> db.auth(‘devweb-test3’,‘mmt-devweb’);Error: Authentication failed.2.仅仅给用户添加adAdmin角色仍然无法访问db,还需要readWrite角色db.createUser(… {… user: “devweb-test”,… pwd: “mmt-devweb”,… roles: [ { role: “dbAdmin”, db: “order” } ]… }… );Successfully added user: { “user” : “devweb-test”, “roles” : [ { “role” : “dbAdmin”, “db” : “order” } ]}在另外一个终端进行登录授权db.auth(‘devweb-test’,‘mmt-devweb’);1> show collections;financialAssetFlowproductDetailproductList> db.financialAssetFlow.find().pretty();Error: error: { “ok” : 0, “errmsg” : “not authorized on order to execute command { find: "financialAssetFlow", filter: {}, lsid: { id: UUID("676a5042-0c80-4b79-9e8a-d91b63e80199") }, $db: "order" }”, “code” : 13, “codeName” : “Unauthorized”} ...

March 25, 2019 · 1 min · jiezi

win7下安装mongoDB

1、官网下载适合自己的版本mongoDB官网地址2、安装选择第二个自定义安装选择位置,弹窗直接ignore(记得去掉勾选内容)3、管理员进入命令行 mongod –bind_ip 127.0.0.1 –port 27017 –dbpath=“D:\mongodb\data” –logpath=“D:\mongodb\logs\mongod.log” –serviceName “Mongodb” –serviceDisplayName “MongoDB” –install以上就是常规安装mongoDB,且已经在服务中可以找到它了,如果想要设置密码请继续下面操作。4、不要关闭服务,直接命令行输入mong.exe 进入mongo执行以下命令,手敲(用户和密码可以自己更改) a:use admin b:db.createUser( { user: “root”, pwd: “123456”, roles: [ { role: “userAdminAnyDatabase”, db: “admin” } ] } )5、退出之后,丢弃掉之前注册的服务: sc delete MongoDB6、重新注册服务(添加–auth参数) mongod –bind_ip 127.0.0.1 –port 27017 –auth –dbpath=“D:\mongodb\data” –logpath=“D:\mongodb\logs\mongod.log” –serviceName “Mongodb” –serviceDisplayName “MongoDB” –install

March 25, 2019 · 1 min · jiezi

前端监控数据收集(请求拦截)

所谓web,即使你我素未谋面,便知志趣相投;足不出户,亦知世界之大。01 — 为什么拦截请求现在的web应用,大都是通过请求(http)去获取资源,拿到资源后再呈现给用户,一个页面中可以有多个这样的请求。每一次请求的开始,等待,完成,异常都会有相应的状态来标识。我们在自己的框架中通常都会使用一个全局过滤器,来拦截请求,目的大同小异:在发送请求之前,修改请求参数,添加请求头请求发送中的进度计算(通常是文件上传)请求出错后的捕获请求结束后,处理后台返回数据结构,进行适配……看看请求的整个流程图:而我们最常用的发送请求的便是XMLHttpRequest。XMLHttpRequest.readyState的五种就绪状态:0:请求未初始化(还没有调用 open())。1:请求已经建立,但是还没有发送(还没有调用 send())。2:请求已发送,正在处理中(通常现在可以从响应中获取内容头)。3:请求在处理中;通常响应中已有部分数据可用了,但是服务器还没有完成响应的生成。4:响应已完成;您可以获取并使用服务器的响应了。并且XMLHttpRequest还提供了每个阶段的事件:abort如果请求中止,会触发abort事件。error网络错误(如太多重定向)会阻止请求完成,会触发error事件。load当事件完成,会触发load事件。loadend当一个请求完成,无论成功(load)或者不成功(abort/error)后触发loadstart当调用send()时,触发单个loadstart事件。progress当等待服务器的响应时,XHR对象会发生progress事件。通常每隔50毫秒左右,所以可以使用这事件给用户反馈请求的进度。timeout当等待服务器的响应超时会触发。02 — 如何拦截请求了解了XMLHttpRequest的请求流程后,我们就可以开始去拦截浏览器发出的请求,去做我们想做的事。方式一:(function (xhr) {// Capture request before any network activity occurs: var send = xhr.send; xhr.send = function (data) { this.addEventListener(’loadstart’, onLoadStart); this.addEventListener(’loadend’, onLoadEnd); this.addEventListener(’error’, onError); return send.apply(this, arguments); };})(XMLHttpRequest.prototype);这种是最简单直接的方式,修改XMLHttpRequest的原型,在发送请求时开启事件监听。大多数情况下都是没什么大问题的,但后来发现在Angular4+以上版本中这样去拦截,请求触发loadend事件后获取到的请求响应成功与否状态始终为false,因为Angualr2后来的版本也使用事件监听来处理拦截,有些地方就冲突了。方式二:出现问题总要解决吧,然后就采用方法一的升级版本,完全重写XMLHttpRequest。(function () { // create XMLHttpRequest proxy object var oldXMLHttpRequest = XMLHttpRequest; // define constructor for my proxy object window.XMLHttpRequest = function () { var actual = new oldXMLHttpRequest(); var self = this; this.onreadystatechange = null; // this is the actual handler on the real XMLHttpRequest object actual.onreadystatechange = function () { if (this.readyState == 1) { onLoadStart.call(this); } else if (this.readyState == 4) { if(this.status==200) onLoadEnd.call(this); else{ onError.call(this); } } if (self.onreadystatechange) { return self.onreadystatechange(); } };// add all proxy getters[“status”, “statusText”, “responseType”, “response”,“readyState”, “responseXML”, “upload”].forEach(function (item) { Object.defineProperty(self, item, { get: function () { return actual[item]; }, set: function (val) { actual[item] = val; } });});// add all proxy getters/setters[“ontimeout, timeout”, “withCredentials”, “onload”, “onerror”, “onprogress”].forEach(function (item) { Object.defineProperty(self, item, { get: function () { return actual[item]; }, set: function (val) { actual[item] = val; } });});// add all pure proxy pass-through methods[“addEventListener”, “send”, “open”, “abort”, “getAllResponseHeaders”,“getResponseHeader”, “overrideMimeType”, “setRequestHeader”, “removeEventListener”].forEach(function (item) { Object.defineProperty(self, item, { value: function () { return actual[item].apply(actual, arguments); } }); }); }})();03 — 项目实战现在我们可以放心的拦截浏览器发出的请求了,妈妈再也不用担心我的学习了,哈哈。说一千道一万,来点干货,直接看项目。传送门:web-monitor喜欢请点个赞呗或者去https://github.com/kisslove/w… Star一下或者打赏一下再或者……哈哈,想法有点多了。 ...

March 19, 2019 · 1 min · jiezi

全栈之路 - MongoDB

安装Mac 安装中遇到的问题首先是Homebrew更新的问题,卡在更新界面不动了(很久没用homebrew就会出现这种问题) 禁止更新: export HOMEBREW_NO_AUTO_UPDATE=true 然后安装就好了 brew install mongodb首先是Homebrew更新的问题,卡在更新界面不动了(很久没用homebrew就会出现这种问题) 禁止更新: export HOMEBREW_NO_AUTO_UPDATE=true 然后安装就好了 brew install mongodb启动mongo 命令是mongod 启动报错 exception in initAndListen: NonExistentPath: Data directory /Users/mifan/Documents/mongo/db not found., terminating 主要就是没找到这个文件夹 新建一个文件夹 然后设置一下路径 mongod –dbpath /Users/mifan/mongo 设置一下 用绝对路径 然后重新运行 mongod !successl了还有个题外的问题, 我用的是自带的终端然后使用了fish插件,但是这个插件不支持$ 这个解决办法就简单了 所有带$的命令都去掉&就正常了(我感觉我在说废话)还有个题外的问题, 我用的是自带的终端然后使用了fish插件,但是这个插件不支持$ 这个解决办法就简单了 所有带$的命令都去掉&就正常了(我感觉我在说废话)查看端口占用 lsof -i:27017kill 端口号基本查看命令show dbs // 查看数据库use admin // 进入数据库 使用数据库show collections //显示数据表

March 17, 2019 · 1 min · jiezi

使用Docker部署Nginx+Flask+Mongo的应用

使用Docker部署Nginx+Flask+Mongo的应用Nginx做为服务器,Mongo为数据库支持,Flask为Python语言的Web框架,利用Docker的容器特性,可以简单地部署在linux服务器上项目准备项目主要目录如下__ project-name |__ docker-file |__ ningx |__ Dockerfile |__ conf |__ nginx.conf |__ flask |__ Dockerfile |__ requirements.txt |__ mongo |__ Dockerfile |__ setup.sh |__ docker-compose.yml |__ src |__ app |__ … |__ run.py简要说明docker-file目录为docker部署的配置文件src目录为flask应用的python代码Docker的详细配置docker-compose配置version: ‘2.2’services: mongo: build: ./mongo volumes: - “./mongo/db:/data/db” restart: always ports: - “27017:27017” environment: MONGO_INITDB_ROOT_USERNAME: root MONGO_INITDB_ROOT_PASSWORD: 123456 flask: build: ./flask links: - mongo ports: - “5000:5000” expose: - “5000” volumes: - ../src:/home/web nginx: build: ./nginx links: - flask volumes: - “./nginx/log:/var/log/nginx” - “../:/usr/share/nginx/html” ports: - “80:80” - “8080:8080” - “443:443” restart: alwaysMongoDB的配置/mongo/Dockerfile的内容如下FROM mongo:3.6# 设置时区ENV TZ=Asia/ShanghaiRUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone# 设置工作目录ENV WORKDIR /usr/local/workENV AUTO_RUN_DIR /docker-entrypoint-initdb.dENV INSTALL_MONGO_SHELL setup.shRUN mkdir -p $WORKDIR# 复制数据库的初始化命令COPY ./$INSTALL_MONGO_SHELL $AUTO_RUN_DIR/RUN chmod +x $AUTO_RUN_DIR/$INSTALL_MONGO_SHELL/mongo/setup.sh的内容如下该文件的目的是,启动MongoDB后创建一个密码为test的用户test,并赋予它数据库test的读写操作#!/bin/bashmongo <<EOFuse admin;db.auth(‘root’, ‘123456’);use dmx_aluminum;db.createUser({user:’test’,pwd:’test’,roles:[{role:‘readWrite’,db:’test’}]});db.createCollection(“user”);EOFFlask应用的配置/flask/Dockerfile的内容如下FROM python:3.6# 设置时区ENV TZ=Asia/ShanghaiRUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone# 设置工作区RUN mkdir -p /home/webWORKDIR /home/web# 添加依赖ADD requirements.txt /home/web/requirements.txtRUN pip3 install -i https://pypi.douban.com/simple/ -r requirements.txt# 使用gunicorn启动应用CMD gunicorn -w 4 -b 0.0.0.0:5000 run:app/src/app/run.py的代码此处注释了调试用的 app.run(),发布时用gunicorn启动from app import create_appapp = create_app(‘default’)app.debug=False# if name == ‘main’:# app.run()Nginx的配置/nginx/Dockerfile的内容如下FROM nginx:1.14# 设置时区ENV TZ=Asia/ShanghaiRUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone# 复制配置COPY conf/nginx.conf /etc/nginx/nginx.conf/nignx/conf/nginx.conf的内容如下user nginx;worker_processes 1;error_log /var/log/nginx/error.log warn;pid /var/run/nginx.pid;events { worker_connections 1024;}http { include /etc/nginx/mime.types; default_type application/octet-stream; log_format main ‘$remote_addr - $remote_user [$time_local] “$request” ’ ‘$status $body_bytes_sent “$http_referer” ’ ‘"$http_user_agent" “$http_x_forwarded_for”’; access_log /var/log/nginx/access.log main; sendfile on; #tcp_nopush on; keepalive_timeout 65; # 开启gzip gzip on; gzip_min_length 1k; gzip_buffers 4 16k; #gzip_http_version 1.0; gzip_comp_level 1; gzip_types text/plain application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png; gzip_vary off; gzip_disable “MSIE [1-6].”; server { listen 80; server_name localhost; keepalive_timeout 5; root /usr/share/nginx/html; location /static/ { alias /usr/share/nginx/html/src/app/static/; } location / { # checks for static file, if not found proxy to app try_files $uri @flask_app; } location @flask_app { proxy_pass http://192.168.0.2:5000; # 发布在阿里云上,应填写内网IP proxy_redirect off; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; #proxy_buffers 8 32k; #proxy_buffer_size 64k; } }}启动部署进入docker-flie目录 cd docker-flie启动docker docker-compose up查看容器状态 docker ps本地部署浏览器输入 127.0.0.1即可最后出现类似docker_file_nginx_1,docker_file_mongo_1, docker_file_flask_1的3个容器,说明成功!!!踩坑吐槽1 mongol容器中的初始化文件需要放在 docker-entrypoint-initdb.d 目录下本人做过如下尝试,会显示 mongdb未启动。ADD setup.sh /data/setup.shRUN chmod +x /data/setup.shCMD ["/data/setup.sh"]2 flask应用无法连接mongo,本文使用link方式。在数据库的配置应相应写成:MONGODB_SETTINGS = { ‘db’: ’test’, ‘host’: ‘mongo’, # 127.0.0.1 host地址一定要写你配置的–link的名字 ‘username’: ’test’, ‘password’: ’test’, ‘port’: 27017 }本地测试时改回127.0.0.13 nginx中配置使用的代理模式,其中执行flask应用的IP,应为内网IP ...

March 17, 2019 · 2 min · jiezi

Mac下安装 MongoDB 问题解决

mongoDB 的学习遇到的问题1. homebrew 更新的问题 卡在更新界面不动了我的处理方法 (很久没用就会出现这个情况) 禁止更新: export HOMEBREW_NO_AUTO_UPDATE=true2. 我用的是fish 终端 不支持$符号, 所有的命令去掉$ 就可以了3. which mongo 查看mongo在哪里 有path出来,你就安装成功了4. 启动mongo 命令是mongod5. 启动出错 exception in initAndListen: NonExistentPath: Data directory /Users/mifan/Documents/mongo/db not found., terminating 主要就是没找到这个文件夹 mongod –dbpath /Users/mifan/mongo 设置一下 用绝对路径 然后重新运行 mongod !successl了6. 查看端口占用 lsof -i:27017 kill 端口号

March 16, 2019 · 1 min · jiezi

TodoList:适合初学者的vue+node小项目

TodoList一个简单的vue + nodejs项目,前端由vue实现,后端由nodejs(express),数据库采用mongodb。github: https://github.com/xiechengbo/netease_todos在线效果展示: http://www.chengbo.xyz前端使用vue-cli脚手架, vue+axio实现的功能(1) 单条添加todo(2) 单条删除todo(3) 双击编辑todo(4) 单条todo已完成相应样式状态改变(5) 全部todo是已完成相应样式状态改变(6) 清除全部已完成todos(7) 待办todos数量显示(8) 所有todos,已完成todos,未完成todos筛选接口展示 import axios from ‘axios’ const baseUrl = process.env.NODE_ENV === “development” ? “https://nei.netease.com/api/apimock/f3e5d93d5eaceda5a624378374ad5cd7" : “http://148.70.150.147:8080” export const getAllTask = params => { return axios.get(${baseUrl}/api/all, {params: params}) } export const addTask = params => { return axios.post(${baseUrl}/api/add, params).then(res => res.data) } export const deleteTask = params => { return axios.post(${baseUrl}/api/deletes, params).then(res => res.data) } export const updateTask = params => { return axios.post(${baseUrl}/api/update, params).then(res => res.data) } export const updateManyTask = params => { return axios.post(${baseUrl}/api/updateMany, params).then(res => res.data) } export const deleteCompletedTask = params => { return axios.post(${baseUrl}/api/deletemany, params) }后端1.后台由express + mongoodb构建。2.路由 module.exports = function(app) { app.get(’/api/all’, TodoController.getAll); app.post(’/api/add’, TodoController.newTodo); app.post(’/api/deletes’, TodoController.deleteOne); app.post(’/api/deletemany’, TodoController.deleteAllCompleted); app.post(’/api/update’, TodoController.updateOne); app.post(’/api/updateMany’, TodoController.updateMany); ….项目启动$ npm install// or$ yarn install开发环境$ npm run dev完整代码点我, 记得star哦!! ...

March 16, 2019 · 1 min · jiezi

mongodb+php 使用方法

Mognodb数据库连接默认格式$m = new Mongo();//这里采用默认连接本机的27017端口,当然也可以连接远程主机如 192.168.0.4:27017,如果端口是27017,端口可以省略。标准连接$m = new Mongo(“mongodb://${username}:${password}@localhost”);实例:$m = new Mongo(“mongodb://127.0.0.1:27017/admin:admin”);数据库的用户名和密码都是admin数据库操作插入数据<?php//这里采用默认连接本机的27017端口,当然你也可以连接远程主机如192.168.0.4:27017//如果端口是27017,端口可以省略$m = new Mongo(“mongodb://127.0.0.1:27017/admin:admin”);//选择comedy数据库,如果以前没该数据库会自动创建,也可以用$m->selectDB(“comedy”);$db = $m->comedy;//选择comedy里面的collection集合,相当于RDBMS里面的表,也可以使用$collection = $db->collection;$db->selectCollection(“collection”);/添加一个元素*****/$obj = array(“title” => “php1”, “author” => “Bill Watterson”);//将$obj 添加到$collection 集合中$collection->insert($obj);/添加另一个元素*****/$obj = array(“title” => “huaibei”, “online” => true);$collection->insert($obj);//$query = array(“title” => “huaibei”);$query = array( “_id” => $obj[’_id’] );$cursor = $collection->find($query);//遍历所有集合中的文档foreach ($cursor as $obj) { echo $obj[“title”] . “\n”; echo $obj["_id"] . “\n”;}//断开MongoDB连接$m->close();带条件的查询mysql: id = 123mongo: array(‘id’=>123)mysql: name link ’%bar%’mongo: array(‘name’ => new MongoRegex(‘/.bar./i’))mysql: where id > 10mongo: array(‘id’ => array(‘$gt’ => 10))mysql: where id >= 10mongo: array(‘id’ => array(‘$gte’ => 10))mysql: where id < 10mongo: array(‘id’ => array(‘$lt’ => 10))mysql: where id <= 10mongo: array(‘id’ => array(‘$lte’ => 10))mysql: where id > 1 and id < 10mongo: array(‘id’ => array(‘$gt’ => 1,’$lt’ => 10))mysql: where id <> 10mongo: array(‘id’ => array(‘$ne’ => 10))mysql: where id in(123)mongo: array(‘id’ => array(‘$in’ => array(1,2,3)))mysql: where id not in(123)mongo: array(‘id’ => array(‘$nin’ => array(1,2,3)))mysql: where id = 2 or id = 9mongo: array(‘id’ => array(‘$or’ => array(array(‘id’=>2),array(‘id’=>9))))mysql: order by name ascmongo: array(‘sort’=>array(‘name’=>1))mysql: order by name descmongo: array(‘sort’=>array(‘name’=>-1))mysql: limit 0,2mongo: array(‘limit’=>array(‘offset’=>0,’rows’=>2))mysql: select name,emailmongo: array(‘name’,’email’)mysql: select count(name)mongo: array(‘COUNT’) //注意:COUNT为大写查询时,每个Object插入时都会自动生成一个独特的_id,它相当于RDBMS中的主键,用于查询时非常方便 (_id每一都不同,很像自动增加的id)<?php$param = array(“name” => “joe”);$collection->insert($param);$joe = $collection->findOne(array("_id" => $param[’_id’]));print_R($joe);$m->close();返回结果:Array ( [_id] => MongoId Object ( [$id] => 4fd30e21870da83416000002 ) [name] => joe )更改字段值<?php$sign = array(“title” => ‘php1’);$param = array(“title” => ‘php1’,‘author’=>’test’);$joe = $collection->update($sign, $param);删除一个数据库$m -> dropDB(“comedy”);列出所有可用数据库$m->listDBs(); //无返回值创建一个MongoDB对象<?php$mo = new Mongo();$db = new MongoDB($mo,’dbname’);//通过创建方式获得一个MongoDB对象删除当前DB<?php$db = $mo->dbname;$db->drop();获得当前数据库名<?php$db = $mo->dbname;$db->_tostring();选择想要的collection://A:$mo = new Mongo();$coll = $mo->dbname->collname;//获得一个collection对象//B:$db = $mo->selectDB(’dbname’);$coll = $db->collname;//C:$db = $mo->dbname;$coll = $db->collname;//D:$db = $mo->dbname;$coll = $db->selectCollectoin(’collname’);//获得一个collection对象插入数据(MongoCollection对象$coll = $mo->db->foo;$a = array(’a’=>’b’);$options = array(’safe’=>true);$rs =$coll->insert($a,$options);删除数据库中的记录(MongoCollection对象)$coll = $mo->db->coll;$c = array(’a’=>1,’s’=>array(’$lt’=>100));$options = array(’safe’=>true);$rs = $coll->remove($c,$options);更新数据库中的记录(MongoCollection对象)$coll = $mo->db->coll;$c = array(’a’=>1,’s’=>array(’$lt’=>100));$newobj = array(’e’=>’f’,’x’=>’y’);$options = array(’safe’=>true,’multiple’=>true);$rs = $coll->remove($c,$newobj,$options);查询collection获得单条记录(MongoCollection类)$coll = $mo->db->coll;$query = array(’s’=>array(’$lt’=>100));$fields = array(’a’=>true,’b’=>true);$rs = $coll->findOne($query,$fields);查询collection获得多条记录(MongoCollection类)$coll = $mo->db->coll;$query = array(’s’=>array(’$lt’=>100));$fields = array(’a’=>true,’b’=>true);$cursor = $coll->find($query,$fields);//排序$cursor->sort(array(‘字段’=>-1));(-1倒序,1正序)//跳过部分记录$cursor->skip(100);跳过100行//只显示部分记录$cursor->limit(100);只显示100行返回一个游标记录对象MongoCursor。针对游标对象MongoCursor的操作(MongoCursor类)$cursor = $coll->find($query,$fields);while($cursor->hasNext()){$r = $cursor->getNext();var_dump($r);}//或者$cursor = $coll->find($query,$fields);foreache($cursor as $k=>$v){var_dump($v);}//或者$cursor = $coll->find($query,$fields);$array= iterator_to_array($cursor);我的博客文章 ...

March 14, 2019 · 2 min · jiezi

使用 Nuxt.js 快速搭建服务端渲染(SSR) 应用

安装 nuxt.jsNuxt.js 官方提功了两种方法来进行项目的初始化,一种是使用Nuxt.js团队的脚手架工具 create-nuxt-app ,一种是根据自己的需求自由配置 使用脚手架适合新手,对 nodejs 后台框架有所了解;按照自己需求自由配置,需要对如何配置 webpack 以及 nodejs 后台框架有所了解。 两种方式比较下就是原生和插件的区别。使用脚手架安装需要有 nodejs 或者 yarn 环境,推荐使用 vscode 自带的控制台输入命令行命令进行操作在有了环境之后直接输入以下命令就可以直接创建一个项目(npx 在npm 5.2.0默认安装,使用最新稳定nodejs环境不用考虑有没有)npx create-nuxt-app <项目名>#或者用yarnyarn create nuxt-app <项目名>之后他会提示你进行一些选择 1.项目名 在这里可以设置项目名,亦可以之后在 package.js 中设置 name 属性,一般是在输入上面命令时的项目名,不需要修改直接回车就好2.项目描述这里是关于项目的描述,比如是做什么的,也可以之后在 package.js 中设置 description 属性3.选择服务器端框架 看自己习惯使用什么了,一般 Express Koa 居多4.扩展插件选择 axios EsLint Prettieraxios 发送HTTP请求EsLint 在保存时代码规范和错误检查自己的代码。Prettier 在保存时格式化/美化自己的代码。5.选择 UI 框架 UI 框架方便快速开发,提供了很多现成的样式,近几年听到最多的就是 Element UI 6.选择测试框架测试框架是用来检测程序有没有到达预期的目的,有没有出错,这里暂时用不到,所以选择 none 就好7.选择渲染模式这里分单页应用(spa)以及普遍的方式(Universal),单页应用有很多路由但是页面只有一个,所有能看到的页面都是 js 即时生成的 dom,第二种是在服务器生成 html ,有多少路由就有多少页面。 使用 nuxt 就是为了解决 SEO 的问题,使其实现所有网站路径完全被收录8.作者这个也可以之后在 package.js 中设置 author 属性 9.选择包管理工具这里选择那个都可以,看自己习惯用哪个10.选择完成开始安装11.安装完成提示信息项目目录关于如何根据自己的需求自由配置,这里不讲,有需要自由配置的一般都不是新手了,推荐看看官方文档添加其他常用功能除了 nuxt 脚手架自带的,我们还需要其他配置,ES6的编译 ,CSS的预处理,其他的用到了再添加安装 babelyarn add babel-cli babel-preset-env配置文件.babelrc{ “presets”: [“env”]}安装 scssyarn add node-sass yarn add sass-loader之后只需要在 vue 文件的 style 标签加一条属性声明下就好<style lang=“sass”></style># or<style lang=“scss”></style> ...

March 13, 2019 · 1 min · jiezi

服务器(CentOS)安装配置mongodb

安装须知mongo DB下载地址mongodb官网下载Linux须知知识:安装过程服务器下载安装包下载: curl -O https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-rhel62-4.0.6.tgz;解压:tar -zxvf mongodb-linux-x86_64-rhel62-4.0.6/;移动到需要存放的目录下mv mongodb-linux-x86_64-rhel62-4.0.6/ /usr/local/mongodb配置环境变量:export PATH=/usr/local/mongodb/bin:$PATH,直接执行此命令,只会创建出临时的环境变量,即重新断开连接服务器后会失效;环境变量持久化配置:需要将mongod路径添加到系统路径中,在/etc/profile文件中,添加 export PATH=/usr/local/mongodb/bin:$PATH;执行source /etc/profile,使系统环境变量立即生效验证是否安装成功:mongod –versiondb version v4.0.6git version: caa42a1f75a56c7643d0b68d3880444375ec42e3OpenSSL version: OpenSSL 1.0.1e-fips 11 Feb 2013allocator: tcmallocmodules: nonebuild environment: distmod: rhel62 distarch: x86_64 target_arch: x86_64mongod启动配置1. 创建数据库存放和日志目录因为MongoDB的数据存储在data目录的db目录下,而该目录在安装过程中并不会自动创建,所以需要手动创建data目录,并在data目录中创建db目录。mongoDB启动默认使用的数据哭存储目录是根目录/data/db;当然也可以在其他目录下创建,然后通过–dbpath来指定;根目录下创建:mkdir -p /data/db;这里为了后期好查找,就不创建在根目录下,而是放在mongodb目录下/usr/local/mongodb/data/db日志目录创建/usr/local/mongodb/logs2. 配置mongod启动文件/usr/local/mongodb/etc下创建配置文件mongod.config:dbpath=/usr/local/mongodb/data # 数据库存放位置(之前创建的)logpath=/usr/local/mongodb/logs/mongodb.log # 数据库日志存放位置(之前创建的)port=27017fork=true #后台运行auth=false # 初次配置先关了权限验证登陆模式journal=false3. 启动mongod启动:mongod -f /usr/local/mongod/etc/mongod.config;进入数据库管理命令界面:mongo创建数据库管理角色db.createUser({user:‘root’,pwd:‘1234567’,roles:[{‘role’:‘userAdminAnyDatabase’,‘db’:‘admin’}]})运行结果:Successfully added user: { “user” : “root”, “roles” : [ { “role” : “userAdminAnyDatabase”, “db” : “admin” } ]}退出服务,谨慎使用kill直接去杀掉mongodb进程,可以使用db.shutdownServer()关闭.使用权限方式启动MongoDB,在配置文件中添加:auth=true , 然后启动:mongod -f /usr/local/mongod/etc/mongod.config进入mongo shell,使用admin数据库use admin并进行验证db.auth(‘root’,‘123456’),验证成功返回1失败返回0;如果不验证或验证失败,是做不了任何操作的4.MongoDB设置为系统服务并且设置开机启动在服务器的系统服务文件中添加mongod配置:vim /etc/rc.d/init.d/mongod,输入:start() {/usr/local/mongodb/bin/mongod –config /usr/local/mongodb/etc/mongod.config}stop() {/usr/local/mongodb/bin/mongod –config /usr/local/mongodb/etc/mongod.config –shutdown}case “$1” in start) start ;;stop) stop ;;restart) stop start ;; *) echo$“Usage: $0 {start|stop|restart}” exit 1esac保存并添加脚本执行权限:chmod +x /etc/rc.d/init.d/mongod;现在可以试试使用service mongod [start|stop|restart|try-restart|reload|force-reload| status]来直接管理MongoDB服务啦;试试关闭服务:[lwh@insnce-4ep /]# service mongod stop2019-03-10T16:45:22.360+0800 I CONTROL [main] log file “/usr/local/mongodb/logs/mongodb.log” exists; moved to “/usr/local/mongodb/logs/mongodb.log.2019-03-10T08-45-22”.killing process with pid: 10652试试开启服务:service mongod start;ok!!其他“积跬步、行千里”—— 持续更新中~,喜欢的话留下个赞和关注哦!往期经典好文:Koa日志中间件封装开发(log4js)团队合作必备的Git操作使用pm2部署node生产环境 ...

March 10, 2019 · 1 min · jiezi

SpringBoot 实战 (十八) | 整合 MongoDB

微信公众号:一个优秀的废人。如有问题,请后台留言,反正我也不会听。前言如题,今天介绍下 SpringBoot 是如何整合 MongoDB 的。MongoDB 简介MongoDB 是由 C++ 编写的非关系型数据库,是一个基于分布式文件存储的开源数据库系统,它将数据存储为一个文档,数据结构由键值 (key=>value) 对组成。MongoDB 文档类似于 JSON 对象。字段值可以包含其他文档,数组及文档数组,非常灵活。存储结构如下:{ “studentId”: “201311611405”, “age”:24, “gender”:“男”, “name”:“一个优秀的废人”}准备工作SpringBoot 2.1.3 RELEASEMongnDB 2.1.3 RELEASEMongoDB 4.0IDEAJDK8创建一个名为 test 的数据库,不会建的。参考菜鸟教程:http://www.runoob.com/mongodb…配置数据源spring: data: mongodb: uri: mongodb://localhost:27017/test以上是无密码写法,如果 MongoDB 设置了密码应这样设置:spring: data: mongodb: uri: mongodb://name:password@localhost:27017/testpom 依赖配置<!– mongodb 依赖 –><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-mongodb</artifactId></dependency><!– web 依赖 –><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency><!– lombok 依赖 –><dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional></dependency><!– test 依赖(没用到) –><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope></dependency>实体类@Datapublic class Student { @Id private String id; @NotNull private String studentId; private Integer age; private String name; private String gender;}dao 层和 JPA 一样,SpringBoot 同样为开发者准备了一套 Repository ,只需要继承 MongoRepository 传入实体类型以及主键类型即可。@Repositorypublic interface StudentRepository extends MongoRepository<Student, String> {}service 层public interface StudentService { Student addStudent(Student student); void deleteStudent(String id); Student updateStudent(Student student); Student findStudentById(String id); List<Student> findAllStudent();}实现类:@Servicepublic class StudentServiceImpl implements StudentService { @Autowired private StudentRepository studentRepository; /** * 添加学生信息 * @param student * @return / @Override @Transactional(rollbackFor = Exception.class) public Student addStudent(Student student) { return studentRepository.save(student); } /* * 根据 id 删除学生信息 * @param id / @Override public void deleteStudent(String id) { studentRepository.deleteById(id); } /* * 更新学生信息 * @param student * @return / @Override @Transactional(rollbackFor = Exception.class) public Student updateStudent(Student student) { Student oldStudent = this.findStudentById(student.getId()); if (oldStudent != null){ oldStudent.setStudentId(student.getStudentId()); oldStudent.setAge(student.getAge()); oldStudent.setName(student.getName()); oldStudent.setGender(student.getGender()); return studentRepository.save(oldStudent); } else { return null; } } /* * 根据 id 查询学生信息 * @param id * @return / @Override public Student findStudentById(String id) { return studentRepository.findById(id).get(); } /* * 查询学生信息列表 * @return */ @Override public List<Student> findAllStudent() { return studentRepository.findAll(); }}controller 层@RestController@RequestMapping("/student")public class StudentController { @Autowired private StudentService studentService; @PostMapping("/add") public Student addStudent(@RequestBody Student student){ return studentService.addStudent(student); } @PutMapping("/update") public Student updateStudent(@RequestBody Student student){ return studentService.updateStudent(student); } @GetMapping("/{id}") public Student findStudentById(@PathVariable(“id”) String id){ return studentService.findStudentById(id); } @DeleteMapping("/{id}") public void deleteStudentById(@PathVariable(“id”) String id){ studentService.deleteStudent(id); } @GetMapping("/list") public List<Student> findAllStudent(){ return studentService.findAllStudent(); }}测试结果Postman 测试已经全部通过,这里仅展示了保存操作。这里推荐一个数据库可视化工具 Robo 3T 。下载地址:https://robomongo.org/download完整代码https://github.com/turoDog/De…如果觉得对你有帮助,请给个 Star 再走呗,非常感谢。后语如果本文对你哪怕有一丁点帮助,请帮忙点好看。你的好看是我坚持写作的动力。另外,关注之后在发送 1024 可领取免费学习资料。资料详情请看这篇旧文:Python、C++、Java、Linux、Go、前端、算法资料分享 ...

March 9, 2019 · 2 min · jiezi

Linux 下安装MongoDB

Linux 下安装MongoDB下载地址, 选择自己需要的版本上传解压,进入mongodb目录创建data目录和log目录mkdir datamkdir logcd datamkdir db创建配置文件cd mongodbmkdir conf cd conftouch mongodb.conf编辑配置文件vim confdbpath = /opt/mongodb/data/db #数据文件存放目录 logpath = /opt/mongodb/log/mongodb.log #日志文件存放目录 port = 27017 #端口 fork = true #以守护程序的方式启用,即在后台运行 启动cd /usr/local/mongodb/ //切换到mongodb./bin/mongod –config ./conf/mongodb.conf //启动服务./mongod -shutdown -dbpath=/usr/local/mongodb/data/db //停止mongodb连接mongodbcd /opt/mongodb/bin./mongo2.安装可视化工具 Studio 3T

March 8, 2019 · 1 min · jiezi

[转载] Redis、MongoDB及Memcached的区别

1 基本概念1.1 Redis(内存数据库)Redis是一个key-value存储系统(布式内缓存,高性能的key-value数据库)。和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set –有序集合)和hash(哈希类型)。这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。在此基础上,redis支持各种不同方式的排序。与memcached一样,为了保证效率,数据都是缓存在内存中。区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。1.2 MongoDB(NoSQL数据库)MongoDB是一个介于关系数据库和非关系数据库之间的产品(基于分布式文件存储的数据库),是非关系数据库当中功能最丰富,最像关系数据库的。他支持的数据结构非常松散,是类似json的bson格式,因此可以存储比较复杂的数据类型。Mongo最大的特点是他支持的查询语言非常强大,其语法有点类似于面向对象的查询语言,几乎可以实现类似关系数据库单表查询的绝大部分功能,而且还支持对数据建立索引。1.3 Memcached(内存Cache)Memcached是一个高性能的分布式内存对象缓存系统,用于动态Web应用以减轻数据库负载。它通过在内存中缓存数据和对象来减少读取数据库的次数,从而提高动态、数据库驱动网站的速度。Memcached基于一个存储键/值对的hashmap。其守护进程(daemon )是用C写的,但是客户端可以用任何语言来编写,并通过memcached协议与守护进程通信。2 特点2.1 Redis支持多种数据结构,如 string(字符串)、list(双向链表)、dict(hash表)、set(集合)、zset(排序set)、hyperloglog(基数估算)。 支持持久化操作,可以进行aof及rdb数据持久化到磁盘,从而进行数据备份或数据恢复等操作,较好的防止数据丢失的手段。 支持通过Replication进行数据复制,通过master-slave机制,可以实时进行数据的同步复制,支持多级复制和增量复制,master-slave机制是Redis进行HA的重要手段。 单线程请求,所有命令串行执行,并发情况下不需要考虑数据一致性问题。 支持pub/sub消息订阅机制,可以用来进行消息订阅与通知。 支持简单的事务需求,但业界使用场景很少,并不成熟。 Redis 只能使用单线程,性能受限于CPU性能,故单实例CPU最高才可能达到5-6w QPS(取决于数据结构,数据大小以及服务器硬件性能,日常环境中QPS高峰大约在1-2w左右)。 依赖快照进行持久化,AOF增强了可靠性的同时,对性能有所影响。 Redis在string类型上会消耗较多内存,可以使用dict(hash表)压缩存储以降低内存耗用。 Memcached和Redis都是Key-Value类型,不适合在不同数据集之间建立关系,也不适合进行查询搜索。比如redis的keys pattern这种匹配操作,对redis的性能是灾难; Redis在2.0版本后增加了自己的VM特性,突破物理内存的限制;可以对key value设置过期时间(类似memcache); Redis事务支持比较弱,只能保证事务中的每个操作连续执行。2.2 MongoDB适合大数据量的存储,依赖操作系统VM做内存管理,吃内存也比较厉害,服务不要和别的服务在一起。支持丰富的数据表达,索引,最类似关系型数据库,支持的查询语言非常丰富。支持master-slave,replicaset(内部采用paxos选举算法,自动故障恢复),auto sharding机制,对客户端屏蔽了故障转移和切分机制。从1.8版本开始采用binlog方式支持持久化的可靠性。MongoDB不支持事务。MongoDB内置了数据分析的功能(mapreduce),其他不支持。2.3 Memcached可以利用多核优势,单实例吞吐量极高,可以达到几十万QPS(取决于key、value的字节大小以及服务器硬件性能,日常环境中QPS高峰大约在4-6w左右)。适用于最大程度扛量。支持直接配置为session handle。只支持简单的key/value数据结构,不像Redis可以支持丰富的数据类型。无法进行持久化,数据不能备份,只能用于缓存使用,且重启后数据全部丢失。无法进行数据同步,不能将MC中的数据迁移到其他MC实例中。内存分配采用Slab Allocation机制管理内存,value大小分布差异较大时会造成内存利用率降低,并引发低利用率时依然出现踢出等问题。需要用户注重value设计。Memcached可以修改最大可用内存,采用LRU算法。3 应用场景3.1 Redis适用于对读写效率要求都很高,数据处理业务复杂和对安全性要求较高的系统(如新浪微博的计数和微博发布部分系统,对数据安全性、读写要求都很高)。3.2 MongoDB主要解决海量数据的访问效率问题。3.3 Memcached动态系统中减轻数据库负载,提升性能;做缓存,适合多读少写,大数据量的情况(如人人网大量查询用户信息、好友信息、文章信息等);用于在动态系统中减少数据库负载,提升性能;做缓存,提高性能(适合读多写少,对于数据量比较大,可以采用sharding)。

March 8, 2019 · 1 min · jiezi

有坑勿踩(三)——关于数据更新

前言数据更新,CRUD中的U,对任何数据库而言都是最基本的操作。看似简单的更新操作中会藏着哪些坑?今天聊一聊这个话题。在写这个系列文章时,我会假设读者已经对MongoDB有了最基础的了解,因此一些基本名词和概念就不做过多的解释,请自己查阅相关资料。数据更新方式以shell为例,MongoDB的数据更新可以使用以下几种方式:db.<collection>.update()db.<collection>.updateMany()db.<collection>.updateOne()db.<collection>.save()db.<collection>.findAndModify()前三种是由于历史原因产生的,实际上:updateMany = update + {multi: true}updateOne = update 或 update + {multi: false}因为update本身的意义不够清楚,所以3.0以后才出现了updateMany和updateOne两个替代方法。这个方法没多少要说的,唯一要注意的就是,如果用update方法的话,不要忘记操作符($set, $inc等等),不然……updateMany和updateOne则没有这个问题,缺了操作符会直接报错。更新操作对比update三兄弟和findAndModify很多人的疑问可能都在这里,它们到底有什么区别,傻傻分不清楚。首先参数不一样:findAndModifyupdate请阅读文档不多赘述。其次功能不一样,update只是更新操作,而findAndModify可以在找到结果后选择执行更新还是删除操作。说白了功能上findAndModify=updateOne+removeOne。注意它只能对单个文档进行操作。无论更新还是删除,(『找到』『更新』)或(『找到』『删除』)都是原子性的,这点findAndModify和updateOne/removeOne没有任何区别。区别只在于findAndModify在完成动作之后还可以选择把更新/删除之前或之后的文档返回给你。如果没有这个操作,那就必须先find再update或者先update再find,无论怎么做,都不能保证中间不被其他操作捷足先登。因此findAndModify在某些场景下是必要的,比如使用$inc生成递增序列(注意生成递增序列做ID不是个好想法,我在这个问题中做过解释)因为findAndModify只针对单个文档,那么如果条件能找到多个文档怎么办?sort就用在这种场景下。update和savesave实际上是一种特殊的update,即不带操作符的update。通俗地说叫『替换』。替换,代表你已经有这个文档完整的样子,即代表你已经把整个文档从数据库中读出来,在内存中进行了修改,然后完整替换回去。你并不能保证数据在被你读出来到写回去期间是否有别人已经改了数据库中的记录,这就是第一个风险,save操作存在潜在的可能性会覆盖掉别人更新过的数据。例如:db.celebrity.findOne(){ _id: “孙悟空”, title: “石猴” age: 500}你执行了:var obj = db.celebrity.findOne({_id: “孙悟空”});obj.title = “弼马温”;// 其他操作db.celebrity.save(obj);在『其他操作』的地方有人把孙悟空的title更新成了『齐天大圣』,很显然在你save的时候你会把它改回『弼马温』。除了上述问题,save还带来一个额外的副作用,因为整个文档都保存进去了,意味着整个文档都会进入oplog,这会显著增加oplog的使用速度。因此过度使用save常常还会造成oplog不够用,需要很大的oplog才能足够保存24小时的信息。

March 8, 2019 · 1 min · jiezi

高性能mongodb之应用程序跑执行计划

执行计划之前发了一篇关于mongodb执行计划的说明。利用执行计划,我们可以判断每一次sql的执行情况和mongodb给出的执行建议。在mongo shell中跑执行计划的命令,举个例子:db.collecitonName.find({}).explain(“queryPlanner”)执行计划的模式为三种:queryPlanner executionStats allPlansExecution。第一种不会真正跑命令本身,只有响应命令分析后的报告。上面例子的响应结果就是对 db.collecitonName.find({}) 这个查询语句的分析。程序中跑执行计划我使用的是java, mongodb库用的是mongodb-java-driver。mongodb-java-driver的API提供了两种方式去跑执行计划:方式一:MongoClient mongoClient = new MongoClient(new ServerAddress(host, port));mongoClient.getDB(“xxx”).getCollection(“yyy”).find(quert).explain();这是一个便捷的方式。这种方式会真正执行命令,也就是说它使用的是executionStats模式。响应结果会有执行时间、扫描记录数等真实的执行情况。如果你的程序想要在命令执行前做一个预判,这个API不是你想要的。方式二:API没有提供queryPlanner的方式。我花了一些时间去搜索资料,发现网上没有跑queryPlanner的需求,至少我是没有找到类似的发问和使用例子。纠结了一会儿,最终发现库里有这样一个api, mongoClient.getDB(“xxx”).command(BasicDBObject command),支持程序传入一个命令。最后在官方文档里找到了这样一个说明:explainNew in version 3.0.The explain command provides information on the execution of the following commands: aggregate, count, distinct, group, find, findAndModify, delete, and update.Although MongoDB provides the explain command, the preferred method for running explain is to use the db.collection.explain() and cursor.explain() helpers.The explain command has the following syntax:语法如下:{ explain: <command>, verbosity: <string>}explain: <command>。 支持 aggregate, count, distinct, group, find, findAndModify, delete, and update等等的命令。verbosity: <string>。支持模式"queryPlanner" 、“executionStats” 、“allPlansExecution” (Default)跟踪find进去,find支持的字段如下,应有尽有。{ “find”: <string>, “filter”: <document>, “sort”: <document>, “projection”: <document>, “hint”: <document or string>, “skip”: <int>, “limit”: <int>, “batchSize”: <int>, “singleBatch”: <bool>, “comment”: <string>, “maxScan”: <int>, // Deprecated in MongoDB 4.0 “maxTimeMS”: <int>, “readConcern”: <document>, “max”: <document>, “min”: <document>, “returnKey”: <bool>, “showRecordId”: <bool>, “tailable”: <bool>, “oplogReplay”: <bool>, “noCursorTimeout”: <bool>, “awaitData”: <bool>, “allowPartialResults”: <bool>, “collation”: <document>}通过阅读文档,跑queryPlanner模式的执行计划应该是这样的:MongoClient mongoClient = MongoUtil.getConnection(mongodb.getHost(), mongodb.getPort(), “”, “”, mongodb.getDb());BasicDBObject command = new BasicDBObject();BasicDBObject find = new BasicDBObject();find.put(“find”, “集合名”);find.put(“filter”, queryCondition);//查询条件,是一个BasicDBObjectcommand.put(“explain”, find);command.put(“verbosity”, “queryPlanner”);CommandResult explainResult = mongoClient.getDB(mongodb.getDb()).command(command); ...

March 7, 2019 · 1 min · jiezi

想了解数据库安全?看这一篇文章就够了!

本文由云+社区发表作者:腾讯云数据库互联网时代,人与人、人与社会交互过程中产生的行为数据、画像数据、信息数据等正在呈指数级增长,同时数据的价值和重要性不言而喻。数据库作为数据的载体,产品和技术也越来越成熟。近几年,不论是商业数据库帝国的蓬勃发展,还是开源数据技术的不断推陈出新,数据库技术的焦点似乎都集中在高性能、低延迟、多场景化应用方面,却很少去关注数据库安全。今天我们来好好聊一聊数据库安全。数据库的安全性是指保护数据库以防止不合法使用所造成的数据泄露、更改或损坏。安全保护措施是否有效是数据库系统的主要技术指标,而数据安全如同一个木桶,整个防护体系是否坚固完全取决于短板。因此即使网络层、操作系统的安全防护已相对完善,如果存放核心信息的数据库得不到应有的保护,同样会造成较为严重的数据安全危机。数据库安全事件时有发生,处理事故时稍有不慎将会酿成灾难性后果。近日,一次波及范围甚广的事故造成大量用户的数据库异常,导致业务停滞,大量网友在微博吐槽致使TO B类业务登上微博热搜榜实属罕见,短时间行业内外用户的朋友圈被事故文章霸屏。这样的数据安全事故正在给高速发展的互联网服务发出一个“数据安全危机”的红色预警。接下来我们一起盘点“数据安全”的几大重灾区——“天灾”(自然灾害、IDC故障)和“人祸”(黑客攻击、数据信息泄露、人为操作失误)。天灾和人祸一、“天灾”火灾、地震、雷击等自然灾害对数据中心造成的物理伤害会导致数据安全危机。比如雷击,轻微情况可引起设备短路故障,严重则会引发火灾。同时IDC也存在着断电、网络故障、设备老化等一系列影响数据安全的因素。某商业银行核心系统数据库中心出现故障,导致存取款、网银、ATM等多项业务中断长达30多个小时,异地分支机构完全依靠手工办理业务。二、“人祸”人为导致的数据安全危机占数据安全故障总数的的70%。怎么样,这个数据是不是触目惊心。其中也可以分为有意操作和误操作。有意操作是指明知道一些操作会造成数据中心故障,仍执意去做的,这些人往往希望通过造成数据库系统运行瘫痪,而达到不可告人的目的。常见的有黑客、情报人员、商业机密小偷等等,他们攻击的对象往往是数据库里的数据。国内知名信息安全团队“雨袭团”发布报告称,在一年半的时间内,高达8.6亿条个人信息数据被明码标价售卖,个人信息泄露造成的总体经济损失达915亿元,在巨额损失背后是隐藏极深却又庞大的黑色产业链,即数据黑产。误操作是指本意并不想破坏数据库系统,但是由于技术积累经验不够或疏忽引发了数据安全故障。这种故障占到了人为故障的80%以上。网上一直以来都个脍炙人口的段子“从删库到跑路”来调侃这一现象。印度McDelivery(麦乐送)应用泄露了220多万麦当劳用户的个人数据。此次用户数据泄露的根源在于McDelivery公开可访问的API端点(用于获取用户详细信息)未受保护。黑客利用该问题枚举该应用的所有用户,并成功窃取了用户的数据。某物流公司工程师在操作删库过程中,错选了RUSS数据库,打算删除执行的SQL。在选定删除时,因其操作不严谨,光标回跳到RUSS库的实例,在未看清所选内容的情况下,便通过执行删除,RUSS库被删去,导致物流系统故障,无法使用并持续约590分钟。该程序员也因为操作问题被“跑路”了。关于数据库安全危机的预防与应对,数据君也走访了诸多初高阶DBA。初级策略:重启系统,重启系统,重启系统,重要的事情说三遍;先冷备恢复,然后从增量log里面恢复实时数据;先策划好方案,没有出来方案之前,先按兵不动,防止二次事故发生;磁盘数据恢复;别自建数据库系统了,用云数据库。不得不说最后一个老兄眼光很是长远~…..高阶观点:数据库系统的监控手段和历史信息记录为系统的稳定运行提供了保障,通过对这些数据分析,不仅可以找到故障原因,还可以根据进行优化,避免发生二次故障;从初期的数据中心规划设计,到机房建成的验收测试,再到机房运营过程中对于机房的定期检测和对于突发状况的预案等,每一项都需要经过严格的审查;禁止使用存储进程,存储进程难以调试和扩展,更没有移植性;数据订正时,要先select,避免误删去,确认无误才能更新句子;重要数据永远不要直接删去,标记为“删去”状态。不能给程序的用户all privileges、delete、drop等高危命令的权限;应用的网络进行分层规划(接入层、应用层、数据层)。数据层只对固定的应用服务器开放,数据库尽量只放在内网;周密的备份,即使管理员跑路也不怕。腾讯云数据库,誓死捍卫数据安全很多用户说了,物理机自建数据库应该会比云数据库更安全吧?数据都在自己手上,总比放在别人篮子里来的靠谱。先不谈数据阴谋论,单就防范黑客恶意窃取数据和防止数据库误操作导致业务停滞的成本来看,云数据库的好处远超自建数据库。腾讯云数据库(TencentDB)在稳定性、可靠性、安全性、扩展性、易用性和成本维度全面秒杀利用云服务器(即腾讯云的CVM)自建的数据库和物理机自建的数据库,具体指标对比参见下图。腾讯云数据库(TencentDB)尤其重视用户数据安全的保护,面对“数据安全”的几大重灾区,提供了全方位的数据安全保障服务。搜索关注腾讯云数据库官方微信,立得10元腾讯云无门槛代金券,体验移动端一键管理数据库,更有从初阶到高阶数据库实战教程等你来约!对于自然灾害和IDC故障这类“天灾”,TencentDB提供了灾备实例/多可用区、数据传输服务DTS、秒级故障切服务帮助用户以较低的成本提升业务连续服务的能力,同时提升数据的可靠性,避免单地IDC故障导致业务完全瘫痪。灾备实例/多可用区 针对业务连续服务和数据可靠性有强需求或是监管需要的场景,TencentDB提供灾备实例/多可用区,可提供跨地域灾备服务(提供实时备份,秒级切换等)。数据传输服务DTS 提供数据迁移、数据同步、数据订阅于一体的数据库数据传输服务,帮助业务不停服的前提下轻松完成数据库迁移,利用实时同步通道轻松构建异地容灾的高可用数据库架构。秒级故障切换 TencentDB会自动处理故障转移,可以快速恢复数据库操作而无需管理干预。出现可用区中断、主数据库实例故障任一条件,主数据库实例会自动切换到备用副本。对于黑客攻击、数据信息泄露、人为操作失误这类“人祸”,TencentDB提供了DDoS 防护、数据加密、数据审计、数据回档等服务。从外部防护、数据安全传输,以及人为故障的监控和恢复方面,为您的云数据库提供完善的安全防护和高效的故障恢复服务,全链路提高数据资产安全。DDoS防护 在用户数据遭到 DDoS 攻击时,能帮助用户抵御各种攻击流量,保证业务的正常运行数据加密 TencentDB提供透明数据加密 TDE 功能,确保落地数据和备份数据的安全。数据审计 数据库审计能够实时记录腾讯云数据库活动,对数据库操作进行细粒度审计的合规性管理,对数据库遭受到的风险行为进行告警,针对数据库 SQL 注入、异常操作等数据库风险行为进行记录与告警。数据回档 可以使用回档工具对腾讯云平台中的数据库或表进行回档操作,回档是基于冷备+binlog,可进行实时数据回档。期间原有数据库或表的访问不受影响。此文已由腾讯云+社区在各渠道发布获取更多新鲜技术干货,可以关注我们腾讯云技术社区-云加社区官方号及知乎机构号

March 5, 2019 · 1 min · jiezi

nodejs+express+mongodb+react+layui完整的小说阅读系统--悦读

一、起源本人是一个前端攻城狮,本着对全栈工程师的向往,学习了nodejs搭建web服务器,根据所学知识自己设计制作了一个简易的小说阅读系统——悦读。这套系统包括:后台服务、数据库存储、后台管理端、客户端APP。后台管理端包括:书籍管理(增删改查)、用户管理(新增、冻结、解冻)客户端包括:注册、登录、添加书架、阅读、分享等二、技术栈服务端:nodejs、express数据库:mongodb后台管理:layui、jquery客户端:react三、开发流程声明:以下安装开发流程均为Windows操作系统下。1.安装nodejsnodejs安装超级简单,前往nodejs官网下载对应版本的nodejs安装包下载完成后点击安装,一直点击next,直到安装完成即可。安装完成后,打开命令行工具(win+r,再输入cmd),在命令行执行node -v命令,若输出版本号则说明安装成功,否则安装失败,自行检查失败原因。2.安装MongoDBnodejs的mongodb模块只是用来连接mongodb数据库的,并不是真正的数据库,下载安装地址[MongoDB][3]2.1下载完成后双击安装,可以选择custom自定义安装目录:2.2点击browser选择安装目录2.3点击next进入下一步,然后取消勾选install mongodb compass,否则可能要很长时间都一直在执行安装,MongoDB Compass 是一个图形界面管理工具,我们可以在后面自己到官网下载安装,下载地址:https://www.mongodb.com/downl…。2.4创建数据目录,MongoDB将数据存储在 db 目录下,但是这个数据目录不会主动创建,我们在安装完成后需要创建data/db目录,请注意,数据目录应该放在根目录下((如: C:datadb 或者 D:datadb 等 )。2.5启动数据库,cd到mongodb安装目录的bin目录中cd c:mongodbbin,执行mongod –dbpath c:datadb,其中c:datadb是你创建的数据存储目录。3.安装expresscd到项目目录下在命令行执行npm install express –save 安装express安装包执行npm install body-parser –save 用于处理 JSON, Raw, Text 和 URL 编码的数据执行npm install cookie-parser –save 解析Cookie的工具。通过req.cookies可以取到传过来的cookie,并把它们转成对象执行npm install multer –save 用于处理 enctype=“multipart/form-data”(设置表单的MIME编码)的表单数据4.配置路由和http设置新建文件app.js,内容如下:var express = require(“express”);var bodyParser = require(“body-parser”);var app = express();//设置跨域访问app.all(’’, function(req, res, next) { res.header(“Access-Control-Allow-Origin”, “”); // res.header(“access-control-expose-headers”, “Authorization”); res.header(“Access-Control-Allow-Headers”, “Content-Type,XFILENAME,XFILECATEGORY,XFILESIZE”); next();});// json类型的bodyapp.use(bodyParser.json());//string类型bodyapp.use(bodyParser.urlencoded({ extended: false}));// 静态文件目录app.use(express.static(__dirname + ‘/public’));// 图书馆系统后台管理端路由与业务逻辑app.use(’/baseWeb/book/’, require(’./routes/baseweb_book’));// 图书馆系统app客户端路由与业务逻辑app.use(’/webphone/bookPhone/’, require(’./routes/webPhone_book’));app.listen(8080);4.nodejs连接mongodb数据库服务,执行npm install mongodb安装依赖包const MongoClient = require(‘mongodb’).MongoClient;const ObjectID = require(‘mongodb’).ObjectID;const url = “mongodb://localhost:27017/books”;MongoClient.connect(url, { useNewUrlParser: true }, function(err, db) { if (err) throw err; const DBO = db.db(“books”); // 在books数据库user表中添加数据 // 插入一条 let data = {name: ’lilei’, sex:1}; DBO.collection(“user”).insertOne(data, function(err, result) { if (err) throw err; console.log(“添加成功”); }); // 插入多条 let data = [ {name: ’lilei’, sex:1}, {name: ‘hanmeimei’, sex:0} ]; DBO.collection(“user”).insertMany(data, function(err, res) { if (err) throw err; console.log(“插入了” + res.insertedCount + “条数据”); }); // 删除数据 // 删除一条 var whereStr = {name: ’lilei’}; //查询条件 DBO.collection(“user”).deleteOne(whereStr , function(err, result) { if (err) throw err; console.log(“删除成功”); }); //删除多条 var whereStr = {name: ’lilei’}; //查询条件 DBO.collection(“user”).deleteMany(whereStr , function(err, result) { if (err) throw err; console.log(“删除成功”); }); // 修改 // 修改一条 let whereStr = {“name”:‘hanmeimei’}; // 查询条件 let updateStr = {$set: { “name” : “missDeng” }}; DBO.collection(“user”).updateOne(whereStr, updateStr, function(err, res) { if (err) throw err; console.log(“更新成功”); }); // 修改多条 let whereStr = {“name”:‘hanmeimei’}; // 查询条件 let updateStr = {$set: { “name” : “missDeng” }}; DBO.collection(“user”).updateMany(whereStr, updateStr, function(err, res) { if (err) throw err; console.log(“更新成功”); }); // 查询 let obj = {};//查询条件,空对象为查询所有 DBO.collection(“user”).find(obj).toArray(function(err, result){ if (err) throw err; console.log(result); });});5.新建routes目录,在routes目录下创建webPhone_book.js文件,内容如下:const express = require(“express”);const fs = require(‘fs’);const path = require(‘path’);const crypto = require(‘crypto’); //加载加密文件const router = express.Router();const MongoClient = require(‘mongodb’).MongoClient;const ObjectID = require(‘mongodb’).ObjectID;const url = “mongodb://localhost:27017/books”;// 缓存区const buf = new Buffer.alloc(2048);// 连接数据库MongoClient.connect(url, { useNewUrlParser: true }, function(err, db) { if (err) throw err; const DBO = db.db(“books”); // app端注册 router.post(’/enroll’, function(req, res){ let data = { userName: req.body.userName, sex: req.body.sex, userPhone: req.body.userPhone, userEmail: req.body.userEmail, password: req.body.password, status: 1 } for(let k in data){ if(!data[k]){ res.json({code:4, content:“参数异常”}); return false } } DBO.collection(“user”).find({userPhone: data.userPhone}).count(function(err, num){ if(err) throw err; if(num == 0){ // 密码加密 let pwObj = encrypt(data.password); data.password = pwObj.pw; data.key = pwObj.key; DBO.collection(“user”).insertOne(data, function(err, result) { if (err){ res.json({code:4, content: “服务器异常”}); throw err; } res.json({code:1, content: “添加成功”}); }) }else{ res.json({code:2, content: “此手机号码已注册”}) } }); }); // APP端验证登录 router.post(’/login’, function(req, res){ let userPhone = req.body.userPhone; let password = req.body.password; if(userPhone && password){ DBO.collection(“user”).find({userPhone: userPhone}).toArray(function(err, resArr) { if (err) throw err; if (resArr.length > 0) { password = password + resArr[0].key; let md5 = crypto.createHash(‘md5’); let r = md5.update(password).digest(‘hex’); if (r == resArr[0].password) { res.json({code: 1,content: resArr[0]._id}); } else { res.json({code: 2,content: “密码错误”}); } } else { res.json({code: 2,content: “该手机号暂未注册”}); } }) }else{ res.json({code: 4, content: “参数异常”}); } }); }); module.exports = router; 6、使用layui创建后台管理前端页面并绑定接口7、使用react创建APP客户端项目,并使用hbuilder打包成apk安装包四、总结与收获之前并未系统学习过服务端开发,所以在开发过程中遇到很多问题,比如:跨域问题、文件读写、上传文件、下载文件、数据库设计等。这些问题并没有让我感到挫败,反而越战越勇,想尽办法一一解决,完成之后部署在云服务器,推荐给朋友使用,成就感爆棚。当然这个系统还是一个新生儿,还有很多不足和需要优化的地方,希望各位朋友不吝赐教。完整项目github地址:https://github.com/jaxlix/-安卓安装包下载二维码: ...

March 4, 2019 · 3 min · jiezi

windows下mongoDB的环境配置

mongoDB下载及安装官网下载安装,可选择安装的具体路径。建立数据文件及启动为了启动mongoDB方便,我们可以将mongo.exe路径加入到环境变量中,电脑->属性->高级系统设置->环境变量,在path中加入路径。这样就可以随处用到mongod命令在D盘新建一个mongoDB的文件夹用来存放数据文件并在其文件夹下建立data,log的文件夹,在log文件夹下建立mongodb.log文件启动mongodb服务,打开命令行输入mongod –dbpath “D:\mongodb\data” –logpath “D:\mongodb\log\mongodb.log” –logappend输入命令行mongo出现以下界面及说明安装成功解析:mongod –dbpath 命令是创建数据库文件的存放位置,启动mongodb服务时需要先确定数据库文件存放的位置,否则系统不会自动创建,启动会不成功。–logpath 表示日志文件存放的路径 –logappend 表示以追加的方式写日志文件每次启动服务都需要输入以上命令,为了方便,可以将启动数据库写成window服务的方式。mongod –dbpath “D:\mongodb\data” –logpath “D:\mongodb\log\mongodb.log” –logappend –directoryperdb –install经过上面的步骤,我们已经将MongoDB的服务注册到系统服务中了,它会随着系统的开机而开启,所以如果我们在再次开机的时候,以管理员身份打开cmd,输入net start MongoDB,会提示我们请求的服务已经启动。因此,下次开机时,我们不需要做任何操作,MongoDB的服务就已经启动了,我们也就可以使用MongoDB了进入mongoDB的cmd操作环境,在以上都配置完的前提下,输入命令行mongo即可进入,ctrl+C是退出环境。其它命令停止服务 net stop MongoDB重启服务 net restart MongoDB卸载服务(先要停止服务)注意:上述命令中的路径和文件名仍要和自己建立的一致,下同 mongod –dbpath “D:\mongodb\data” –logpath “D:\mongodb\log\mongodb.log” –logappend –directoryperdb –remove重装服务mongod –dbpath “D:\mongodb\data” –logpath “D:\mongodb\log\mongodb.log” –logappend –directoryperdb –reinstall

February 27, 2019 · 1 min · jiezi

mongodb数组字段prefix匹配返回

DOC: https://docs.mongodb.com/manu...collection(test)结构{ _id: Objectd(“123456789”), category: [ ‘apple_1’, ‘apple_2’, ‘banana_1’, ‘banana_2’ ]}Question:对test表的所有数据做category过滤,返回category中以apple开头的元素表原数据:[ { _id: Objectd(“id1”), category: [ ‘apple_1’, ‘apple_2’, ‘banana_1’, ‘banana_2’ ] }, { _id: Objectd(“id2”), category: [ ‘apple_3’, ‘apple_4’, ‘banana_1’, ‘banana_2’ ] } …]返回数据示例:[ { _id: Objectd(“id1”), category: [ ‘apple_1’, ‘apple_2’ ] }, { _id: Objectd(“id2”), category: [ ‘apple_3’, ‘apple_4’ ] } …]数据库try:随机构建test表function getRandomArrayElements(arr, count) { var shuffled = arr.slice(0), i = arr.length, min = i - count, temp, index; while (i– > min) { index = Math.floor((i + 1) * Math.random()); temp = shuffled[index]; shuffled[index] = shuffled[i]; shuffled[i] = temp; } return shuffled.slice(min);}var temp = [‘apple_1’,‘apple_2’,‘banana_3’,‘banana_4’,‘pear_5’,‘pear_6’,‘pear_7’];for(var i =0; i < 10; i ++){ db.getCollection(“test”).insert({ category:getRandomArrayElements(temp, Math.random()*7) })}Try 1:db.test.find({},{‘category’:{ ‘$elemMatch’:{ $regex: ‘apple’ }}})返回:[ { _id: Objectd(“id1”), category: [ ‘apple_1’, ] }, { id: Objectd(“id2”), category: [ ‘apple_3’, ] } …]category只保留了符合过滤规则的第一个元素Try 2:db.test.aggregate( { $unwind: ‘$category’ }, { $match: { category: { $regex: ‘apple’ } } }, //unwind,match顺序不能换)返回:[ { _id: Objectd(“id1”), category: ‘apple_1’ }, { _id: Objectd(“id1”), category: ‘apple_2’ }, { id: Objectd(“id2”), category: ‘apple_3’ }, { id: Objectd(“id2”), category: ‘apple_4’ } …]将一个文档拆分成多个文档返回Try 3(Solution):db.test.aggregate({ $project: { “category”:{ $filter: { input: “$category”, as: “cate”, cond: { // category数组元素cate包含字符串’apple’ $eq: [ { $indexOfCP: ["$$cate", “apple”] }, 0] } } } }})返回:[ { _id: Objectd(“id1”), category: [ ‘apple_1’, ‘apple_2’ ] }, { _id: Objectd(“id2”), category: [ ‘apple_3’, ‘apple_4’ ] } …] ...

February 26, 2019 · 2 min · jiezi

磊哥评测之数据库:腾讯云MongoDB vs自建

本文由云+社区发表作者:磊哥上期文章我们聊到了redis。这期我们来说说另一个网红nosql数据库:MongoDB。有这么一个介绍MongoDB的说法是:MongoDB是非关系数据库当中功能最丰富,最像关系数据库的。这么说是因为作为一个面向文档存储型、数据结构非常松散自由的的数据库,却拥有着丰富的功能特性如强大灵活的查询语言、支持二级索引等特性,新版本的MongDB甚至还支持事务。听小伙伴说MongoDB不仅功能丰富,而且读性能强大到远远把MySQL甩在后面,今天我就代替大家来动手进行一下数据库测试,揭开MongoDB的神秘“面纱”。为了进行数据库对比测试,这次我购买了腾讯云MongoDB的主从版(1主2从),同时在同样配置的云主机自建MongoDB作为对比。下面给出CentOS7 64位上安装MongoDB 3.6的实践如下:vim /etc/yum.repos.d/mongod-org.repo编辑内容如下:[mongodb-org]name=MongoDB Repositorybaseurl=https://repo.mongodb.org/yum/…$releasever/mongodb-org/3.6/x86_64/gpgcheck=1enabled=1gpgkey=https://www.mongodb.org/stati…执行指令yum install mongodb-org -y安装vim /etc/mongod.conf 此处根据自己需求修改bindIp: 0.0.0.0 #监听地址 port: 27017 #监听端口systemctl start mongod.service #开启服务netstat -anpt | grep 27017 #检查是否启动服务开启后可以使用上面的指令测试服务是否启动,如果成功启动的话会看到结果如下图所示:如果无法启动,需要根据日志分析具体原因。根据笔者的实践,大部分的原因会落在配置和权限上。如果排除错误太难,建议重新安装来的快一点。接下来需要安装数据库测试工具,这次我们使用YCSB,雅虎开发的一个很强大的测试工具。在安装YCSB前需要安装Java和Maven,测试前需要在workloads文件夹中创建配置文件,配置如下图所示:考虑到购买的mongoDB是副本集配置,一个主节点带两个从节点,我们在本地也配置好副本集群,使用用 ./mongod –replSet amymongo –dbpath /data/27019 –port 27019 –logpath /var/log/mongodb/27019.log –fork 配置从节点,具体配置和初始化方法参考https://cloud.tencent.com/dev…(当然部署在本机的方案不能保证高可用)在workloads中防止配置文件,我们选择插入1千万条记录,执行1千万次操作,测试两种场景:read/update 9:1和纯insert场景。废话少说,下面就一起来看看测试结果吧。场景读更新read/update 9:1,单位ops/sec:场景纯写入insert,单位ops/sec:场景读更新read/update 9:1,单位us(延时):场景纯写入insert,单位us(延时):看来mongodb真的是一个高性能的数据库,为啥呢,因为mongo的延时单位居然是us微秒、微秒、微秒。。。16GB的内存基本上20线程之后延时就会大大增加,在100线程的时候基本上延时基本在1000us以上,而读多场景跟写入场景相比,写入场景的性能略差一点,随着线程数的增大,写入场景的吞吐量和延时表现和读更新场景的差距会扩大。有读者可能会有疑惑,既然数据库测试是比较云和自建,看起来差距也没有那么大,用自建好像也可以接受啊。这里我要把测试中的发现讲给大家听,听完之后大家就明白了。第一点,笔者买的是16G内存的机器(流下了没有钱的泪水),测试的时候发现cvm的内存占用基本到了百分之60左右,笔者在建立副本集和加大测试数据量(购买数据量的百分之80)之后发现,内存占用基本到了百分之80以上。看来mongo的第一个缺点,就是对内存的消耗真的非常可怕!!如果遇到高并发大数据量读写,恐怕分分钟就存在着存在着OOM的风险。所以这里奉劝各位同学,如果要自建MongoDB,还是尽量购买超大内存满足业务需求,避免在业务高峰的时候被“干掉”。如果因为跟笔者一样贫穷不想买那么大的内存,可以考虑使用云数据库,云MongoDB具备动态伸缩能力,即使没有买够大的内存,也完全来得及在业务高峰扩容, 即使发生故障,也有完善的数据自动备份和无损恢复机制来恢复数据,在可用性上保障就高多了。第二点,笔者在后续测试本地副本集的时候,尝试读secondary节点的数据,结果遇到了读延迟很高的情况。在网上研究了一下发现是因为,MongoDB 复制集里 Secondary 不断从主上批量拉取 oplog,然后在本地重放,以保证数据与 Primary 一致。这里为了防止脏读,会加一个锁阻塞所有的读请求。所以如果遇到 Secondary 重放 oplog 占用锁时间长,读取的延时也会对应变长。这个锁最高能锁多久呢,看到有个案例锁了接近一个小时。。。看到的人内心一定是崩溃的,而在云Mongo测试的时候没有遇到这个情况,我想这一定是针对这个缺陷做了很大的改进,使用了其他方法实现同步。总的来说,MongoDB确实可以不借助其他第三方工具实现高可用和分片功能,具备的高可用的故障切换,分片可以实现数据的分部均衡,大数据量的时候通过路由实现了服务器的负载均衡。所以MongoDB自身的可用性较高,也难怪会在短短时间内成为流行的nosql数据库。但是MongoDB也存在着一些坑:如对内存的占用过高、对网络的占用过高、存在从节点锁导致读几乎不可用的情况,这些情况在实际业务使用的时候会导致很严重的问题,集群宕机、服务瘫痪、数据丢失无时不刻不是覆盖在运维同学心头的阴影。这个时候云MongoDB几乎就是救星,弹性伸缩、随时扩容、真正安全的数据热备以及强大的专业运维架构师团队,才能真的确保业务安全无故障的运行下去。写到这里,笔者也在思考,云数据库到底是什么,它仅仅是把数据库封装一下,改改内核,提供给使用者吗?不,云数据库应当是一整套专业服务,除了数据库之外,还有监控、安全、迁移、灾备、运维等一系列的服务提供。能让业务开发专注于业务本身,把专业的交给专业的人去做。此文已由腾讯云+社区在各渠道发布获取更多新鲜技术干货,可以关注我们腾讯云技术社区-云加社区官方号及知乎机构号

February 26, 2019 · 1 min · jiezi

数据工程师妹子养成手记——数据库篇

这篇文章没有代码,请放心阅读。程序员最宝贵的东西是生命,生命属于程序员只有一次。一个程序员的一生应该这样度过:当她回首往事的时候,她不会因为搭建环境浪费时间而悔恨,也不会因为集群无法运行而羞耻。这样,在她开发的时候,她能够说:“我的整个生命和全部精力,都已经献给了开发中最重要的事情——设计程序,实现程序和调Bug。” ——P酱。P酱是公司新来的实习生妹子。听说是一个文科生。文科生应该会去文案组或者策划组吧。什么?来数据组?让我来带?于是我和P酱生活工作在了一起。<!–more–>P酱你会些什么?“我叫P酱,在XX大学读研二,爱好是拍照和被拍,大家可以在B站找到我跳舞的视频,比如av170001。我的另外一个爱好是写代码……”当一个文科妹子说自己喜欢写代码的时候,整个办公室热闹了起来。“P酱,听说你喜欢写代码,那你写过什么东西吗?”新人介绍会议结束以后,我问P酱。“一般都是各种分析程序,我们的专业要做很多调查报告,他们都是用Excel来计算的。我喜欢用Python来把这些统计过程自动化。后来也写过自动写诗的程序、鬼畜视频生成器等等。”“真不敢相信你是文科生。这么说你的兴趣是数据分析方向咯?”“其实我对师父你做的爬虫很有兴趣。但是听说会经常和网站发生对抗?女孩子还是不要打打杀杀的好~”于是我让P酱负责对爬虫的原始数据进行清洗、整理并做简单的分析。一种船新的数据储存方式“P酱,爬虫抓到的原始数据是存放在MongoDB里面的,你的Python还不错吧,你试一试用Python来读写MongoDB看看。”“MongoDB是什么呀?”“是一个和MySQL不太一样的数据库。”“MySQL我知道,MongoDB和MySQL有什么不一样呢?”“我举个例子,当你要插入数据的时候,你需要做的,就是‘插入’。咳咳,你不要脸红,我是指你不需要写SQL语句、不需要建表、不需要提前定义字段。仅仅只需要一行代码就能够实现了。我这里给你写了一篇文档,讲到了MongoDB的增删改查,你试一试。数据库已经给你搭建好了,你直接连上去用吧”半天以后。“师父,我已经会使用MongoDB啦。”“你读爬虫的原始数据,主要涉及到的就是查询操作,为了巩固插入、修改和删除的操作,再给你一个小任务吧。试一试写一个人员管理系统吧。”既然有关系,就整整齐齐放在一起看吧“P酱,你看起来很高兴的样子啊。”“因为我觉得MongoDB比起MySQL太简单了啊~”“你确定?那我看看你怎么对整行数据去重的?”“师父,我知道distinct关键字可以对一个字段去重。但是整行数据我是读出来用Python来去重的。”“这个时候你就要用到MongoDB的聚合查询了。文档已经给你写好了,拿去看吧。”“还有还有,这里你把店铺信息和菜单信息放在了两个集合里面,我怎么样才能把他们联表查询出来呢?”“联表查询是MySQL里面的操作,在MongoDB里面,没有表,只有集合,所以叫做联集合查询更恰当一些。这也是要用到聚合查询,也在这个文档里面了。”再给你一个玩具吧。“P酱,之前让你做的爬虫数据监控系统怎么样了?”“功能已经做好了,但是有一个地方查询起来特别慢。我已经加过索引了,但还是很慢。怀疑是同时联了四个集合的数据造成的。”“这边的数据实际上每小时才更新一次,你没有必要每次刷新页面都去查询MongoDB的。我觉得是时候让你用一下Redis做缓存了。”“Redis就是那个内存数据库吗?我知道我知道。”“给你写了一份文档,包含Redis里面的各种数据类型和使用方式。你试一试把Redis和MongoDB结合起来看看能不能提高速度。”你怎么擅自加功能啊!“P酱,你怎么在爬虫监控系统的网页上加了一个广播窗口?”“呀,被师父发现了。因为我想到同一个爬虫可能会被几个人监控,所以就用Redis的发布订阅功能做出来了这个广播的功能。一旦爬虫状态发生改变,所有人都能收到推送。”“既然你这么闲,那不如加上账号登录功能,把权限验证也做上去?不同的人只能看到自己负责的爬虫。顺便你可以试一试用Redis实现……”“实现布隆过滤器和Session管理是吗?”“你怎么知道我要说什么?”“因为我早上看到你在文档上面更新了布隆过滤器和Session管理相关的内容啊~”红色的锁?“师父师父,你知道什么是RedLock吗?”“你学得这么快?都知道RedLock了?RedLock是Redis官方给出的分布式锁的算法。已经有很多编程语言实现它了。”“原来RedLock只是一个算法啊……”为什么我学的这么快呢?“师父师父,我觉得很奇怪啊,为什么MongoDB和Redis我学得这么快呢?难道是因为他们本来就简单?还是因为我太聪明了?”“为什么你不说是因为你师父教的好呢?““因为这是事实啊不用我说出来”“咳咳,实际上是因为两个原因。一是你一直通过项目驱动来学习,先有需求,然后再去学习实现这个需求所要涉及到的技能。所以你知道你学的东西能用来干什么,自然就能学得快……”“那第二个原因是什么呢?”“第二个原因,我先问你一个问题,你会搭建Redis集群吗?会搭建MongoDB集群吗?知道什么叫做哨兵吗?你知道如何优化MongoDB的启动参数吗?”“这…………好像都不知道额…………”“因为你的角色是数据工程师,不是数据库工程师,所以数据库搭建、底层优化这些内容我都给你跳过了。”“这些听起来都很重要啊,师父你会教我吗?”“你想经常值夜班吗?想半夜3点被人打电话叫起来修数据库吗?认清自己的定位啊,数据库工程师的技能当然很重要,但你是要成为数据工程师的人,技能树应该点在合适的方向。”后记后来,P酱成了别人的女朋友。幸好我还有左手和右手,于是我把我给P酱总结的文档编撰成了《左手MongoDB,右手Redis——从入门到商业实战》这本书。本书现在已经在京东、亚马逊、淘宝上架。这本书的定位是MongoDB和Redis的应用,所以有意弱化了数据库的搭建、维护和底层优化。所以本书可能不适合数据库工程师。希望本书能够给那些一直想掌握MongoDB、Redis,但是又不知道从何处下手的读者,提供一个学习的方向。

February 23, 2019 · 1 min · jiezi

Mongodb数据库安装

通过yum源安装或者通过tar安装包安装创建源仓库文件vi /etc/yum.repos.d/mongodb-org-3.4.repo写入源配置文件[mongodb-org-3.4]name=MongoDB Repositorybaseurl=https://repo.mongodb.org/yum/redhat/$releasever/mongodb-org/3.4/x86_64/gpgcheck=1enabled=1gpgkey=https://www.mongodb.org/static/pgp/server-3.4.ascyum安装yum install mongodb-org -y启动、停止、重启// 启动service mongod start// 停止service mongod stop// 重启命令service mongod restart// 可以通过查看日志文件cat /var/log/mongodb/mongodb.logchkconfig mongod on // 检查是否启动成功使用mongo## 查看数据库show dbs;## 查看数据库版本db.version();## 常用命令帮助db.help();卸载yum erase $(rpm -qa |grep mongodb-org)移除数据库文件和日志文件rm- rr /var/log/mongodbrm -r /var/lib/mongo修改配置文件的 bind_ip, 默认是 127.0.0.1 只限于本机连接。所以安装完成后必须把这个修改为 0.0.0.0 ,否则通过别的机器是没法连接的!MongoDB默认将数据文件存储在/var/lib/mongo目录,默认日志文件在/var/log/mongodb中。如果要修改,可以在 /etc/mongod.conf 配置中指定备用日志和数据文件目录。参考Centos下的Mongodb安装;官方文档;

February 23, 2019 · 1 min · jiezi

高性高mongodb之执行计划

我的专栏地址:我的segmentfault,欢迎浏览一、执行计划介绍MongoDB 3.0之后,explain的返回与使用方法与之前版本有了不少变化,介于3.0之后的优秀特色,本文仅针对MongoDB 3.0+的explain进行讨论。现版本explain有三种模式,分别如下:queryPlannerexecutionStatsallPlansExecution其中 queryPlanner 是现版本explain的默认模式,queryPlanner模式下并不会去真正进行query语句查询,而是针对query语句进行执行计划分析并选出winning plan。举个执行计划的命令例子:db.usertable.find({“w”: 1}).explain(“queryPlanner”)举个执行计划响应结果的例子:{ “queryPlanner”:{ “plannerVersion”:1, “namespace”:“game_db.game_user”, #该值返回的是该query所查询的表 “indexFilterSet”:false, #是否应用了index filter “parsedQuery”:{ #查询条件 “w”:{ “$eq”:1 } }, “winningPlan”:{ #查询优化器针对该query所返回的最优执行计划的详细内容 “stage”:“FETCH”, #最优执行计划的stage,这里返回是FETCH,可以理解为通过返回的index位置去检索具体的文档 “inputStage”:{ # #explain.queryPlanner.winningPlan.stage的child stage,此处是IXSCAN,表示进行的是index scanning。 “stage”:“IXSCAN”, #索引查找 “keyPattern”:{ #所扫描的index内容,此处是w:1与n:1。 “w”:1, “n”:1 }, “indexName”:“w_1_n_1”, #winning plan所选用的index。 “isMultiKey”:false, #本次查询是否使用了多键、复合索引 “direction”:“forward”, #此query的查询顺序,此处是forward,如果用了.sort({w:-1})将显示backward。 “indexBounds”:{ #winningplan所扫描的索引范围,此处查询条件是w:1,使用的index是w与n的联合索引,故w是[1.0,1.0]而n没有指定在查询条件中,故是[MinKey,MaxKey]。 “w”:[ “[1.0, 1.0]” ], “n”:[ “[MinKey, MaxKey]” ] } } }, “rejectedPlans”:[ #其他执行计划(非最优而被查询优化器reject的)的详细返回,其中具体信息与winningPlan的返回中意义相同,故不在此赘述。 { “stage”:“FETCH”, “inputStage”:{ “stage”:“IXSCAN”, “keyPattern”:{ “w”:1, “v”:1 }, “indexName”:“w_1_v_1”, “isMultiKey”:false, “direction”:“forward”, “indexBounds”:{ “w”:[ “[1.0, 1.0]” ], “v”:[ “[MinKey, MaxKey]” ] } } } ] }, “serverInfo” : { “host” : “ALI-SZ-VT-TEST001”, “port” : 27017, “version” : “4.0.5”, “gitVersion” : “3739429dd92b92d1b0ab120911a23d50bf03c412” }, “ok” : 1}二、queryPlanner学习2.1 Stage的意义如explain.queryPlanner.winningPlan.stageexplain.queryPlanner.winningPlan.inputStage**等。stage/inputStage值值的意义COLLSCAN全表扫描IXSCAN索引扫描FETCH根据索引去检索指定documentSHARD_MERGE将各个分片返回数据进行mergeSORT表明在内存中进行了排序(与老版本的scanAndOrder:true一致)LIMIT使用limit限制返回数SKIP使用skip进行跳过IDHACK针对_id进行查询SHARDING_FILTER通过mongos对分片数据进行查询COUNT利用db.coll.explain().count()之类进行count运算COUNTSCANcount不使用用Index进行count时的stage返回COUNT_SCANcount使用了Index进行count时的stage返回SUBPLA未使用到索引的$or查询的stage返回TEXT使用全文索引进行查询时候的stage返回PROJECTION限定返回字段时候stage的返回 2.2一个stage/inputStage的特点执行一:db.usertable.find({“field0”: “use”}).explain(“queryPlanner”){ … “winningPlan” : { “stage” : “COLLSCAN”, “filter” : { “field0” : { “$eq” : “use” } }, “direction” : “forward” }, …}执行二:db.usertable.find({“field0”: “use”}).limit(1).explain(“queryPlanner”){ … “winningPlan” : { “stage” : “LIMIT”, “limitAmount” : 1, “inputStage” : { “stage” : “COLLSCAN”, “filter” : { “field0” : { “$eq” : “use” } }, “direction” : “forward” } }, …}执行二在执行一的基础上增加了 limit限掉, queryPlanner由 stage(COLLSCAN) 变成了 stage(LIMIT)、inputStage.stage(COLLSCAN)。说明在判断queryPlanner是否达到用户想要的效果要对 stageinputStage.stag综合考虑。参考文章:MongoDB干货系列2-MongoDB执行计划分析详解 http://www.mongoing.com/eshu_explain2官方文档: https://docs.mongodb.com/manual/tutorial/analyze-query-plan/ ...

February 22, 2019 · 1 min · jiezi

MongoDB 资源、库、工具、应用程序精选列表中文版

推荐阅读MongoDB 资源、库、工具、应用程序精选列表中文版有哪些鲜为人知,但是很有意思的网站?一份攻城狮笔记每天搜集 Github 上优秀的项目一些有趣的民间故事超好用的谷歌浏览器、Sublime Text、Phpstorm、油猴插件合集目录资源文档文章图书会谈教程更多库CC++C#/.NETDelphiElixirErlangGoHaskellJavaJavaScriptJuliaLispMathematicaPerlPHPPythonRRubyRustScala工具管理数据部署桌面发展监控ShellWeb应用资源文档MongoDB介绍MongoDB文档MongoDB教程更多MongoDB教程 (by Studio 3T)文章带有MongoDB Atlas,Microsoft Azure和无服务器功能的自定义WordPress仪表板(Ahmad Awais)关于扩展MongoDB的五件事(A. Jesse Jiryu Davis,MongoDB Inc.) - Scale 101优化MongoDB复合索引(A. Jesse Jiryu Davis,MongoDB Inc.) - 您需要/必须知道的关于索引的一切PyMongo,Perl和C中的服务器发现和监控(A. Jesse Jiryu Davis,MongoDB Inc.)监控MongoDB性能指标(Jean-Mathieu Saponaro,Datadog)图书Builder Book - 了解如何从头开始构建完整堆栈JavaScript Web应用程序MongoDB应用设计模式(Rick Copeland)Little MongoDB Book - 基本介绍会谈MongoDB架构设计(Tugdual Grall,MongoDB Inc.) [47’]MongoDB的部分和模糊匹配(John Page,MongoDB Inc.) [35’]在Amazon Web Services上扩展MongoDB(Michael Saffitz,Apptentive) [50’]教程使用AngularJS,Node.js和MongoDB创建电视节目追踪器 - 使用Mongoose构建REST API以创建和检索数据Kubernetes示例 - Kubernetes上基本Node.js和MongoDB Web堆栈的部署教程在AWS上部署高度可用的MongoDB副本集 - 有关如何在Amazon Web Services上部署MongoDB副本集的详细指南更多MongoDB源代码MongoDB大学 - 认证和免费在线课程库Cmongo-c-driver - 官方C驱动程序C++mongo-cxx-driver - 官方C ++驱动程序C#/.NETmongo-csharp-driver - 官方C#驱动程序mongo-queue-csharp - MongoDB之上的C#消息队列MongoDB Messaging - 轻量级队列发布/订阅处理库MongoRepository - C#驱动程序之上的存储库抽象层DelphiTMongoWire - 最小的社区Delphi驱动程序Elixirmongodb - 社区Elixir司机mongodb_ecto - Ecto数据库包装器的适配器Erlangmongodb-erlang - 社区Erlang驱动程序Gomgo - 社区围棋司机Haskellmongodb - 社区Haskell司机JavaJongo - 在Java中查询Mongo shellHibernate OGM - 用于NoSQL数据存储的JPA的强大功能和简单性mongo-java-driver - 官方Java驱动程序mongo-queue-java - MongoDB之上的Java消息队列mongoFS - GridFS的增强功能,允许更多功能Mongojack - 基于Jackson,允许您轻松处理您的mongo对象作为POJOMorphia - Java ODM(“对象 - 文档映射器”)Morphium - Java ODM和缓存层Mungbean - 在JVM上运行的语言的社区驱动程序Spring Data MongoDB - 基于Spring的对象文档支持和存储库JavaScriptCamo - 基于类的ES6 ODM,适用于类似Mongo的数据库MEAN.JS - 基于MongoDB,Express,AngularJS和Node.js的完整堆栈MERN(mern-starter) - 基于MongoDB,Express,React和Node.js的完整堆栈Meteor - 基于MongoDB的实时/被动客户端 - 服务器框架,具有许多功能Mongoose - Node.js异步ODMCASL Mongoose - 与Mongoose集成的权限管理库mongration - Node.js迁移框架Moonridge - 在Mongoose和socket.io之上进行实时查询的框架node-mongodb-native - 官方Node.js驱动程序JuliaMongo.jl - C驱动程序绑定Lispcl-mongo - 社区Common Lisp接口mongo-cl-driver社区Common Lisp驱动程序mongo-el - 社区Emacs Lisp驱动程序MathematicaMongoDBLink - 社区Mathematica驱动程序Perlmongo-perl-driver - 官方Perl驱动程序PHPPHPDoctrine MongoDB - 围绕本机PHP Mongo PECL扩展的包装器,以提供附加功能eloquent-mongodb-repository - 基于laravel-mongodb构建的存储库实现laravel-mongodb - Laravel的Eloquent模型和查询构建器mongodb-repository - 存储库实现PHPMongo ODM - ODM基于PHP Mongo PECL扩展PHPMongo Migrator - 基于PHPMongo ODM的迁移工具pecl / mongodb - 官方PHP驱动程序yadm - 快速无模式ODMPythonFlask-Stupe - Flask扩展,为Flask增加了PyMongo支持MongoEngine - 在PyMongo之上的ODMMongoLog - MongoDB日志记录处理程序Mongo-Thingy - 最惯用,最友好但最强大的ODMMotor - 用于Tornado或asyncio的非阻塞Python驱动程序PyMongo - 官方(和推荐)Python驱动程序minimongo - 轻量级,无模式,Pythonic面向对象的接口scrapy-mongodb - 用于Scrapy的MongoDB管道Mongo - 基于marshmallow的驱动程序无关(异步/同步)ODMRmongolite - 快速简单的R客户端Rubyawesome_explain - 一个解释Mongoid查询的简单全局方法mongo-ruby-driver - 官方Ruby驱动程序Mongoid - ODM框架Rustmongo-rust-driver-prototype - Rust 1.x和MongoDB 3.0.x的原型驱动程序Scalamongo-scala-driver - Scala官方驱动程序ReactiveMongo - 非阻塞Scala驱动程序Spark-MongoDB - 使用Spark SQL读/写数据工具管理mongoctl - 使用JSON配置管理MongoDB服务器和副本集MongoDB Smasher - 生成随机数据集并对您的设置进行基准测试mongodb-tools - 三个巧妙的Python脚本,用于处理集合和索引mtools - 用于设置测试环境和可视化日志文件的脚本集合nginx-gridfs - 用于从GridFS提供文件的Nginx模块nginx-mongodb-rest - 作为Nginx模块编写的REST客户端pt-mongodb-query-digest - 从查询分析器聚合查询并报告查询使用情况统计信息pt-mongodb-summary - MongoDB集群状态概述命令行工具服务:撰写 - IBM DBaaS产品(也有其他数据库类型)mLab - 完全管理的DBaaS(以前称为MongoLab)MongoDB Atlas - MongoDB Inc. DBaaS提供(适用于AWS,Azure或GCP)MongoDB云管理器 - MongoDB Inc.数据库管理提供ObjectRocket - Rackspace DBaaS报价(也有其他数据库类型)Scalegrid - 完全托管的DBaaS(可选择自带Azure / AWS账户)数据mongo_fdw - PostgreSQL外部数据包装器mongo-hadoop - Hadoop连接器Mongolastic - MongoDB到Elasticsearch(反之亦然)迁移工具MongoMultiMaster - 多主复制MoSQL - MongoDB到PostgreSQL流复制部署DB — AI Playground - 在线游乐场,用于编写,调试和共享聚合和查询ansible-role-mongodb - Ansible角色chef-mongodb - 厨师食谱Dockerfile头盔图puppet-mongodb - 木偶模块(以前的puppetlabs-mongodb)桌面dbKoda - 跨平台和开源IDEMongoHub - Mac原生客户端Mongotron - 使用Electron构建的跨平台和开源客户端NoSQLBooster - 功能丰富但易于使用的跨平台IDE(以前称为MongoBooster)Nosqlclient - 跨平台,自托管且易于使用的管理工具(以前称为Mongoclient)Robo 3T - 免费,原生和跨平台的以shell为中心的GUI(以前称为Robomongo)Studio 3T - 跨平台GUI,稳定而强大(以前称为MongoChef)发展mgodatagen - 随机数据生成器Mongo Playground - 在线查询游乐场Mongo Seeding - 用于使用JS和JSON文件填充数据库的Node.js库,CLI和Docker映像Mongoeye - 模式和数据分析器:探索集合中的数据多样性 - 模式分析器:查看您的集合中的哪些字段以及它们的内容服务:MongoDB Stitch - MongoDB Inc.无服务器平台提供监控check_mongodb - Nagios插件(在Bash中)Mongoop - 长期运营监控和警报Motop - MongoDB顶级克隆mtop - 另一个顶级克隆mongo-monitor - 简单的监控CLImongo-munin - Munin插件的集合mongomon - 更多Munin插件nagios-plugin-mongodb - Nagios插件(Python)Percona监控和管理 - 用于管理和监控数据库性能的免费和开源平台服务:Datadog - 基于SaaS的监控VividCortex - 基于SaaS的查询性能分析和监控Shellmongo-hacker - MongoDB shell增强功能WebadminMongo - 基于Web的用户界面,用于处理连接和数据库需求mongo-express - 使用Express构建的基于Web的管理界面mongoadmin - 使用Django构建的管理界面mongri - 用JavaScript编写的基于Web的用户界面Rockmongo - 用于MongoDB的PHPMyAdmin,有点像服务:HumongouS.io - 简单的在线GUI和数据可视化仪表板MongoDB Compass - MongoDB Inc.在线GUI和数据可视化平台(具有社区版)应用那些开源应用程序将MongoDB放在堆栈中的某个位置:Builder Book App - 用于发布使用React和Express构建的书籍或文档的Web应用程序CodeCombat - 用于学习如何编码的多人编程游戏Countly - 使用Node.js构建的移动和网络分析和营销平台GrandNode - 使用ASP.NET构建的多平台电子商务购物车Leanote - 用Go构建的Evernote克隆NodeBB - 基于Node.js的论坛软件(“为现代网络构建”)Quokka - 使用Flask构建的Python CMS反应 - 使用ES6构建的事件驱动的实时商务平台SaaS Boilerplate - SaaS产品的Boilerplate,使用TypeScript,React和Express构建正常运行时间 - 使用Node.js和Bootstrap构建的远程监控应用程序LicenseApache License 2.0 ...

February 22, 2019 · 2 min · jiezi

高性能mongodb之利用javascript函数式编程玩转mongodb shell

我的专栏地址:我的segmentfault,欢迎浏览命令一集合记录数列表: db.getCollectionNames().forEach((name) => {print(name+","+db[name].stats().count)})快速展示mongo所有集合和集合的文档数,但是没有做排序。> db.getCollectionNames().forEach((name) => {print(name+","+db[name].stats().count)})log.login_online,2673475log.challenge_result,390836log.order,2674511log.animal,1534481log.animal_arrest,1095140命令二集合按记录条数排序: db.getCollectionNames().map((name) => db[name]).sort((a,b) => {return a.count()-b.count()}).forEach((db) => {print(db.getName()+","+db.count())})快速展示mongo所有集合和集合的文档数,并排好序。运用知识:map、sort、forEach>db.getCollectionNames().map((name) => db[name]).sort((a,b) => {return a.count()-b.count()}).forEach((db) => {print(db.getName()+","+db.count())})log.challenge_result,390836log.animal_arrest,1095140log.animal,1534481log.login_online,2673475log.order,2674511

February 22, 2019 · 1 min · jiezi

MongoDB 分组统计

【摘要】 MongoDB 在进行分组统计时如果面对一些比较复杂的计算情况,往往会遇到 shell 脚本过于复杂的问题。而集算器 SPL 语言,则因其有丰富的函数库及易用性恰好能弥补 Mongo 这方面的不足。若想了解更多,请前往乾学院:MongoDB 分组统计!MongoDB 作为 NoSql 文档型数据库,在全球范围得到广泛的支持与应用。在比较常用的数据库功能中,相对于普通的增删改查,使用 group 聚合分组统计有些复杂,而 MongoDB 也给予了支持。本文将对MongoDb分组的实现方法及示例进行分析,通过在 MongoDB 脚本中操作、使用集算器 SPL 语言操作两种操作途径,进行简单的归纳总结。具体的问题场景包括以下几个方面:1. 内嵌数组结构的统计2. 内嵌文档求和3. 分段分组结构统计4. 多字段分组统计1. 内嵌数组结构的统计对嵌套数组结构中的数据进行统计处理例如查询考试科目的平均分及每个学生的总成绩:测试数据:期待统计结果:Mongodb脚本:由于各科分数 scroe 是按课目、成绩记录的数组结构,统计前需要将它拆解,将每科成绩与学生对应,然后再实现分组计算。这需要熟悉 unwind 与 group 组合的应用。SPL 脚本 (student.dfx):按课目统计的总分数:每个学生的总成绩:脚本说明: A1:连接 mongodb 数据库。 A2:获取 student 表中的数据。 A3:将 scroe 数据合并成序表,再按课程分组,计算平均分。 A4:统计每个学生的成绩后返回列名为 NAME、TOTAL 的序表。new 函数表示生成新序表。 A5:关闭数据库连接。 这个嵌套结构统计的例子比较常见,相信很多人都遇到过,需要先拆解再分组计算,主要是熟悉 mongodb 对嵌套数据结构的处理。2. 内嵌文档求和对内嵌文档中的数据求和处理, 例如统计下面每条记录中 income,output 的数量和。 测试数据:期待统计结果:Mongodb脚本:var fields = [ “income”, “output”];db.computer.aggregate([ { $project:{ “values”:{ $filter:{ input:{ “$objectToArray”:"$$ROOT" }, cond:{ $in:[ “$$this.k”, fields ] } } } } }, { $unwind:"$values" }, { $project:{ key:"$values.k", values:{ “$sum”:{ “$let”:{ “vars”:{ “item”:{ “$objectToArray”:"$values.v" } }, “in”:"$$item.v" } } } } }, {$sort: {"_id":-1}}, { “$group”: { “_id”: “$_id”, ‘income’:{"$first": “$values”}, “output”:{"$last": “$values”} }},]); filter将income,output 部分信息存放到数组中,用 unwind 拆解成记录,再累计各项值求和,按 _id 分组合并数据。SPL脚本:统计结果脚本说明: A1:连接数据库 A2:获取 computer 表中的数据 A3:将 income、output 字段中的数据分别转换成序列求和,再与 ID 组合生成新序表 A4:关闭数据库连接。 获取子记录的字段值,然后求和,相对于 mongo 脚本简化了不少。这个内嵌文档与内嵌数组在组织结构上有点类似,不小心容易混淆,因此需要特别注意与上例中的 scroe 数组结构比较,写出的脚本有所不同。3. 分段分组结构统计统计各段内的记录数量。例如下面按销售量分段,统计各段内的数据量,数据如下:分段方法:0-3000;3000-5000;5000-7500;7500-10000;10000 以上。期望结果:Mongo 脚本这个需求按条件分段分组,mongodb 没有提供对应的 api,实现起来有点繁琐,上面的程序是其中实现的一个例子参考,当然也可以写成其它实现形式。下面看看集算器脚本的实现。SPL脚本:脚本说明: A1:定义 SALES 分组区间。 A2:连接 mongodb 数据库。 A3:获取 sales 表中的数据。 A4:根据 SALES 区间分组统计员工数。其中函数 pseg()表示返回成员在序列中的区段序号,int() 表示转换成整数。 A5:关闭数据库连接。 Mongodb脚本与 SPL 脚本都实现了预期的结果,但函数pseg 的使用让 SPL 脚本精简了不少。4. 多字段分组统计统计分类项下的总数及各子项数。下面统计按 addr 分类的 book 的数量以及其下不同 book 类型的数量。期望结果:Mongo脚本先按 addr,book 分组统计 book 数,再按 addr 分组统计 book 数,调整显示顺序。SPL脚本 (books.dfx):计算结果:脚本说明: A1:连接 mongodb 数据库。 A2:获取 books 表中的数据。 A3:按 addr,book 分组统计 book 数顾。 A4:再按 addr 分组统计 book 数。 A5:将 A4 中的 Total 按 addr 关联后合并到序表中。 B5: 返回序表 A5。 A6:关闭数据库连接。 这个例子中的 SPL 脚本除了一如既往的精简清晰外,还显示了如何简单方便地与 Java 程序集成。 在 Java 程序中如果要对 MongoDB 实现上面的分组统计功能,需要根据不同的需求重新一五一十地实现,比较麻烦的同时也不通用。而如果用集算器来实现就容易多了,集算器提供了 JDBC 驱动程序,支持在 Java 程序中用 JDBC 存储过程方式访问计算结果,调用方法与调用存储过程相同。(JDBC 具体配置参考《集算器教程》中的“JDBC基本使用”章节) Java 调用主要过程如下: public void testStudent (){ Connection con = null; com.esproc.jdbc.InternalCStatement st; try{ // 建立连接 Class.forName(“com.esproc.jdbc.InternalDriver”); con= DriverManager.getConnection(“jdbc:esproc:local://”); //调用存储过程,其中books是 dfx 的文件名 st =(com. esproc.jdbc.InternalCStatement)con.prepareCall(“call books ()”); //执行存储过程 st.execute(); // 获取结果集 ResultSet rs = st.getResultSet(); 。。。。。。。 catch(Exception e){ System.out.println(e); } 可以看到,集算器的计算结果能够很方便地供 Java 应用程序使用。除了上面的调用方式,程序也可以修改成直接加载 SPL 脚本的函数,用 SPL 脚本文件名当参数来实现。同时,集算器也支持 ODBC 驱动,与其它支持 ODBC 的语言集成也与此类似。 简单总结一下,MongoDB 的聚合分组计算的操作与存储文档的结构息息相关,丰富的文档结构一方面有利于存储,同时数据查询展示也可以做到多样化,但另一方面也带来了 shell 脚本操作的复杂性,写起来比较不容易, 需要考虑的细节、步骤也比较多。通过上面这几个简单案例的分析比较,可以看到集算器 SPL 在实现分组统计方面能简化操作,降低难度,从而有效地帮助我们解决问题。 ...

February 18, 2019 · 2 min · jiezi

简化 MongoDB 关联运算

【摘要】MongoDB提供的 lookup 对多表关联实现了基本的支持,但面对一些比较复杂的关联情况,往往会遇到 shell 脚本过于复杂的问题。而集算器 SPL 语言,则因其离散性、易用性恰好能弥补 Mongo 这方面的不足。若想了解更多,请前往乾学院:简化 MongoDB 关联运算!MongoDB属于 NoSql 中的基于分布式文件存储的文档型数据库,这种bson格式的文档结构,更加贴近我们对物体各方面的属性描述。而在使用 MongoDB 存储数据的过程中,有时候难免需要进行关联表查询。自从 MongoDB 3.2 版本后,它提供了 $lookup 进行关联表查询,让查询功能改进了不少。但在实现应用场景中,所遇到的环境错综复杂,问题解决也非易事,脚本书写起来也并不简单。好在有了集算器 SPL 语言的协助,处理起来就相对容易多了。 本文我们将针对 MongoDB 在关联运算方面的问题进行讨论分析,并通过集算器 SPL 语言加以改进,方便用户使用 MongoDB。讨论将分为以下几个部分: 1. 关联嵌套结构情况 1…………………………………………….. 1 2. 关联嵌套结构情况 2…………………………………………….. 3 3. 关联嵌套结构情况 3…………………………………………….. 4 4. 两表关联查询………………………………………………………. 6 5. 多表关联查询………………………………………………………. 8 6. 关联表中的数组查找…………………………………………… 10 Java 应用程序调用 DFX 脚本…………………………………… 121.关联嵌套结构情况 1两个关联表,表 A 与表 B 中的内嵌文档信息关联, 且返回的信息在内嵌文档中。表 childsgroup 字段childs是嵌套数组结构,需要合并的信息 name 在其下。测试数据:history:childsgroup:表History中的child_id与表childsgroup中的childs.id关联,希望得到下面结果:{ “_id” : ObjectId(“5bab2ae8ab2f1bdb4f434bc3”), “id” : “001”, “history” : “today worked”, “child_id” : “ch001”, “childInfo” : { “name” : “a”, “mobile” : 1111 } ………………}Mongo 脚本 这个脚本用了几个函数lookup、pipeline、match、unwind、replaceRoot处理,一般 mongodb 用户不容易写出这样复杂脚本;那么我们再看看 spl 脚本是如何实现的:SPL脚本 ( 文件名:childsgroup.dfx)关联查询结果:脚本说明: A1:连接 mongodb 数据库。 A2:获取 history 表中的数据。 A3:获取 childsgroup 表中的数据。 A4:将 childsgroup 中的 childs 数据提取出来合并成序表。 A5:表 history 中的 child_id 与表 childs 中的 id 关联查询,追加 info 字段, 返回序表。 A6:关闭数据库连接。 相对 mongodb 脚本写法,SPL 脚本的难度降低了不少,思路也更加清晰,也不需要再去熟悉有关 mongo 函数的用法,以及如何去组合处理数据等,节约了不少时间。2.关联嵌套结构情况 2两个关联表,表 A 与表 B 中的内嵌文档信息关联, 将信息合并到内嵌文档中。表 txtPost 字段 comment 是嵌套数组结构,需要把 comment_content 合并到其下。txtComment:txtPost期望结果:Mongo 脚本表txtPost 按 comment 拆解成记录,然后与表 txtComment 关联查询,将其结果放到数组中,再将数组拆解成记录,将comment_content 值移到 comment 下,最后分组合并。SPL 脚本:关联查询结果:脚本说明: A1:连接 mongodb 数据库。 A2:获取 txtPost 表中的数据。 A3:获取 txtComment 表中的数据。 A4:将序表 A2 下的 comment 与 post_no 组合成序表,其中 post_no 改名为 pno。 A5:序表 A4 通过 comment_no 与序表 A3 关联,追加字段 comment_content,将其改名为 Content。 A6:按 pno 分组返回序表,~ 表示当前记录。 A7:关闭数据库连接。 Mongo、SPL 脚本实现方式类似,都是把嵌套结构的数据转换成行列结构的数据,再分组合并。但 SPL 脚本的实现更简单明了。3.关联嵌套结构情况 3两个关联表,表 A 与表 B 中的内嵌文档信息关联, 且返回的信息在记录上。表collection2字段product是嵌套数组结构,返回的信息是isCompleted等字段。测试数据: collection1: { _id: ‘5bc2e44a106342152cd83e97’, description { status: ‘Good’, machine: ‘X’ }, order: ‘A’, lot: ‘1’ }; collection2: { _id: ‘5bc2e44a106342152cd83e80’, isCompleted: false, serialNo: ‘1’, batchNo: ‘2’, product: [ // note the subdocuments here {order: ‘A’, lot: ‘1’}, {order: ‘A’, lot: ‘2’} ] }期待结果 { _id: 5bc2e44a106342152cd83e97, description: { status: ‘Good’, machine: ‘X’, }, order: ‘A’, lot: ‘1’ , isCompleted: false, serialNo: ‘1’, batchNo: ‘2’ }Mongo 脚本lookup 两表关联查询,首个 addFields获取isCompleted数组的第一个记录,后一个addFields 转换成所需要的几个字段信息SPL脚本:脚本说明: A1:连接 mongodb 数据库。 A2:获取 collection1 表中的数据。 A3:获取 collection2 表中的数据。 A4:根据条件 order, lot 从序表 A2 中查询记录,然后追加序表 A3 中的字段 serialNo, batchNo,返回合并后的序表。 A5:关闭数据库连接。 Mongo、SPL 脚本都实现了预期的结果。SPL 很清晰地实现了从数据记录中的内嵌结构中筛选,将符合条件的数据合并成新序表。4.两表关联查询从关联表中选择所需要的字段组合成新表。Collection1: collection2:期望结果:Mongo 脚本lookup 两表进行关联查询,redact 对记录根据条件进行遍历处理,project 选择要显示的字段。SPL脚本:脚本说明: A1:连接 mongodb 数据库。 A2:获取c1表中的数据。 A3:获取c2表中的数据。 A4:两表按字段 user1,user2 关联,追加序表 A3 中的 output 字段,返回序表。 A5:关闭数据库连接。 Mongo、SPL 脚本都实现了预期的结果。SPL 通过 join 把两个关联表不同的字段合并成新表,与关系数据库用法类似。5.多表关联查询多于两个表的关联查询,结合成一张大表。Doc1: Doc2: Doc3:合并后的结果: { “_id” : ObjectId(“5901a4c63541b7d5d3293766”), “firstName” : “shubham”, “lastName” : “verma”, “address” : { “address” : “Gurgaon” }, “social” : { “fbURLs” : “http://www.facebook.com”, “twitterURLs” : “http://www.twitter.com” } } Mongo 脚本 由于 Mongodb 数据结构原因,写法也多样化,展示也各不相同。SPL脚本: Mongo、SPL 脚本都实现了预期的结果。此 SPL 脚本与上面例子类似,只是多了一个关联表,每次 join 就新增加字段,最后叠加构成一张大表。 SPL 脚本的简洁性、统一性非常明显。6.关联表中的数组查找从关联表记录数据组中查找符合条件的记录, 用给定的字段组合成新表。测试数据:users:workouts:期望结果:Mongo 脚本把关联表 users,workouts 查询结果放到数组中,再将数组拆解,提升子记录的位置,去掉不需要的字段。 SPL脚本 (users.dfx):脚本说明: A1:连接 mongodb 数据库。 A2:获取users表中的数据。 A3:获取workouts表中的数据。 A4:查询序表 A3 的 _id 值存在于序表A2中 workouts 数组的记录, 并追加 name 字段。返回合并的序表。 A5:关闭数据库连接。 由于需要获取序列的交集不为空为条件,故将 _id 转换成序列。 Mongo、SPL 脚本都实现了预期的结果。从脚本实现过程来看,SPL 集成度高而又不失灵活性,让程序简化了不少。7.Java 应用程序调用 DFX 脚本 在通过 SPL 脚本对 MongoDB 数据进行了关联计算后,其结果可以被 java 应用程序很容易地使用。集算器提供了 JDBC 驱动程序,用 JDBC 存储过程方式访问,与调用存储过程相同。(JDBC 具体配置参考《集算器教程》中的“JDBC基本使用”章节) Java调用主要过程如下: public void testUsers(){ Connection con = null; com.esproc.jdbc.InternalCStatement st; try{ //建立连接 Class.forName(“com.esproc.jdbc.InternalDriver”); con= DriverManager.getConnection(“jdbc:esproc:local://”); //调用存储过程,其中 users是 dfx 的文件名 st =(com. esproc.jdbc.InternalCStatement)con.prepareCall(“call users> ()”); //执行存储过程 st.execute(); //获取结果集 ResultSet rs = st.getResultSet(); 。。。。。。。 catch(Exception e){ System.out.println(e); } 可以看到,使用时按标准的 JDBC 方法操作,集算器很方便嵌入到 Java 应用程序中。同时,集算器也支持 ODBC 驱动,因此集成到其它支持 ODBC 的语言也非常容易。 Mongo 存储的数据结构相对关系数据库更复杂、更灵活,其提供的查询语言也非常强、适应面广,同时需要了解函数也不少,函数之间的结合更是变化无穷,因此要熟练掌握并应用也并非易事。集算器的离散性、易用性恰好能弥补 Mongo 这方面的不足,在降低 mongo 学习成本及使用复杂度、难度的同时,让 mongo 的功能得到更充分的展现。 ...

February 13, 2019 · 3 min · jiezi

mongoDB原生查询与spring data mongoDB的对象

一、按照in、eq、lte等条件组合查询,同时添加sort和limit1、原生db.message.find( { receiverRoleId: {$in: [1381073, 1381073]}, resourceType:3, sendTime: {$lte: 1523355918300} }) .sort({sendTime: -1 }) .limit(10);2、spring data mongoDBCriteria criteria = Criteria.where(“receiverRoleId”).in(receiverRoleIds) .and(“readState”).is(readState.getIndex()) .and(“sendTime”).lte(sendTime);Query query = Query.query(criteria);query.with(new Sort(Sort.Direction.DESC, “sendTime”));query.limit(count);mongoTemplate.find(query, Notification.class);二、执行update操作,更新单个文档1、原生db.message.update( {_id: 586537, readState: 2}, {$set: {readState: 1}}, {multi: false});2、spring data mongoDBCriteria criteria = Criteria.where("_id").is(id).and(“readState”).is(ReadState.UNREAD.getIndex());Query query = Query.query(criteria);Update update = Update.update(“readState”, ReadState.READ.getIndex());mongoTemplate.updateFirst(query, update, Notification.class);三、通过findAndModify命令更新文档并且返回更新之后的文档(只能作用于单个文档)1、原生db.message.findAndModify({ query: {_id: 586537, readState: 2}, update: {$set: {publishType: 1}}, new: true});2、spring data mongoDBQuery query = Query.query(Criteria.where("_id").is(2).and(“readState”).is(2));Update update = Update.update(“publishType”, 1);Notice updateResult = mongoTemplate.findAndModify( query, update, FindAndModifyOptions.options().returnNew(true), Notice.class);四、聚合操作(根据某一字段group,并且将文档中的某一字段合并到数组中,最后取数组中的第一个元素)1、原生db.message.aggregate([ { $match: {toAffairId : {$in: [590934, 591016]}} }, { $sort: {sendTime: -1} }, { $group: {_id: “$toAffairId”, contents: {$push: “$content”}} }, { $project: {_id: 0, “affaiId”: “$_id”, contents: {$slice: ["$contents", 1]} } }]);2、spring data mongoDBCriteria criteria = Criteria.where(“toAffairId”).in(affairIds);Aggregation aggregation = Aggregation.newAggregation( match(criteria), sort(Sort.Direction.DESC, “sendTime”), group(“toAffairId”).push(“content”).as(“contents”),AggregationResults<MobileDynamicMessageDataModel> results = mongoTemplate.aggregate( aggregation, collectionName, MobileDynamicMessageDataModel.class); ...

January 29, 2019 · 1 min · jiezi

前端修炼之路

一步,两步,三步四步五步,就这样到达了人生的巅峰传统前端生态-初级不使用打包、中间处理工具,手工处理js、css、图片等资源掌握以下知识点:基础结构:html,h5基础样式:css,css3基础语法:jsjq框架:jq,jq系列插件ui框架:bootstrap3,bootstrap4等基础插件:lodash,echarts等其他:移动端适配,浏览器兼容,浏览器调试等恭喜完成新手村修炼任务,正式开启江湖冒险之旅vue 生态-中级基础套件:vue2,vuex,vue-router,axios,vue 静态生成器,vue 多页面,vue history 模式UI 组件库:element、iview、vuex、mint-ui、vant、cube-ui等后端渲染:nuxt.js语法升级:es6+,less、scss、styls工具:vue-cli,babel7,webpack4,eslint,vue devtools其他:调试工具,vuepress打怪升级,恭喜晋级成功React 生态基础套件:react,react-router,jsx,axios,react react history 模式,redux、mobx等UI 组件库:antd,antd-mobile后端渲染:next.js工具:create-react-app,react-devtoolsreact-native持续打副本刷经验中微信生态-高级小程序生态:api,组件,工具,云开发,mpvue、wepy等小游戏生态微信公众号生态:微信授权登录、支付、分享等常用功能闯关成功,成功晋级Node 生态-全栈基础:node,npm、cnpm、npx、nvm、yarn,pm2,pug,nginx框架:express,koa2,think.js,egg.js数据库:mongoodb,mongoose插件:redis、express-redis-cache、Redis Desktop Manager、moment、nodemailer等命令行:linux、cmd、mac 命令工具:express-generator,koa-generator,robo3t,postman,mobaXterm,Sequel Pro击败终极 Boss,成功完成凡间界修炼,开始新世界之旅其他常用工具:编辑器 sublime 研究,ps切图工具,抓包工具,上传代码工具,图片压缩工具,截图工具,取颜色工具,markdown等git 类:svn,git,github,码云,sourceTree埋点:谷歌统计,百度统计,前端错误收集等其他:pwa,parcel,typescript,electron,可视化,工程化,单元测试,前端安全,性能优化,数据结构+算法,设计模式等

January 28, 2019 · 1 min · jiezi

MongoDB 高级查询

参考官方文档(图文并茂非常好看):Getting Started - MongoDB DocumentationMongoDB的查询功能非常强大,同时有些地方也会有点复杂。所以需要下点功夫学习和操练才能用好。关于Mongo Shell当我们进入Mongo Shell客户端后,实际上是进入了一个Javascript语言的交互环境。也就是说,MongoDB中的很多命令,尤其是包括定义函数等高级命令,实际上都是Javascript语言,甚至说可以是jQuery。了解了这点,一些高级命令如Aggregation学起来就会放松很多。官方说明:基本查询功能比较运算: 等于$lt: Less Than$gt: Greater Than$gte: Greater Than or Equal$ne: Not Equal# age大于等于18db.mycollection1.find( { age:{$gt: 18} } )逻辑运算$and$ordb.mycollection1.find( { $or: [ { age: {$gte: 20} }, { salary: {$gt: 5000} }, { job: “HR” } ]} )范围运算$in$nin: Not Indb.mycollection1.find( { age: { $in: [10, 20, 30] }} )正则表达式有两种方法:/表达式内容/{$regex: “表达式内容”}db.mycollection1.find( { name: /^Ja\w+$/} )# 或db.mycollection1.find( { name: { $regex: “/^Jaso\w?$” }} )limit和skip# 限定显示条数db.mycollection1.find().limit(数量)# 跳过指定第几条数据db.mycollection1.find().skip(2)# 混合使用db.mycollection1.find().limit(10).skip(3)自定义函数查询自定义查询是指使用自定义函数,格式为$where: function(){…}db.mycollection1.find( { $where: function() { return this.age >= 18; }} )投影即搜索的返回值中,只显示指定的某些字段。字段指为0的不现实,指为1的显示,默认为1。# 格式为:db.mycollection1.find( {查询条件}, {显示与否的选项})# 如:db.mycollection1.find( {}, { _id: 0, name: 1, age: 1 })排序可以按指定的某些字段排序,字段标记为1的为Asc升序,标记为-1的为Desc降序。db.mycollection1.find().sort({ name:1, age:-1 })统计使用count()函数。db.mycollection1.find().count()db.mycollection1.count( {查询条件} )消除重复使用distinct()函数。# 格式为:db.集合名.distinct( “指定字段”, {查询条件} )# 如db.mycollection1.distinct( “job”, { age: {$lt: 40} } )聚合管道 AggregationAggregation是MongoDB特有的一种Pipline管道型、聚合查询方式。语法稍微复杂一些。聚合管道可以达到多步骤的分组、筛选功能。这个管道中的每一个步骤,成为一个stage。常用的管道有:$match:简单的根据条件过滤筛选$group:将数据分组,一般配合一些统计函数,如$sum。$project:修改document的结构。如增删改,或创建计算结果$lookup:$unwind:将List列表类型的Document进行拆分$sort$limit$skip语法格式为:db.集合名.aggregate( [ {管道表达式1}, {管道表达式2}, {管道表达式2}] )示例:db.Orders.aggregate( [ {$match: { status: “A” } }, {$group: { _id: “$cut_id”, total: { $sum: “$amount” } } }] )管道的Map Reduce ...

January 24, 2019 · 1 min · jiezi

一篇文章入门MongoDB

MongoDB既是NoSQL数据库,又是内存数据库,而且它是现在最强大、最流行的NoSQL数据库。区别与别的NoSQL数据库,MongoDB主要是基于Documents文档(即一条JSON数据)的。MongoDB的特点:NoSQL数据库内存数据库存储基于JSON或BSON支持丰富的高级查询命令基于Javascript语法对数据之间关系的支持比较弱支持map-reduce的运算框架支持GirdFS的分布式文件系统MongoDB持久化MongoDB虽然是内存数据库,但是它主要是将数据存储在硬盘的,所有要操作的数据通过mmap的方式映射到内存某个区域内。所以相对于Redis的真·内存数据库而言,MongoDB只是将大部分的操作数据存在内存中。Mac中,Mongodb的数据存储位置默认为:/usr/local/var/mongodb。里面名称类似collection-4-3122184014923990948.wt即为一个collection。安装Ubuntu安装:$ sudo apt-get install mongodb-orgMac安装:$ brew install mongodb# 启动mongodb服务$ brew services start mongodb#或前端启动$ mongod –config /usr/local/etc/mongod.conf配置MongoDB的配置文件在/etc/mongod.conf。常用的配置项有:# 默认端口27107# 日志位置 /var/log/mongodb/mongod.logMongo Shell (Javascript)当我们进入MongoDB客户端后,实际上是进入了一个类Javascript语言的Shell交互环境。也就是说,MongoDB中的很多命令,尤其是包括定义函数等高级命令,实际上都是Javascript语言。了解了这点,一些高级命令如Aggregation学起来就会放松很多。常用命令# 服务端启动$ mongod /etc/mongod.conf# 服务端启停 (Ubuntu上)$ sudo service mongod start$ sudo service mongod stop$ sudo service mongod restart# 进入客户端mongo进入mongo客户端后,就进入了shell交互页面了。常用的命令如下(注意mongodb区分大小写):# 显示当前数据库db# 显示所有数据库show databases# 或show dbs# 切换数据库use 数据库名# 删除当前数据库db.dropDatabase()集合首先要记住,MongoDB中,有这么几个概念:Database 数据库:即一个NoSQL的非关系型数据库Collection 集合: 即代替传统"表格"概念的一个集合,收集性质相似的一些数据,如员工集合,或产品集合。Document 文档: 即一个标准JSON格式数据。相当于传统数据库的一行record数据。在关系数据库中,是以表为一个数据集。而MongoDB中,是以Collection为一个数据集:其中每一个配有_id的记录,就是一个Document文档。相当于一条记录。可以看出,MongoDB对数据之间事务关系支持比较弱,如果业务这一方面要求比较高的话,MongoDB还是并不适合此类型的应用。创建集合:不用手动创建集合,当第一条数据插入时,集合自动就生成了。当然,手动创建也是可以的:db.createCollection( “集合名”, {各种属性设置} )# 如 (capped表示是否设置容量上限)db.createCollection( “Products”, {capped: true, size: 1000} )# 查看所有集合show collections# 删除集合db.集合名称.drop()数据类型在MongoDB中的一个Document,即一个JSON格式的文档中,每个值都是要指定数据类型的。现有数据类型如下:Object ID:文档ID,相当于传统表中的表主键ID。可以自己创建如果自己不设置,MongoDB会自动生成一个名为_id的"主键"StringBooleanIntegerDoubleArray:列表,可以存储多个值。Object:可以嵌套另一个子Document文档,或说JSON数据。TimestampDateNull数据操作# 插入数据db.集合名称.insert( {数据} )# 如db.mycollection1.insert( {“name”: “Jason”, “age”: 18} )# 或db.mycollection1.insert( {name: “Jason”, age: 18} )# 显示集合中所有数据db.集合名.find()# 修改数据 (如果不存在对应的ID,则创建一条新数据)db.集合名.save( {"_id": “ID号”, 数据} )# 更新单条数据db.集合名.update( {查询条件}, {更新项目} )# 如db.mycollection1.update( {name:“Jason”}, {age:30} )# 或db.mycollection1.update( {name:“Jason”}, { $set:{age:30} } )# 更新多条数据 (使用"multi"选项)db.mycollection1.update( {job: “HR”}, {salary: 8000}, {multi: true} )# 删除单条数据db.mycollection1.remove( {查询}, {justOne: true} )# 删除多条数据 (“justOne"选项默认为false)db.mycollection1.remove( {查询} )MongoDB数据备份和恢复注意:MongoDB导出时候不是单文件,而是巨多JSON和BSON文件。# 备份$ mongodump -h 主机IP:端口 -d 数据库名 -o 导出路径# 如$ mongodump -h 192.168.1.101:27017 -d mydb1 -o /var/db/mongodb/# 从本机恢复$ mongorestore -d mydb1 –dir /var/db/mongodb/ ...

January 24, 2019 · 1 min · jiezi

手把手教你用koa+mongoodb实现自己的接口

实际前端开发中我们常常需要模拟数据,市场上有许多工具提供使用但是基本都是提供数据展示,如果我们想要一个具备增删改查功能的接口怎么办呢?当然是强大自己啦!!!首先我们需要新建一个目录mkdir koa-project安装koakoa 依赖node V7.6.0及以上版本, 首先确认node版本在7.6.0以上,版本低的请自行搞定cd koa-projectnpm initnpm install koa –save写个demo试一试mkdir koa-apicd koa-apitouch index.jsconst Koa = require(‘koa’)const app = new Koa()app.use(async(ctx)=>{ ctx.body = ‘Hello World’})app.listen(3000,()=>{ console.log(‘服务已经启动’)})这个时候打开浏览器输入localhost://3000你会发现界面已经出现了程序员标配的“Hello World”,当然这个时候你可以在ctx.body后面放上json数据这样就已经达到了大部分接口模拟工具实现的功能,但是我们能满足于此嘛?不,我们要让这个接口实现增删改查的功能,这个时候我们就需要一个数据库了,所以接下来我们要白活一个数据库了安装mongoodb去官网下载MongoDB,https://www.mongodb.com/ 然后傻瓜式安装即可(这里推荐一个安装教程http://www.runoob.com/mongodb…)运行mongoodb记住这里我默认你已经配置好环境变量了,如果安装出现问题请自行谷歌,没有的话直接输入mongod既可启动mongoodb服务器安装mongoosenpm install mongoose –save万事俱备,起锅烧油,用mongoose连接数据库mkdir databasecd databasetouch init.js今日有事,有空继续更新,会尽快。。。

January 24, 2019 · 1 min · jiezi

在Node.js下运用MQTT协议实现即时通讯及离线推送

前言前些日子了解到mqtt这样一个协议,可以在web上达到即时通讯的效果,但网上并不能很方便地找到一篇目前版本的在node下正确实现这个协议的博客。自己捣鼓了一段时间,理解不深刻,但也算是基本能够达到使用目的。本文尚未对离线消息的接收顺序进行处理。代码服务端: server.js//服务端引入中间件moscalet mosca = require(‘mosca’)let settings = { port: 5112}let server = new mosca.Server(settings)server.on(‘ready’, function(){ console.log(‘Mosca server is up and running at port 5112’); })server.on(‘published’, function(packet, client) { console.log(‘Published’, packet.payload)})server.on(‘clientDisconnected’, function(client){ console.log(‘disconnected: ‘, client.id)})推送端: pub.js//客户端引入mqttlet mqtt = require(‘mqtt’);let client = mqtt.connect(‘mqtt://localhost’, { port: 5112, clientId: ‘cli_pub’,})let num = 0;setInterval(function (){ client.publish(’test’, ‘Hello mqtt ’ + (++num), {qos:1}, () => console.log(num));}, 1000)订阅端: sub.jslet mqtt = require(‘mqtt’)let client = mqtt.connect(‘mqtt://localhost’, { port: 5112, clientId: ‘cli_sub’,})client.subscribe(’test’,{qos:1})client.on(‘message’, function (topic, message) { console.log(‘received message: ‘, message.toString())})server运行后,先启动pub,再启动sub,即可在sub中接收到推送过来的消息序列至此实现了简单的即时推送离线推送相关配置及简要介绍离线配置-服务端:要实现消息的离线推送,必然需要一个存储临时数据的部件此处用到的是mongo,当然可以根据需要选择其他的存储工具server.js中的settings需更改为:let settings = { port: 5112, persistence:{ //增加了此项 factory: mosca.persistence.Mongo, url: “mongodb://localhost:27017/mosca” }}factory: 引入mosca对特定存储工具的一些处理方法url: 其中的 27017 为mongo所监听的端口号,mosca为存储相关数据的数据库值得一提的是:配置好mongo的环境后,不需要提前在mongo中手动创建,若数据库不存在会自动生成,而且mosca会为你作好其他一切基本事项 (即:若只想临时体验下效果,甚至可以暂时把mongo放一边 ) 在mongo中,可以看到自动新添了db: mosca及其下的collection(相当于关系型数据库中的表/关系)离线配置-客户端:pub.js和sub.js中的client中都可以改为:let client = mqtt.connect(‘mqtt://localhost’, { port: 5112, clientId: ‘cli_**’, clean: false//增加了此项})clientId: 区分客户端的识别码clean: 此处决定了客户端在服务端的session是否会被清除,默认为true,为实现离线推送,我们需要将其保留clean及上文中的persistence为实现离线推送的关键配置mqtt.connect()会返回一个mqttClient对象,包含了:reconnect(), subscribe(), publish()等一系列方法。本文中发送端接收端被分为了pub.js和sub.js两个独立文件,仅仅为了方便在不同控制台中观察效果一个client可以既为推送端,又为订阅端至此,所有代码已完成其他介绍:client.subscribe():为本客户端订阅一个话题,所有订阅此话题的用户都会收到在此话题下推送的信息//client.subscribe(topic,opts)client.subscribe(’test’,{qos:1})opts中的qos为通信机制,控制发送端与接收端的互锁程度上文中的其中一个collection: subscriptions即记录各用户话题订阅情况用户cli_sub及cli2_sub订阅了话题test:(新增一个cli2_pub,下文有用)注:重复执行脚本sub.js实际上对topic进行了重复订阅实际编码时,应避免topic的重复订阅,即使重复订阅并不影响实现效果client.publish():向指定topic发送数据message为Buffer或String格式,可以通过序列化或转json实现对复杂数据对象的传送//client.publish(topic, message, opts, callback)let num = 0;setInterval(function (){ client.publish(’test’, ‘Hello mqtt ’ + (++num), {qos:1}, () => console.log(num));}, 1000)参数不再赘述此处用一个定时器定时在 topic: test 下发送’Hello mqtt 1,2,3..‘用回调函数实时打印一下发送的num:当订阅者处于离线状态时,可以在collection: packets中查看到临时数据的存储情况:mosca把每一条推送消息为所有订阅用户都生成了独立的记录,用同一个messageId进行关联当其中一个用户(cli2_sub)上线时,获取到其对应的数据,而后数据库中相应记录便会被删除此时仅有cli_sub用户的数据当cli2_sub上线接收消息后,packets中记录将被清空client.on():即在client上触发的事件,此处只列举消息接收事件//client.on(event, callback)client.on(‘message’, function (topic, message) { console.log(‘received message: ‘, message.toString())})处理为简单地打印到控制台附mosca.js文档:https://www.npmjs.com/package…mqtt.js文档:https://www.npmjs.com/package…windows环境下mongo的配置:https://jingyan.baidu.com/art…及一位前辈的文章:https://www.jianshu.com/p/831…转载请注明出处 ; ) ...

January 24, 2019 · 1 min · jiezi

Mac环境下手把手教你如何使用mongoDB+Robo 3T

1.在Mac下安装MongoDB用homebrew安装最简单,推荐使用此方法安装sudo brew install mongodb安装完成后,运行mongod会发现报错,别慌报错原因是因为你没有data/db这个文件夹所以执行以下代码sudo mkdir -p /data/dbsudo chown ‘username’ /data/db完成后,再执行mongod大功告成2.接下来安装 Robo 3T官网:https://robomongo.org/download接下来安装就好了记住开软件连接数据库时,命令行一定要启动mongod

January 21, 2019 · 1 min · jiezi

mongodb如何实现数组对象求和

原本地址:mongodb如何实现数组对象求和mongodb在计算集合数组值时候,我们通常会想到使用$group与$sum,但是如果是数组里面多个json对象,并且还需要根据条件过滤多个对象的内容该如何处理?现在让我们来实现它,假设mongodb中有个user集合,其数据内容如下:/* 1 /{ “_id” : ObjectId(“5c414a6a0847e00385143003”), “date” : “2019-01-18 09”, “data” : [ { “app_platform” : “ios”, “user” : 3028 }, d { “app_platform” : “android”, “user” : 4472 }, ]}…现在我们需要计算date日期为"2019-01-18 09"并且app_platform的类型为"ios"的user总数如果可以,请先思考下mongodb语句如何实现。实现过程中有个执行非常重要,即$unwind,官方解释:Deconstructs an array field from the input documents to output a document for each element. Each output document is the input document with the value of the array field replaced by the element.从输入文档中解构一个数组字段,为每个元素输出一个文档。每个输出文档都是输入文档,数组字段的值由元素替换。于是我们便想到将data数组对象分条拆开,化繁为简,mongodb语句如下:db.getCollection(‘user’).aggregate([ { $project: { _id: 1, data: 1, date: 1} }, { $match: {“date”: “2019-01-18 09”} }, { $unwind: “$data” },])得到结果如下:/ 1 /{ “_id” : ObjectId(“5c414a6a0847e00385143003”), “date” : “2019-01-18 09”, “data” : { “app_platform” : “ios”, “user” : 3028 }}/ 2 /{ “_id” : ObjectId(“5c414a6a0847e00385143003”), “date” : “2019-01-18 09”, “data” : { “app_platform” : “android”, “user” : 4472 }}可以看到数据由数组变成了多条文档数据,于是问题转变为计算结果的user总数,是不是觉得问题变简单了,而且我们也可以继续使用$match来过滤app_platform数据,mongodb语句如下:db.getCollection(‘user’).aggregate([ { $project: { _id: 1, data: 1, date: 1} }, { $match: {“date”: “2019-01-18 09”} }, { $unwind: “$data” }, { $match: { “data.app_platform”: { $in: [“ios”]} }, }])执行结果如下:/ 1 /{ “_id” : ObjectId(“5c414a6a0847e00385143003”), “date” : “2019-01-18 09”, “data” : { “app_platform” : “ios”, “user” : 3028 }}可以看到数据已经被过滤了,如果自信观察两个$match的作用可以发现,mongodb是按顺序执行的,即$match作用于其前面的操作结果集合让我们继续计算,此时只需要使用group与sum对data里的user字段求和即可,mongodb语句如下:db.getCollection(‘user’).aggregate([ { $project: { _id: 1, data: 1, date: 1} }, { $match: {“date”: “2019-01-18 09”} }, { $unwind: “$data” }, { $match: { “data.app_platform”: { $in: [“ios”]} } }, { $group: { _id: null, “user”: {$sum: “$data.user”}} }])结果如下:/ 1 */{ “_id” : null, “user” : 7500}计算得出的user即我们所需要的数据。其实所有的难点如下:计算数组对象数据时将其转变为多条简单的数据格式,$unwind指令将问题轻松得降低了难度mongodb的执行顺序,$project,$match都是顺序执行并作用于之前的操作结果理解了这两点,相信再难的mongodb语句你也能实现。happy coding! ...

January 21, 2019 · 2 min · jiezi

如何保证MongoDB的安全性?

上周写了个简短的新闻《MongoDB裸奔,2亿国人求职简历泄漏!》:根据安全站点HackenProof的报告,由于MongoDB数据库没有采取任何安全保护措施,导致共计202,730,434份国人求职简历泄漏。然后很多人评论说MongoDB躺枪了。MongoDB确实躺枪了,因为这事的责任当然不在数据库,而在于使用数据库的人没有做必要的安全配置。那么我们应该如何保证MongoDB的安全性?下面我将介绍保护MongoDB的3个简单的方法:绑定局域网IP,杜绝互联网访问配置防火墙,保护27017端口配置账号密码,对数据库进行访问控制本教程所使用的系统配置如下:Ubuntu 16.04mongodb 4.0.5Ubuntu 16.04安装MongoDB参考MongoDB文档:Install MongoDB Community Edition on Ubuntusudo apt-key adv –keyserver hkp://keyserver.ubuntu.com:80 –recv 9DA31620334BD75D9DCB49F368818C72E52529D4echo “deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu xenial/mongodb-org/4.0 multiverse” | sudo tee /etc/apt/sources.list.d/mongodb-org-4.0.listsudo apt-get updatesudo apt-get install -y mongodb-org=4.0.5 mongodb-org-server=4.0.5 mongodb-org-shell=4.0.5 mongodb-org-mongos=4.0.5 mongodb-org-tools=4.0.5sudo service mongod start1. 绑定局域网IP,杜绝互联网访问话说MongoDB被黑了这么多年,自身确实有一定的责任。版本3.6之前,MongoDB默认绑定的居然是0.0.0.0,这就意味着我们可以通过互联网访问MongoDB,那黑客当然也可以。这样的默认配置是一个很大的安全漏洞,很多MongoDB初学者都栽在这一点。关于这个问题,MongoDB的文档说得很委婉:Default Bind to LocalhostStarting with MongoDB 3.6, MongoDB binaries, mongod and mongos, bind to localhost by default. From MongoDB versions 2.6 to 3.4, only the binaries from the official MongoDB RPM (Red Hat, CentOS, Fedora Linux, and derivatives) and DEB (Debian, Ubuntu, and derivatives) packages would bind to localhost by default.也就是说,从3.6开始,MongoDB默认绑定localhost,这就意味着我们只能在本机访问MongoDB。至于2.6到3.4,只有从MongoDB RPM与DEB下载的安装包才默认绑定localhost,换句话说,其他方式下载的安装包则默认绑定0.0.0.0。因此,如果你使用的MongoDB是3.6之前的版本,就要特别注意这一点了。在开发环境下,MongoDB绑定localhost没毛病。但是,在生产环境下,我们通常会有多个节点,这时需要修改MongoDB绑定的IP,通过配置net.bindIp可以实现。如果为了省事,直接把net.bindIp配置为0.0.0.0,那就不太妙了。正确的做法应该是绑定局域网IP,这样只有局域网内的节点可以访问MongoDB。除非黑客端掉了你的服务器,否则他是没法访问你的MongoDB的。哪些IP是局域网的呢?按照标准,有下面这些网段:10.0.0.0 – 10.255.255.255172.16.0.0 – 172.31.255.255192.168.0.0 – 192.168.255.255最常用的局域网网段就是192.168.0.0到192.168.255.255了。修改MongoDB的配置文件vim /etc/mongod.conf将net.bindIp设为局域网IP地址192.168.59.99:net: port: 27017 bindIp: 192.168.59.99重启MongoDBsudo service mongod restart2. 配置防火墙,保护27017端口MongoDB默认使用的是27017端口,我们应该配置本地防火墙把这个端口保护起来,禁止外部IP访问。在MongoDB绑定0.0.0.0,且没有配置防火墙的情况下,使用nmap命令远程扫描27017端口,结果如下:nmap -p 27017 113.207.35.149Starting Nmap 6.49BETA3 ( https://nmap.org ) at 2019-01-19 14:17 CSTNmap scan report for 113.207.35.149Host is up (0.042s latency).PORT STATE SERVICE27017/tcp open mongodNmap done: 1 IP address (1 host up) scanned in 14.34 seconds可知,27017端口是"open"的,这就意味着我们可以远程访问MongoDB数据库。配置UFW防火墙Ubuntu上默认的防火墙软件是UFW,配置起来非常简单。默认情况下,ufw并没有激活:sudo ufw statusStatus: inactive执行以下命令,即可配置ufw规则,并启动防火墙:sudo ufw default deny incoming // 默认禁止访问本机所有端口sudo ufw default allow outgoing // 允许本机访问外部网络sudo ufw allow 22/tcp // 允许SSH登陆sudo ufw allow from 192.168.59.100 to any port 27017 // 仅允许局域网内IP为192.168.59.100的服务器访问mongodbsudo ufw enable我所配置的规则也非常容易理解,根据命令就能看出来。这时,再查看ufw的状态,可以发现防火墙已经激活了:sudo ufw statusStatus: activeTo Action From– —— —-22/tcp ALLOW Anywhere80/tcp ALLOW Anywhere443/tcp ALLOW Anywhere27017 ALLOW 192.168.59.10022/tcp (v6) ALLOW Anywhere (v6)这时,再使用nmap命令远程扫描27017端口,结果如下:nmap -p 27017 113.207.35.149Starting Nmap 6.49BETA3 ( https://nmap.org ) at 2019-01-19 14:40 CSTNmap scan report for 113.207.35.149Host is up (0.053s latency).PORT STATE SERVICE27017/tcp filtered mongodNmap done: 1 IP address (1 host up) scanned in 13.68 seconds可知,27017端口的状态为"filtered",已经被防火墙保护起来了,更加安全。Linux上常用的防火墙工具还有iptables,这里就不再赘述了。另外,云服务器都支持配置防火墙,也有必要配置一下,它们与本机的防火墙是独立的,可以共同来保证数据库的安全。3. 配置账号密码,对数据库进行访问控制默认情况下,MongoDB并没有配置账号和密码,黑客只要登陆你的服务器之后可以直接查看数据库。给MongoDB配置账号密码,可以有效解决这个问题。连接mongodbmongo配置账号密码账号为"myUserAdmin",密码为"abc123"。use admindb.createUser( { user: “myUserAdmin”, pwd: “abc123”, roles: [ { role: “userAdminAnyDatabase”, db: “admin” }, “readWriteAnyDatabase” ] })修改MongoDB的配置文件vim /etc/mongod.conf将security.authorization设为"enabled":security: authorization: enabled重启MongoDBsudo service mongod restart连接mongodb再次连接mongodb时,则需要指定账号与密码。mongo -u “myUserAdmin” -p “abc123” –authenticationDatabase “admin"如果不提供账号密码,则无法查看数据库,会出现如下这种错误:show dbs2019-01-20T22:13:53.477+0800 E QUERY [js] Error: listDatabases failed:{ “ok” : 0, “errmsg” : “command listDatabases requires authentication”, “code” : 13, “codeName” : “Unauthorized”}另外,MongoDB还支持配置多个权限不同的账号,针对性地对特定数据库的读写权限进行配置。这样更加细致的访问控制可以增强安全性,举个不太恰当的例子,对于团队中的实习生,应该只给他们读权限,这样可以有效防止出现误操作导致删库等极端情况。总结可以发现,本文介绍的方法都非常简单,属于常识,但是都是必要的。作为数据库管理者,如果这些都没有配置,那显然是非常不专业的,责怪MongoDB也没有用,因为换个数据库也会有同样的问题。根据MongoDB文档提供的Security Checklist,我们还可以使用TLS/SSL来加密MongoDB连接,这样做会在一定程度上牺牲性能,大家可以根据需要来配置。另外,保证数据库的访问安全非常重要,同时也需要保证数据的安全性,做好必要的数据备份。关于如何保护数据的安全性,可以参考我们的博客《Fundebug是这样备份数据的》。参考MongoDB裸奔,2亿国人求职简历泄漏!Fundebug是这样备份数据的关于FundebugFundebug专注于JavaScript、微信小程序、微信小游戏、支付宝小程序、React Native、Node.js和Java线上应用实时BUG监控。 自从2016年双十一正式上线,Fundebug累计处理了9亿+错误事件,付费客户有Google、360、金山软件、百姓网等众多品牌企业。欢迎大家免费试用!版权声明转载时请注明作者Fundebug以及本文地址:https://blog.fundebug.com/2019/01/21/how-to-protect-mongodb/ ...

January 21, 2019 · 2 min · jiezi

windows下mongodb的zip包安装

第一步:下载去官网下载mondodb的zip包,mongodb是一个基于分布式文件存储的开源数据库系统。第二步:解压将下载的zip包解压在相应目录(你想装在那个目录就解压到那个目录)。第三步:添加配置给mongodb一个配置文件,一般数据库都是有配置文件的,我们在首次启动mondodb的时候需要给mongodb相关配置,其中配置包括:数据存储在哪个目录日志保存在哪个目录日志相关的其他配置… …操作:在解压到的目录新建一个mongo.conf(配置文件名一般以.conf为扩展名,当然也可以是.config为扩展名)文件,打开文件后在文件中进行相关配置如下:dbpath=d:\mongodb\data #此为数据存储的路径 logpath=d:\mongodb\logs\mongo.log #此为日志存储的路径 journal=true #启用日志文件,默认就为开启 logappend=true #错误日志采用追加的形式 quiet=true #是否过滤无用日志 port=27017 #端口号 注意:以上配置的logpath(用来存放日志)在相关目录一定要有mongo.log文件,首次启动应该需要手动创建一个。第四步:启动mongodb在解压的目录的bin目录下打开cmd命令行,因为bin目录下才有mongod.exe可执行文件,在命令行输入mongod –config d:\mongodb\mongo.conf注意:后面的路径是mongo.conf文件的路径。此时就已经配置好mongodb了。打开浏览器在url框中输入localhost:27017浏览器显示如下信息就说明配置好了,也已经成功启动了mongodb:It looks like you are trying to access MongoDB over HTTP on the native driver port.此时mongodb就已经成功启动了,可以使用Navicat连接mongodb了(cmd命令行不能关闭,关闭后也就关闭了mongodb的服务了)。第五步:使用mongodb在下次使用mongodb的时候,需要再次启动mongoldb,再次启动mongodb的方法和首次启动mongodb的方式相差不多,在bin目录下打开命令行,输入:mongod –dbpath d:\mongodb\data就可以启动mongodb了。这只是用来启动mongodb的服务命令,之后就可以使用Navicat连接mongodb了。如果要在命令行操作mongodb,需要在bin目录下新打开一个cmd命令行执行mongo命令就可以使用mongodb的命令了。第六步:安装到windows服务中细心的同学也可能会发现,我们每次启动mongodb都需要进到mongodb的bin目录下下打开命令行敲键盘输入:mongod –dbpath d:\mongodb\data况且我们都已经在配置文件中配置了dbpath的路径,但每次在启动的时候还是要输入dbpath,因此我们可以把mongodb的服务安装到windows service进程中,具体操作:在bin目录下打开命令行输入一下命令即可:mongod –congif d:\mongodb\mongo.conf –install此时打开资源管理器进入服务就可以看到MongoDB的服务已经在Windows Service里面了,下次再启动mongodb的时候就可以在任意位置打开命令行输入net start mongodb就可以了,而且这个窗口可以关闭,关闭后mongodb服务仍在启动。net stop mongodb用来关闭windows service中的mongodb服务。如果要移除windows service中的mongodb服务,则在bin目录下打开命令行输入:mongod –remove就可以移除windows服务中的mongodb服务了。文中若有表述不妥或是知识点有误之处,欢迎留言指正批评!

January 15, 2019 · 1 min · jiezi

SpringBoot之MongoTemplate的查询可以怎么耍

学习一个新的数据库,一般怎么下手呢?基本的CURD没跑了,当可以熟练的增、删、改、查一个数据库时,可以说对这个数据库算是入门了,如果需要更进一步的话,就需要了解下数据库的特性,比如索引、事物、锁、分布式支持等本篇博文为mongodb的入门篇,将介绍一下基本的查询操作,在Spring中可以怎么玩原文可参看: 190113-SpringBoot高级篇MongoDB之查询基本使用姿势I. 基本使用0. 环境准备在正式开始之前,先准备好环境,搭建好工程,对于这一步的详细信息,可以参考博文: 181213-SpringBoot高级篇MongoDB之基本环境搭建与使用接下来,在一个集合中,准备一下数据如下,我们的基本查询范围就是这些数据1. 根据字段进行查询最常见的查询场景,比如我们根据查询user=一灰灰blog的数据,这里主要会使用Query + Criteria 来完成@Componentpublic class MongoReadWrapper { private static final String COLLECTION_NAME = “demo”; @Autowired private MongoTemplate mongoTemplate; /** * 指定field查询 / public void specialFieldQuery() { Query query = new Query(Criteria.where(“user”).is(“一灰灰blog”)); // 查询一条满足条件的数据 Map result = mongoTemplate.findOne(query, Map.class, COLLECTION_NAME); System.out.println(“query: " + query + " | specialFieldQueryOne: " + result); // 满足所有条件的数据 List<Map> ans = mongoTemplate.find(query, Map.class, COLLECTION_NAME); System.out.println(“query: " + query + " | specialFieldQueryAll: " + ans); }}上面是一个实际的case,从中可以知道一般的查询方式为:Criteria.where(xxx).is(xxx)来指定具体的查询条件封装Query对象 new Query(criteria)借助mongoTemplate执行查询 mongoTemplate.findOne(query, resultType, collectionName)其中findOne表示只获取一条满足条件的数据;find则会将所有满足条件的返回;上面执行之后,删除结果如query: Query: { “user” : “一灰灰blog” }, Fields: { }, Sort: { } | specialFieldQueryOne: {_id=5c2368b258f984a4fda63cee, user=一灰灰blog, desc=帅气逼人的码农界老秀}query: Query: { “user” : “一灰灰blog” }, Fields: { }, Sort: { } | specialFieldQueryAll: [{_id=5c2368b258f984a4fda63cee, user=一灰灰blog, desc=帅气逼人的码农界老秀}, {_id=5c3afaf4e3ac8e8d2d39238a, user=一灰灰blog, desc=帅气逼人的码农界老秀3}, {_id=5c3afb1ce3ac8e8d2d39238d, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=18.0}, {_id=5c3b0031e3ac8e8d2d39238e, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=20.0}, {_id=5c3b003ee3ac8e8d2d39238f, user=一灰灰blog, desc=帅气逼人的码农界老秀6, sign=hello world}]2. and多条件查询前面是只有一个条件满足,现在如果是要求同时满足多个条件,则利用org.springframework.data.mongodb.core.query.Criteria#and来斜街多个查询条件/* * 多个查询条件同时满足 /public void andQuery() { Query query = new Query(Criteria.where(“user”).is(“一灰灰blog”).and(“age”).is(18)); Map result = mongoTemplate.findOne(query, Map.class, COLLECTION_NAME); System.out.println(“query: " + query + " | andQuery: " + result);}删除结果如下query: Query: { “user” : “一灰灰blog”, “age” : 18 }, Fields: { }, Sort: { } | andQuery: {_id=5c3afb1ce3ac8e8d2d39238d, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=18.0}3. or或查询and对应的就是or,多个条件中只要一个满足即可,这个与and的使用有些区别, 借助org.springframework.data.mongodb.core.query.Criteria#orOperator来实现,传参为多个Criteria对象,其中每一个表示一种查询条件/* * 或查询 /public void orQuery() { // 等同于 db.getCollection(‘demo’).find({“user”: “一灰灰blog”, $or: [{ “age”: 18}, { “sign”: {$exists: true}}]}) Query query = new Query(Criteria.where(“user”).is(“一灰灰blog”) .orOperator(Criteria.where(“age”).is(18), Criteria.where(“sign”).exists(true))); List<Map> result = mongoTemplate.find(query, Map.class, COLLECTION_NAME); System.out.println(“query: " + query + " | orQuery: " + result); // 单独的or查询 // 等同于Query: { “$or” : [{ “age” : 18 }, { “sign” : { “$exists” : true } }] }, Fields: { }, Sort: { } query = new Query(new Criteria().orOperator(Criteria.where(“age”).is(18), Criteria.where(“sign”).exists(true))); result = mongoTemplate.find(query, Map.class, COLLECTION_NAME); System.out.println(“query: " + query + " | orQuery: " + result);}执行后输出结果为query: Query: { “user” : “一灰灰blog”, “$or” : [{ “age” : 18 }, { “sign” : { “$exists” : true } }] }, Fields: { }, Sort: { } | orQuery: [{_id=5c3afb1ce3ac8e8d2d39238d, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=18.0}, {_id=5c3b003ee3ac8e8d2d39238f, user=一灰灰blog, desc=帅气逼人的码农界老秀6, sign=hello world}]query: Query: { “$or” : [{ “age” : 18 }, { “sign” : { “$exists” : true } }] }, Fields: { }, Sort: { } | orQuery: [{_id=5c3afb1ce3ac8e8d2d39238d, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=18.0}, {_id=5c3b003ee3ac8e8d2d39238f, user=一灰灰blog, desc=帅气逼人的码农界老秀6, sign=hello world}, {_id=5c3b0538e3ac8e8d2d392390, user=二灰灰blog, desc=帅气逼人的码农界老秀6, sign=hello world}]4. in查询标准的in查询case/* * in查询 /public void inQuery() { // 相当于: Query query = new Query(Criteria.where(“age”).in(Arrays.asList(18, 20, 30))); List<Map> result = mongoTemplate.find(query, Map.class, COLLECTION_NAME); System.out.println(“query: " + query + " | inQuery: " + result);}输出query: Query: { “age” : { “$in” : [18, 20, 30] } }, Fields: { }, Sort: { } | inQuery: [{_id=5c3afb1ce3ac8e8d2d39238d, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=18.0}, {_id=5c3b0031e3ac8e8d2d39238e, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=20.0}]5. 数值比较数值的比较大小,主要使用的是 get, gt, lt, let/* * 数字类型,比较查询 > /public void compareBigQuery() { // age > 18 Query query = new Query(Criteria.where(“age”).gt(18)); List<Map> result = mongoTemplate.find(query, Map.class, COLLECTION_NAME); System.out.println(“query: " + query + " | compareBigQuery: " + result); // age >= 18 query = new Query(Criteria.where(“age”).gte(18)); result = mongoTemplate.find(query, Map.class, COLLECTION_NAME); System.out.println(“query: " + query + " | compareBigQuery: " + result);}/* * 数字类型,比较查询 < /public void compareSmallQuery() { // age < 20 Query query = new Query(Criteria.where(“age”).lt(20)); List<Map> result = mongoTemplate.find(query, Map.class, COLLECTION_NAME); System.out.println(“query: " + query + " | compareSmallQuery: " + result); // age <= 20 query = new Query(Criteria.where(“age”).lte(20)); result = mongoTemplate.find(query, Map.class, COLLECTION_NAME); System.out.println(“query: " + query + " | compareSmallQuery: " + result);}输出query: Query: { “age” : { “$gt” : 18 } }, Fields: { }, Sort: { } | compareBigQuery: [{_id=5c3b0031e3ac8e8d2d39238e, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=20.0}]query: Query: { “age” : { “$gte” : 18 } }, Fields: { }, Sort: { } | compareBigQuery: [{_id=5c3afb1ce3ac8e8d2d39238d, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=18.0}, {_id=5c3b0031e3ac8e8d2d39238e, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=20.0}]query: Query: { “age” : { “$lt” : 20 } }, Fields: { }, Sort: { } | compareSmallQuery: [{_id=5c3afb1ce3ac8e8d2d39238d, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=18.0}]query: Query: { “age” : { “$lte” : 20 } }, Fields: { }, Sort: { } | compareSmallQuery: [{_id=5c3afb1ce3ac8e8d2d39238d, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=18.0}, {_id=5c3b0031e3ac8e8d2d39238e, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=20.0}]6. 正则查询牛逼高大上的功能/* * 正则查询 /public void regexQuery() { Query query = new Query(Criteria.where(“user”).regex("^一灰灰blog”)); List<Map> result = mongoTemplate.find(query, Map.class, COLLECTION_NAME); System.out.println(“query: " + query + " | regexQuery: " + result);}输出query: Query: { “user” : { “$regex” : “^一灰灰blog”, “$options” : "” } }, Fields: { }, Sort: { } | regexQuery: [{_id=5c2368b258f984a4fda63cee, user=一灰灰blog, desc=帅气逼人的码农界老秀}, {_id=5c3afacde3ac8e8d2d392389, user=一灰灰blog2, desc=帅气逼人的码农界老秀2}, {_id=5c3afaf4e3ac8e8d2d39238a, user=一灰灰blog, desc=帅气逼人的码农界老秀3}, {_id=5c3afafbe3ac8e8d2d39238b, user=一灰灰blog4, desc=帅气逼人的码农界老秀4}, {_id=5c3afb0ae3ac8e8d2d39238c, user=一灰灰blog5, desc=帅气逼人的码农界老秀5}, {_id=5c3afb1ce3ac8e8d2d39238d, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=18.0}, {_id=5c3b0031e3ac8e8d2d39238e, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=20.0}, {_id=5c3b003ee3ac8e8d2d39238f, user=一灰灰blog, desc=帅气逼人的码农界老秀6, sign=hello world}]7. 查询总数统计常用,这个主要利用的是mongoTemplate.count方法/* * 查询总数 /public void countQuery() { Query query = new Query(Criteria.where(“user”).is(“一灰灰blog”)); long cnt = mongoTemplate.count(query, COLLECTION_NAME); System.out.println(“query: " + query + " | cnt " + cnt);}输出query: Query: { “user” : “一灰灰blog” }, Fields: { }, Sort: { } | cnt 58. 分组查询这个对应的是mysql中的group查询,但是在mongodb中,更多的是通过聚合查询,可以完成很多类似的操作,下面借助聚合,来看一下分组计算总数怎么玩/ * 分组查询 /public void groupQuery() { // 根据用户名进行分组统计,每个用户名对应的数量 // aggregate([ { “$group” : { “_id” : “user” , “userCount” : { “$sum” : 1}}}] ) Aggregation aggregation = Aggregation.newAggregation(Aggregation.group(“user”).count().as(“userCount”)); AggregationResults<Map> ans = mongoTemplate.aggregate(aggregation, COLLECTION_NAME, Map.class); System.out.println(“query: " + aggregation + " | groupQuery " + ans.getMappedResults());}注意下,这里用Aggregation而不是前面的Query和Criteria,输出如下query: { “aggregate” : “collection”, “pipeline” : [{ “$group” : { “_id” : “$user”, “userCount” : { “$sum” : 1 } } }] } | groupQuery [{_id=一灰灰blog, userCount=5}, {_id=一灰灰blog2, userCount=1}, {_id=一灰灰blog4, userCount=1}, {_id=二灰灰blog, userCount=1}, {_id=一灰灰blog5, userCount=1}]9. 排序sort,比较常见的了,在mongodb中有个有意思的地方在于某个字段,document中并不一定存在,这是会怎样呢?/* * 排序查询 /public void sortQuery() { // sort查询条件,需要用with来衔接 Query query = Query.query(Criteria.where(“user”).is(“一灰灰blog”)).with(Sort.by(“age”)); List<Map> result = mongoTemplate.find(query, Map.class, COLLECTION_NAME); System.out.println(“query: " + query + " | sortQuery " + result);}输出结果如下,对于没有这个字段的document也被查出来了query: Query: { “user” : “一灰灰blog” }, Fields: { }, Sort: { “age” : 1 } | sortQuery [{_id=5c2368b258f984a4fda63cee, user=一灰灰blog, desc=帅气逼人的码农界老秀}, {_id=5c3afaf4e3ac8e8d2d39238a, user=一灰灰blog, desc=帅气逼人的码农界老秀3}, {_id=5c3b003ee3ac8e8d2d39238f, user=一灰灰blog, desc=帅气逼人的码农界老秀6, sign=hello world}, {_id=5c3afb1ce3ac8e8d2d39238d, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=18.0}, {_id=5c3b0031e3ac8e8d2d39238e, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=20.0}]10. 分页数据量多的时候,分页查询比较常见,用得多就是limit和skip了/* * 分页查询 */public void pageQuery() { // limit限定查询2条 Query query = Query.query(Criteria.where(“user”).is(“一灰灰blog”)).with(Sort.by(“age”)).limit(2); List<Map> result = mongoTemplate.find(query, Map.class, COLLECTION_NAME); System.out.println(“query: " + query + " | limitPageQuery " + result); // skip()方法来跳过指定数量的数据 query = Query.query(Criteria.where(“user”).is(“一灰灰blog”)).with(Sort.by(“age”)).skip(2); result = mongoTemplate.find(query, Map.class, COLLECTION_NAME); System.out.println(“query: " + query + " | skipPageQuery " + result);}输出结果表明,limit用来限制查询多少条数据,skip则表示跳过前面多少条数据query: Query: { “user” : “一灰灰blog” }, Fields: { }, Sort: { “age” : 1 } | limitPageQuery [{_id=5c2368b258f984a4fda63cee, user=一灰灰blog, desc=帅气逼人的码农界老秀}, {_id=5c3afaf4e3ac8e8d2d39238a, user=一灰灰blog, desc=帅气逼人的码农界老秀3}]query: Query: { “user” : “一灰灰blog” }, Fields: { }, Sort: { “age” : 1 } | skipPageQuery [{_id=5c3b003ee3ac8e8d2d39238f, user=一灰灰blog, desc=帅气逼人的码农界老秀6, sign=hello world}, {_id=5c3afb1ce3ac8e8d2d39238d, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=18.0}, {_id=5c3b0031e3ac8e8d2d39238e, user=一灰灰blog, desc=帅气逼人的码农界老秀6, age=20.0}]11. 小结上面给出的一些常见的查询姿势,当然并不全面,比如我们如果需要查询document中的部分字段怎么办?比如document内部结果比较复杂,有内嵌的对象或者数组时,嵌套查询可以怎么玩?索引什么的又可以怎么利用起来,从而优化查询效率?如何通过传说中自动生成的_id来获取文档创建的时间戳?先留着这些疑问,后面再补上II. 其他0. 项目工程:spring-boot-demomodule: mongo-template相关博文: 181213-SpringBoot高级篇MongoDB之基本环境搭建与使用1. 一灰灰Blog一灰灰Blog个人博客 https://blog.hhui.top一灰灰Blog-Spring专题博客 http://spring.hhui.top一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛2. 声明尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激微博地址: 小灰灰BlogQQ: 一灰灰/33027978403. 扫描关注一灰灰blog知识星球 ...

January 13, 2019 · 5 min · jiezi

Python猫荐书系列之五:Python高性能编程

稍微关心编程语言的使用趋势的人都知道,最近几年,国内最火的两种语言非 Python 与 Go 莫属,于是,隔三差五就会有人问:这两种语言谁更厉害/好找工作/高工资……对于编程语言的争论,就是猿界的生理周期,每个月都要闹上一回。到了年末,各类榜单也是特别抓人眼球,闹得更凶。其实,它们各有对方所无法比拟的优势以及用武之地,很多争论都是没有必要的。身为一个正在努力学习 Python 的(准)中年程序员,我觉得吧,先把一门语言精进了再说。没有差劲的语言,只有差劲的程序员,等真的把语言学好了,必定是“山重水复疑无路,柳暗花明又一村”。铺垫已了,进入今天的正题,Python 猫荐书系列之五——<center>Python高性能编程</center> 本书适合已入门 Python、还想要进阶和提高的读者阅读。所有计算机语言说到底都是在硬件层面的数据操作,所以高性能编程的一个终极目标可以说是“高性能硬件编程”。然而,Python 是一门高度抽象的计算机语言,它的一大优势是开发团队的高效,不可否认地存在这样或那样的设计缺陷,以及由于开发者的水平而造成的人为的性能缺陷。本书的一大目的就是通过介绍各种模块和原理,来促成在快速开发 Python 的同时避免很多性能局限,既减低开发及维护成本,又收获系统的高效。1、性能分析是基础首先的一个关键就是性能分析,借此可以找到性能的瓶颈,使得性能调优做到事半功倍。性能调优能够让你的代码能够跑得“足够快”以及“足够瘦”。性能分析能够让你用最小的代价做出最实用的决定。书中介绍了几种性能分析的工具:(1)基本技术如 IPython 的 %timeit 魔法函数、time.time()、以及一个计时修饰器,使用这些技术来了解语句和函数的行为。(2)内置工具如 cProfile,了解代码中哪些函数耗时最长,并用 runsnake 进行可视化。(3)line_profiler 工具,对选定的函数进行逐行分析,其结果包含每行被调用的次数以及每行花费的时间百分比。(4)memory_profiler 工具,以图的形式展示RAM的使用情况随时间的变化,解释为什么某个函数占用了比预期更多的 RAM。(5)Guppy 项目的 heapy 工具,查看 Python 堆中对象的数量以及每个对象的大小,这对于消灭奇怪的内存泄漏特别有用。(6)dowser 工具,通过Web浏览器界面审查一个持续运行的进程中的实时对象。(7)dis 模块,查看 CPython 的字节码,了解基于栈的 Python 虚拟机如何运行。(8)单元测试,在性能分析时要避免由优化手段带来的破坏性后果。作者强调了性能分析的重要性,同时也对如何确保性能分析的成功提了醒,例如,将测试代码与主体代码分离、避免硬件条件的干扰(如在BIOS上禁用了TurboBoost、禁用了操作系统改写SpeedStep、只使用主电源等)、运行实验时禁用后台工具如备份和Dropbox、多次实验、重启并重跑实验来二次验证结果,等等。性能分析对于高性能编程的作用,就好比复杂度分析对于算法的作用,它本身不是高性能编程的一部分,但却是最终有效的一种评判标准。2、数据结构的影响高性能编程最重要的事情是了解数据结构所能提供的性能保证。高性能编程的很大一部分是了解你查询数据的方式,并选择一个能够迅速响应这个查询的数据结构。书中主要分析了 4 种数据结构:列表和元组就类似于其它编程语言的数组,主要用于存储具有内在次序的数据;而字典和集合就类似其它编程语言的哈希表/散列集,主要用于存储无序的数据。本书在介绍相关内容的时候很克制,所介绍的都是些影响“速度更快、开销更低”的内容,例如:内置的 Tim 排序算法、列表的 resize 操作带来的超额分配的开销、元组的内存滞留(intern机制)带来的资源优化、散列函数与嗅探函数的工作原理、散列碰撞带来的麻烦与应对、Python 命名空间的管理,等等。理解了这些内容,就能更加了解在什么情况下使用什么数据结构,以及如何优化这些数据结构的性能。另外,关于这 4 种数据结构,书中还得出了一些有趣的结论:对于一个拥有100 000 000个元素的大列表,实际分配的可能是112 500 007个元素;初始化一个列表比初始化一个元组慢5.1 倍;字典或集合默认的最小长度是8(也就是说,即使你只保存3个值,Python仍然会分配 8 个元素)、对于有限大小的字典不存在一个最佳的散列函数。3、矩阵和矢量计算矢量计算是计算机工作原理不可或缺的部分,也是在芯片层次上对程序进行加速所必须了解的部分。然而,原生 Python 并不支持矢量操作,因为 Python 列表存储的不是实际的数据,而是对实际数据的引用。在矢量和矩阵操作时,这种存储结构会造成极大的性能下降。比如,grid[5][2] 中的两个数字其实是索引值,程序需要根据索引值进行两次查找,才能获得实际的数据。同时,因为数据被分片存储,我们只能分别对每一片进行传输,而不是一次性传输整个块,因此,内存传输的开销也很大。减少瓶颈最好的方法是让代码知道如何分配我们的内存以及如何使用我们的数据进行计算。Numpy 能够将数据连续存储在内存中并支持数据的矢量操作,在数据处理方面,它是高性能编程的最佳解决方案之一。Numpy 带来性能提升的关键在于,它使用了高度优化且特殊构建的对象,取代了通用的列表结构来处理数组,由此减少了内存碎片;此外,自动矢量化的数学操作使得矩阵计算非常高效。Numpy 在矢量操作上的缺陷是一次只能处理一个操作。例如,当我们做 A B + C 这样的矢量操作时,先要等待 A B 操作完成,并保存数据在一个临时矢量中,然后再将这个新的矢量和 C 相加。Numexpr 模块可以将矢量表达式编译成非常高效的代码,可以将缓存失效以及临时变量的数量最小化。另外,它还能利用多核 CPU 以及 Intel 芯片专用的指令集来将速度最大化。书中尝试了多种优化方法的组合,通过详细的分析,展示了高性能编程所能带来的性能提升效果。4、编译器书中提出一个观点:让你的代码运行更快的最简单的办法就是让它做更少的工作。 编译器把代码编译成机器码,是提高性能的关键组成部分。不同的编译器有什么优势呢,它们对于性能提升会带来多少好处呢?书中主要介绍了如下编译工具:Cython ——这是编译成C最通用的工具,覆盖了Numpy和普通的Python代码(需要一些C语言的知识)。Shed Skin —— 一个用于非Numpy代码的,自动把Python转换成C的转换器。Numba —— 一个专用于Numpy代码的新编译器。Pythran —— 一个用于Numpy和非numpy代码的新编译器。PyPy —— 一个用于非Numpy代码的,取代常规Python可执行程序的稳定的即时编译器。书中分析了这几种编译器的工作原理、优化范围、以及适用场景等,是不错的入门介绍。此外,作者还提到了其它的编译工具,如Theano、Parakeet、PyViennaCL、ViennaCL、Nuitka 与 Pyston 等,它们各有取舍,在不同领域提供了支撑之力。5、密集型任务高性能编程的一个改进方向是提高密集型任务的处理效率,而这样的任务无非两大类:I/O 密集型与 CPU 密集型。I/O 密集型任务主要是磁盘读写与网络通信任务,占用较多 I/O 时间,而对 CPU 要求较少;CPU 密集型任务恰恰相反,它们要消耗较多的 CPU 时间,进行大量的复杂的计算,例如计算圆周率与解析视频等。改善 I/O 密集型任务的技术是异步编程 ,它使得程序在 I/O 阻塞时,并发执行其它任务,并通过“事件循环”机制来管理各项任务的运行时机,从而提升程序的执行效率。书中介绍了三种异步编程的库:Gevent、Tornado 和 Asyncio,对三种模块的区别做了较多分析。改善 CPU 密集型任务的主要方法是利用多核 CPU 进行多进程的运算。Multiprocessing 模块使用基于进程和基于线程的并行处理,在队列上共享任务,以及在进程间共享数据,是处理 CPU 密集型任务的重要技术。书中没有隐瞒它的局限性:Amdahl 定律揭示的优化限度、适应于单机多核而多机则有其它选择、全局解释锁 GIL 的束缚、以及进程间通信(同步数据和检查共享数据)的开销。针对进程间通信问题,书中还分析了多种解决方案,例如 Less Naïve Pool、Manager、Redis、RawValue、MMap 等。6、集群与现场教训集群是一种多服务器运行相同任务的结构,也就是说,集群中的各节点提供相同的服务,其优点是系统扩展容易、具备容灾恢复能力。集群需要克服的挑战有:机器间信息同步的延迟、机器间配置与性能的差异、机器的损耗与维护、其它难以预料的问题。书中列举了两个惨痛的教训:华尔街公司骑士资本由于软件升级引入的错误,损失4.62亿美元;Skype 公司 24 小时全球中断的严重事故。书中给我们重点介绍了三个集群化解决方案:Parallel Python、IPython Parallel 和 NSQ。引申也介绍了一些普遍使用的方案,如 Celery、Gearman、PyRes、SQS。关于现场教训,它们不仅仅是一些事故或者故事而已,由成功的公司所总结出来的经验更是来之不易的智慧。书中单独用一章内容分享了六篇文章,这些文章出自几个使用 Python 的公司/大型组织,像是Adaptive Lab、RadimRehurek、Smesh、PyPy 与 Lanyrd ,这些国外组织的一线实践经验,应该也能给国内的 Python 社区带来一些启示。7、写在最后众所周知,Python 应用前景大、简单易学、方便开发与部署,然而与其它编程语言相比,它的性能几乎总是落于下风。如何解决这个难题呢?本期荐书的书目就是一种回应。《Python高性能编程》全书从微观到宏观对高性能编程的方方面面做了讲解,主要包含以下主题:计算机内部结构的背景知识、列表和元组、字典和集合、迭代器和生成器、矩阵和矢量计算、编译器、并发、集群和工作队列等。这些内容为编写更快的 Python 指明了答案。本篇文章主要以梳理书中的内容要点为主,平均而兼顾地理清了全书脉络(PS:介绍得太面面俱到了,但愿不被指责为一篇流水账的读书笔记才好……)。我认为,鉴于书中谈及的这些话题,它就足以成为我们荐书栏目的一员了。除去某些句段的糟糕翻译、成书时间比较早(2014年)而造成的过时外,这本书总体质量不错,可称为是一份优秀的高性能编程的指引手册。关于荐书栏目,我最后多说几句。本栏目原计划两周左右出一篇,但由于其它系列文章花费了我不少时间,而要写好一篇荐书/书评也特别费劲,最后生生造成了现在两月一更的尴尬局面……这篇文章是个错误的示范,我不该试图全面通读与概括其内容的。因此,我决定今后选一些易读的书目,在写作上也尽量走短小精悍风,希望能持续地将本栏目运作下去。若你有什么建议(如书目推荐、书评推荐、写作建议、甚至是投稿),我随时欢迎,先行致谢啦。往期荐书回顾: 第一期:《编写高质量代码改善 Python 程序的 91 个建议》第二期:《Python最佳实践指南》第三期:《黑客与画家》第四期:《Python源码剖析》—————–本文原创并首发于微信公众号【Python猫】,后台回复“爱学习”,免费获得20+本精选电子书。 ...

January 13, 2019 · 1 min · jiezi

使用scrapy抓取Youtube播放页数据

可参看Knowsmore抓取Youtube播放页数据的前提是scrapy部署的机器可以正常访问Youtube网站示例网址抓取的原理是读取Youtube播放页桌面版网页源代码中的全局变量: ytInitialData存取到Mongo中的数据如下:{ “title” : “20130410 锵锵三人行 陈平原谈中国教育问题”, “view_count” : “12,407 views”}代码如下:# -- coding: utf-8 --import scrapyimport reimport jsonfrom scrapy import Selectorfrom knowsmore.items import YoutubeItemfrom ..common import class YoutubeSpider(scrapy.Spider): custom_settings = { ‘DOWNLOADER_MIDDLEWARES’ : { } } name = ‘youtube’ allowed_domains = [‘www.youtube.com’] start_urls = [‘https://www.youtube.com/watch?v=3vkqOdMBP48’] def parse(self, response): ytInitialData = r1(r’ytInitialData"] = (.?)}};’, response.body) if ytInitialData: ytInitialData = ‘%s}}’ % ytInitialData ytInitialDataObj = json.loads(ytInitialData) videoInfo = ytInitialDataObj[‘contents’][’twoColumnWatchNextResults’][‘results’][‘results’][‘contents’][0][‘videoPrimaryInfoRenderer’] Item = YoutubeItem( title = videoInfo[’title’][‘simpleText’].encode(‘utf-8’), view_count = videoInfo[‘viewCount’][‘videoViewCountRenderer’][‘viewCount’][‘simpleText’] ) yield Item ...

January 9, 2019 · 1 min · jiezi

Mac安装MongoDB并启动

Mac上装MongoDB的官网教程:https://docs.mongodb.com/manu…,英文不错的可以直接看,教程也很清晰。两种版本MongoDB有两种版本: 1.(Enterprise Edition)企业版,更高级的功能和技术服务(应该要钱) 2.(Community Edition)社区版,免费,一般来说足够用的了两者差异主要体现在安全认证、系统认证等方面。本次讲述安装的当然是免费的社区版。两种安装方法:手动安装(manually),需要下载安装包、解压、编译…比较繁琐使用mac系统的Homebrew安装(Homebrew),Homebrew是mac系统上的包管理工具,类似node的npm。此方法比较方便快捷。用Homebrew安装MongoDB一、更新Homebrew,打开命令行工具执行以下命令,为什么更新Homebrew?brew update二、 安装mongonDB,安装前可以执行 mongo -version 检查是否已安装,若已安装继续执行以下命令会提示你是否替代当前版本. 如果想安装最新开发版则加上–devel:brew install mongodb –devel brew install mongodb 三、 创建MongoDB数据存放的默认文件夹,MongoDB数据存放的默认路径为/data/db(即根目录下/data/db),但该目录的文件夹在你电脑不一定存在,执行以下命令创建(你未必有权限创建,所以加上sudo)sudo mkdir -p /data/db 四、设置db文件夹权限,数据库需要读写数据,如果你没root权限,在根目录下创建的文件夹默认是没有写入权限的,直接启动MongoDB会报错。所以要设置权限。权限设置有两种方法,1、通过界面操作文件夹右击鼠标,进入"显示简介"->共享与权限->打开右下角操作锁->进行权限修改2、通过命令行操作,以下是其中一种设置读写权限命令(xxx为你的用户名)sudo chown -R xxx /data/db五、启动MongoDB服务,执行完以上操作,我们来开启MongoDB,有两种开启方式1、通过执行配置文件启动(自行百度);2、直接通过一下命令启动:mongod看到这个证明我们已经开启了MongoDB服务。六、进入mongoDB操作模式,我们已经开启了mongodb服务,但在当前命令面板下是无法进行其他操作的,所以再新打开一个命令面板,然后输入下面的命令进入mongoDB的操作模式(前提是MongoDB服务已打开,若关闭则直接报错)mongo现在我们可以进行数据库的相关操作了,比如执行show dbs到这里我们已经完成Mongo的安装、启动、操作了最后学习过程中会遇到很多问题,希望记录下来方便日后回头看看。如文章出现有错误,麻烦指出;或有相关知识点讨论也十分欢迎 ^_^!

January 9, 2019 · 1 min · jiezi

使用Scrapy抓取新浪微博用户信息

详细代码可查看Knowsmore数据的来源是新浪微博的手机端H5页面个人资料API:https://m.weibo.cn/profile/in…【用户ID】发出的微博API:https://m.weibo.cn/api/contai…【用户ID】_-WEIBO_SECOND_PROFILE_WEIBO&page_type=03&page=【页数从1开始】# -- coding: utf-8 --import scrapyimport reimport jsonimport os,sysfrom scrapy import Selector, Requestfrom knowsmore.items import WeiboUserItem, WeiboStatusItemfrom ..common import *from ..model.mongodb import *WEIBO_USER_CONFIG = { ‘BASE_URL’ : ‘https://m.weibo.cn’, ‘USER_IDS’ : [‘6883966016’]}class WeiboUserSpider(scrapy.Spider): name = “weibo_user” def start_requests(self): for uid in WEIBO_USER_CONFIG[‘USER_IDS’]: url = ‘%s/profile/info?uid=%s’ % (WEIBO_USER_CONFIG[‘BASE_URL’], uid) yield Request(url) # Define your statuses implementation here, just a demo below for i in range(1, 2): status_url = ‘%s/api/container/getIndex?containerid=230413%s-_WEIBO_SECOND_PROFILE_WEIBO&page_type=03&page=%d’ % (WEIBO_USER_CONFIG[‘BASE_URL’], uid, i) yield Request(status_url, callback=self.parse_status) # https://m.weibo.cn/profile/1784537661 def parse(self, response): user_data = json.loads(response.text) yield WeiboUserItem( fans_url = user_data[‘data’][‘fans’], follow_url = user_data[‘data’][‘follow’], more_url = user_data[‘data’][‘more’], user = user_data[‘data’][‘user’] ) # https://m.weibo.cn/api/container/getIndex?containerid=2304131784537661_-_WEIBO_SECOND_PROFILE_WEIBO&page_type=03&page=2 def parse_status(self, response): status_data = json.loads(response.text) yield WeiboStatusItem( cards = status_data[‘data’][‘cards’] ) ...

January 7, 2019 · 1 min · jiezi

从MongoDB漫谈数据库

今天的主题是从MongoDB漫谈数据库,在日常的项目中,我们一般都是使用的mysql作为数据库,但是一旦有问题,又常常会听到类似“要不换成MongoDB试试”的声音,因此就让我们这些小白来随便聊聊数据库什么是数据库我们就用最简单的话来说,数据库,就是保存数据的一个仓库数据库(Database)的基本概念数据库就是按照一定的数据结构来组织,储存和管理数据的仓库我们写的程序都是在内存中运行的,一旦程序运行结束或者计算机断点,程序运行中的数据就会全部丢失;所以我们就需要将一些程序的数据持久化到键盘之中,以确保数据的安全性。数据库则是大批量数据持久化的普遍选择,1.文件 2. 数据库为什么用数据库存储数据数据库是有结构的数据库可以提供各种接口,让数据处理(增删改查)快捷方便各种语言(PHP、jsp、.net..)提供了完善的接口数据库流行度(来源:https://db-engines.com/en/ranking)从统计的数据中可以看出,最受欢迎的DBMS是“关系型”,前五名中占据了四位,把数据扩大到前十,关系型数据库也占到了七位之多。为什么大多数程序员更喜欢使用MySQL?开源,只有企业才需要购买许可证具有广泛的用途:可广泛用于大多数平台,如Linux,Windows,Ubuntu,Mac OS X等易于使用可靠的:多年来一直经过试验和测试适用于PHP(PHP天下第一),也可以与其他编程语言一起使用,如JAVA,PERL,C,C ++等适用于小型和大型应用那么关系型数据库这么好为什么前五名中还会出现MongoDB的身影呢,我们来看一下关系数据库和MySQL的缺点是什么?可伸缩性:向特定记录添加更多数据可能涉及扩展到多个表,列和行,而因为数据是按行存储,即使只针对其中某一列进行运算,关系型数据库也会将整行数据从存储设备中读入内存,导致I/O较高由第一条产生而来的一个缺陷就是无法存储数据结构速度:由于数据结构的问题倒是分析数据时需要一定的时间,另外只能够进行子字符串的匹配查询,当表的数据逐渐变大的时候,like查询的匹配会非常慢,即使在有索引的情况下在使用之前,需要先编写架构以定义表,同时表结构schema扩展不方便 如要需要修改表结构,需要执行执行DDL(data definition language),语句修改,修改期间会导致锁表是什么让MongoDB如此吸引人?灵活性:文档结构更符合开发人员在各自编程语言中的编码方式,这些编程语言在键值对中是清晰且结构化的,这样可以随时轻松添加和编辑数据/文档支持各种查询:字段,表达式,范围查询,JavaScript函数等更快的周转时间:因为存储在MySQL数据库中的多个表中的相关数据存储在MongoDB中的同一文档中没有严格的模式:可以在定义文档结构之前先创建文档MongoDB的功能使它更适合处理大量数据先让我们来比较一下两者再来详细说明 MySQLMongoDB版本1995- 2018(mysql 8.0)2009结构关系型非关系型灵活性弱,需要在使用之前先定义数据库模式与MySQL相比具有相当大的灵活性 - 定义不需要的模式可扩展性可以,但是比较困难,MySQL数据库可以垂直扩展,可以向单个服务器添加更多资源比MySQL更具可扩展性。MongoDB可水平扩展,可以添加更多服务器来扩展您的数据库需要DB管理员是否 - 开发人员和管理员都可以使用适用场景会计师事务所和银行,以及需要具有清晰架构的结构化数据的其他公司。非常适合具有或多或少固定要求的企业(twitter例外)具有实时数据,物联网,内容管理,移动应用,社交网络,面向大数据/网络分析的系统以及不需要具有清晰架构或其架构的结构化数据的业务的理想选择不断变化灵活性首先说说灵活性,举个列子,一个商城里面会有许多的商品,而这些商品都是有自己独特的属性的,比如电视有屏幕尺寸、屏幕分辨率,而空调有制冷类型、外机噪音等属性,要把它们放到产品表里是非常困难的,额外增加了程序员对于数据表设计的工作,而MongoDB没有Schema(模式、数据模型)就会显得很简单。MongoDB 的灵活还体现在非结构化和半结构化的数据上。MongoDB 提供全文索引,也支持地理位置查询和索引。例如一位用户想知道方圆五公里哪里有公共卫生间,这是「地理范围查询」。然后他搜索最近的单车。摩拜单车正是使用 MongoDB 完成这样的「距离排序查询」。可扩展性数据一台机器放不下了,就需要sharding(分片)把它放到好几台机器上去。分片是MongoDB多年以来的原生功能,与MongoDB其他功能高效整合。例如,分片集群中一个复杂的聚合查询会自动地根据 Shard Key(片键)分配到多个结点上运行,尽可能将计算任务下推到数据结点上,最后在一个结点上聚合所有结点的结果。分片还可以在各个结点间自动迁移数据,均衡其数据量。同时配合着MongoDB的复制(副本集)技术,可以有效的避免数据的丢失(在测试的时候发现mongo会自动发现副本集的所有机器地址,当一台Mongo被停掉时,连接的Server不会报错)MongoDB中使用分片集群结构分布:MongoDB的缺点众所周知,MongoDB占用了大量的服务器内存MongoDB在安全性上略微会差一点过于自由灵活的文件存储格式带来的数据错误(。。。。。。)单个文档大小限制为16 M对于数组型的数据操作不够丰富什么时候选择MongoDB说了这么多狗屎一样总结的话,最重要的就是我们在什么时候选择使用MongoDB日志系统,系统运行过程中产生的日志信息,一般种类较多、范围较大、内容也比较杂乱。通过MongoDB可以将这些杂乱的日志进行收集管理地理位置存储,MongoDB支持地理位置、二维空间索引,可以存储经纬度,因此可以很快的计算出两点之间的距离,等位置信息数据规模增长很快(比如供给的关注信息)需要保证高可用的环境文件存储需求其他场景,如游戏开发中可以通过MongoDB存储用户信息、装备、积分等,除此之外物流系统、社交系统、甚至物联网系统数据库的类型说了这么多,为什么我们会把mysql和MongoDB放在一起比较进行选择,就是因为它们是不同类型的数据库,从数据库发展至今,大致上分为三种类型RDBMS(关系型数据库)首先要提到的一定是我们最熟悉的mysql数据库所属于的关系型数据库。关系型数据库的特点:比如MySql 、sql server Oracle 等特点 通过一张张表来建立关联基本都使用SQL语言来管理数据库Nosql (非关系型数据库)NoSql,也就是MongoDB的数据库类型,源自2009年在San Francisco举办的一次Meetup,在该Meetup上出现了NoSql技术的描述:open source, distributed, non relational databases非关系型数据库的特点:没有行 、列的概念 用json类储存数据集合相当于“表”,文档相当于“行”标准化和非标准化的摩擦。标准化限制创新,非标准话不能统一NoSql在刚提出的时候被解释成Non-Relational,也有No-sql的意思,但是随着近些年的快速发展,SQL已经逐步被应用在了更广泛的领域,因此,SQL已不再是RDBMS的专属特征,NoSql技术体系中也引入了SQL能力,因此而演变出来了Not-Only-SQL的概念大多数NoSql技术,弱化了对ACID语义以及复杂关联查询的支持,采用了更加简洁或更加专业的数据模型,优化了读写路径,从而能够换取更高的读写性能NewSql根据wiki中的定义<font color=#FF7F50 size=5 face=“黑体”>NewSQL</font> is a class of modern relational database management systems that seek to provide the same scalable performance of NoSQL systems for online transaction processing (OLTP) read-write workloads while still maintaining the ACID guarantees of a traditional database system.NewSql可以说是传统的RDBMS与NoSql技术结合之下的产物,因此,可以将典型NewSql技术理解成分布式关系型数据库,能够支持分布式事务是一个基本前提。NoSQL与NewSQL在技术栈上有很多重叠,但在是否支持关系型模型及对复杂事务的支持力度上是存在明显区别的。因为本人不了解,所以这里不做多说。这里我只是简单的介绍一下数据库的类型,对于一种存储技术属于NoSql或者NewSql,亦或是RDBMS是不能简单归类的,毕竟技术是在不断进步的,比如MySQL现在也兼容了nosql的特性:或者有的人会奇怪为什么在介绍MongoDB的缺点时没有提事务的事,这是因为在2018年夏季的MongoDB4.0版本中,MongoDB引入了事务功能,支持多文档ACID特性,例如使用mongo shell进行事务操作具体压测数据后期补上参考文章:《MongoDB vs MySQL : Understanding the difference》——Tanya Noronha《NewSQL是否是NoSQL的取代者?》——Jaison《MongoDB 4.0 事务实现解析》——张友东 ...

January 7, 2019 · 1 min · jiezi

小程序云开发实战系列02--云数据库

以前一直是使用关系型数据库,第一次使用NoSQL,跟大家分享一下我有限的使用心得,希望对像我一样初使用NoSQL的开发者有所帮助。首先说说微信小程序云开发里集成的这个NoSQL,官方并没有说明是哪种NoSQL数据库,但从开发文档和暴露的API,还有官方论坛里的讨论来看应该是一个简化版的MongoDB。需要指出的是微信小程序关于云数据库的开发文档非常的简略,对于像我这样没有太多NoSQL经验的用户,很多时候需要参考MongoDB的相关文档。接下来重点谈谈我在使用这个NoSQL云数据库时最不适应的一个痛点—-文档级别的原子操作。我们经常要使用到原子操作,来避免当多个用户同时对同一个field(字段)编辑时发生冲突。我在使用前其实最担心的痛点是有无schema的区别,但是使用下来发现我挺习惯,也挺喜欢无schema的,后文再详说。现在具体来看看MongoDB只支持document(文档)级别的原子操作。对于我来说,这个限制鼓励我尽量把所有关系都放在一个document里。对此我一开始是有点抗拒的,对于从关系型数据库过来的人特别不习惯。而更让我苦恼的是微信小程序云开发集成的这个云数据库是一个简化版MongoDB,只提供了非常有限的原子操作指令(command)。对于一些常用的document级别原子操作,我必须构想自己的解决办法,而没有提供直接对应的command。以下是两个我在实际开发中遇到的这类问题及我的解决办法:1.应用场景:对于一个视频,我需要一个叫total_likes的field(字段),当有用户点击“喜欢”时该field递增1,当有用户取消“喜欢”时该field递减1。痛点:小程序云数据库只提供了递增指令的原子操作,没有提供递减指令。const _ = db.commanddb.collection(‘video’).doc(‘video-id’).update({ data: { total_likes: _.inc(1) }})解决办法:要实现递减的原子操作,只需在递增指令里传入负数,如data: { total_likes: _.inc(-1)}2.应用场景:对于一个线上课程,我需要一个叫subscribers的field(字段)来记录有多少人订阅了该课程。当有用户点击“订阅”时该字段需记录该用户的id,名字及头像;当有用户取消“订阅”时需把该用户从subscribers字段里删除。痛点:我们很自然的会想到用数组(Array)数据类型来维护subscribers这个字段,虽然小程序云数据库提供了一些针对数组的原子操作,如push,pop,shift和unshfit,可是无法实现取消订阅这个场景的原子操作。解决办法:弃用Array转而使用对象(object)数据类型来维护subscribers这个字段。最终的数据看起来会是这样的:{ “subscribers”: { “userID-1”: { “name”: “小明”, “avatar”: “https://avatar-1.com” }, “userID-2”: { “name”: “小红”, “avatar”: “https://avatar-2.com” }, “userID-3”: { “name”: “小李”, “avatar”: “https://avatar-3.com” }, … }}当有用户订阅时的原子操作:const subscriber = “subscribers.” + user.id;db.collection(‘class’).where({ _id: ‘classID’,}).limit(1).update({ data: { [subscriber]: { avatar: user.avatar, name: user.name, } }})当有用户取消订阅时的原子操作:const subscriber = “subscribers.” + user.id;db.collection(‘class’).doc(‘classID’).update({ data: { [subscriber]: _.remove() }})前文说到我很喜欢无schema,因为它非常适合快速迭代开发。而且由于云数据库使用的是类似JSON的数据结构,对于全栈开发者,基本上可以实现由前端来定义数据结构。这样的开发流程非常适合小团队,不需要庞大的并行开发,突出沟通效率和对产品需求的随机应变。顺带一提的是微信小程序云开发能力是从基础库2.2.3开始支持的,但如果要支持所有版本的基础库,可以在 app.json / game.json 中增加字段 “cloud”: true本系列第一章:小程序云开发实战系列01–云环境设置《Meetup丨活动报名组局》是我最近开发的一个活动报名预约工具小程序,这个系列文章主要来自我在开发这款小程序时的一些体会心得。感兴趣的小伙伴可以扫下面的二维码进入我的小程序。 ...

January 6, 2019 · 1 min · jiezi

Centos7安装mongodb

初始安装初始安装很简单,具体过程参考了:https://blog.csdn.net/junshan…,其中最重要的就是mongodb.conf这个文件的配置,具体如下:port=27017#端口 dbpath=/u03/mongodb/db#数据库存文件存放目录 logpath=/u03/mongodb/mongodb.log#日志文件存放路径 logappend=true#使用追加的方式写日志 fork=true#不以守护程序的方式启用,即不在后台运行 maxConns=100#最大同时连接数 auth=true#不启用验证 journal=true#每次写入会记录一条操作日志(通过journal可以重新构造出写入的数据)。#即使宕机,启动时wiredtiger会先将数据恢复到最近一次的checkpoint点,然后重放后续的journal日志来恢复。storageEngine=mmapv1 #存储引擎有mmapv1、wiretiger、mongorocksbind_ip = 0.0.0.0#这样就可外部访问了,例如从win10中去连虚拟机中的MongoDB启动安装完成后,启动mongodbmongod –config /u03/mongodb/mongodb.conf启动报错exception in initAndListen: NonExistentPath: Data directory /data/db not found结果启动报错,找不到/data/db这个路径,/data/db这个路径用于存储数据库相关数据,在安装mongodb时,mongodb.conf内配置的dbpath=/u03/mongodb/db。因此通过mongod –dbpath /u03/mongodb/db来启动。查看mongodb端口状态启动成功后,查看mongodb是否处于listening的状态。netstat -lanp | grep “27017"创建数据库接下来开始创建 数据库(在服务器上操作)进入命令行mongo创建数据库use test创建用户并授权db.createUser({ user:“test”, pwd:“test”, roles:[{role:“userAdmin”,db:“test”}]})windows连接报错数据库创建成功后,开始在本地环境远程连接mongodb。结果,又报错了!Cannot connect to the MongoDB at 192.168.58.131:27017. Error: Network is unreachable.首先排查mongodb.conf,bind_ip是否为0.0.0.0,端口号是否为27017;其次查看服务器防火墙是否关闭, systemctl status firewalld ,如果防火墙开启,则关闭防火墙systemctl stop firewalld(关于防火墙的学习来自于 https://www.cnblogs.com/moxia…);然后又在本地检查27017端口是否开启,telnet 192.168.58.131 27017,第一次提示“telnet不是内部或外部命令”,解决办法: https://blog.csdn.net/haijing…,再次运行,报“无法打开到主机的连接。 在端口 27017: 连接失败”,由此可知,是27017端口的问题。查遍了所有资源都没有解决,最终在https://blog.csdn.net/hongwei…,在启动命令最后加上–bind_ip_all,`mongod –dbpath /u03/mongodb/db –auth –bind_ip_all## 调试过程中其他错误在调整无法连接这个问题时,尝试了各种方法,数据库来回启动了无数次,其中也遇到了一些其他报错,在这里列出:1. exception in initAndListen: DBPathInUse: Unable to lock the lock file: /u03/mongodb/db/mongod.lock (Unknown error). Another mongod instance is already running on the /u03/mongodb/db directory这个错误的原因是mongodb上次关闭异常,先通过mongo --repair进行修复,修复后若还有问题,通过rm mongod.lock -rf删除lock文件,rm diagnostic.data/* -rf删除数据文件。2.```Failed to set up listener: SocketException: Address already in use上个问题解决后又报地址已被使用,ps aux | grep mongod查看进程,kill -9 进程号杀死进程。附:正确关闭mongodb数据库的方法:正确的关闭方法:停止Mongodb查看进程,使用kill命令;不能使用kill -9。在命令行使用shutdown命令。robo3 远程连接最后使用robo3连接mongodb,终于成功了! ...

January 2, 2019 · 1 min · jiezi

有坑勿踩(二)——关于游标

前言聊一聊一个最基本的问题,游标的使用。可能你从来没有注意过它,但其实它在MongoDB的使用中是普遍存在的,也存在一些常见的坑需要引起我们的注意。在写这个系列文章时,我会假设读者已经对MongoDB有了最基础的了解,因此一些基本名词和概念就不做过多的解释,请自己查阅相关资料。使用场景可能你以为你并没有经常在使用游标,但是其实只要在做查询,几乎时时刻刻都在用它。本质上所有查询的数据都是从游标来的。你说你用toArray()?不存在的,它也是在遍历游标然后返回给你一个数组而已。正是因为这样,就出现了第一个问题:除非你确定返回数据量有限,否则不要随便toArray()。这里说的toArray()包括:shell中的toArray()。例如: var result = db.coll.find().toArray();node中的toArray()。例如:var result = await db.collection(“coll”).find().toArray();python中的list()。例如:result = list(db.coll.find());Java中的toArray()。例如:DBCursor.toArray();因为无论游标里有多少数据,toArray()都会给你挖出来放到内存里,变成数组返回给你。慢不说,内存也占用了很多。所以在可能的情况下,还是尽可能使用hasNext()/next()来得更好。游标主要来自两个地方:findaggregation注意二者返回的虽然都是“游标”,但又是两种不同的游标,使用上API也不完全相同,使用的时候请先查阅API(特别是使用NodeJS之类的动态语言的时候不要想当然)。batchSize与getmore说完从哪里来,下面就该说说怎么用的问题。可能你已经从什么地方看到过getmore,比如mongostat的结果中。getmore的作用是从游标中提取一批数据,具体提取多少则是由batchSize决定。所以当程序进行查询的时候,实际上在后台发生的事情包括:驱动在后台获取batchSize条数据并自己缓存起来;每次程序调用游标的next()方法时,从这些缓存中提取一条并返回;当batchSize条数据都返回完之后,驱动再次通过getmore获取batchSize条数据。我们可以通过shell来观察这一过程:先插入一批数据:use foofor(var i = 0; i < 1000; i++) { db.bar.insert({i: i});}强制日志记录所有操作:db.setProfilingLevel(0, 0)跟踪日志:tail -f mongod.log现在执行一条find语句:replset:PRIMARY> db.bar.find().batchSize(50);2018-12-29T16:01:29.587+0800 I COMMAND [conn12] command test.bar appName: “MongoDB Shell” command: find { find: “bar”, filter: {}, batchSize: 50.0, $clusterTime: { clusterTime: Timestamp(1546070474, 1), signature: { hash: BinData(0, 0000000000000000000000000000000000000000), keyId: 0 } }, $db: “test” } planSummary: COLLSCAN cursorid:77199395767 keysExamined:0 docsExamined:50 numYields:0 nreturned:50 reslen:2062 locks:{ Global: { acquireCount: { r: 1 } }, Database: { acquireCount: { r: 1 } }, Collection: { acquireCount: { r: 1 } } } protocol:op_msg 0ms虽然我们在shell中只输出了20条结果,但实际上我们已经从这个游标中获取了50条数据(日志中的黑体部分)。所以当我们继续遍历这个游标时是暂时不需要再次从数据库中取数据的。同时注意我们已经有了一个游标cursor:77199395767。但当我们第三次遍历20条数据时,则会出现getmore日志:replset:PRIMARY> it2018-12-29T16:03:46.007+0800 I COMMAND [conn12] command test.bar appName: “MongoDB Shell” command: getMore { getMore: 77199395767, collection: “bar”, batchSize: 50.0, $clusterTime: { clusterTime: Timestamp(1546070594, 1), signature: { hash: BinData(0, 0000000000000000000000000000000000000000), keyId: 0 } }, $db: “test” } originatingCommand: { find: “bar”, filter: {}, batchSize: 50.0, $clusterTime: { clusterTime: Timestamp(1546070474, 1), signature: { hash: BinData(0, 0000000000000000000000000000000000000000), keyId: 0 } }, $db: “test” } planSummary: COLLSCAN cursorid:77199395767 keysExamined:0 docsExamined:50 numYields:0 nreturned:50 reslen:2061 locks:{ Global: { acquireCount: { r: 1 } }, Database: { acquireCount: { r: 1 } }, Collection: { acquireCount: { r: 1 } } } protocol:op_msg 0ms2018-12-29T16:03:46.010+0800 I COMMAND [conn12] command admin.$cmd appName: “MongoDB Shell” command: replSetGetStatus { replSetGetStatus: 1.0, forShell: 1.0, $clusterTime: { clusterTime: Timestamp(1546070624, 1), signature: { hash: BinData(0, 0000000000000000000000000000000000000000), keyId: 0 } }, $db: “admin” } numYields:0 reslen:896 locks:{} protocol:op_msg 0ms它通过同一个游标再次提取了50条数据供使用。当我们用完缓存中的数据之前都是不会再看到新的getmore指令的。游标超时上面已经了解了游标与驱动是如何配合工作的,那么游标超时是怎么发生的呢?条件很简单,2次getmore之间间隔了超过10分钟,即一个游标在服务端超过10分钟无人访问,则会被回收掉。这时候如果你再针对这个游标进行getmore,就会得到游标不存在的错误(是的,超时的游标在数据库中是不存在的,你得到的错误不会是超时,而是游标不存在。为了便于理解,我们下面还是称之为“游标超时”)。那么假设你通过游标读取数据的时候是为了进行一系列分析处理,那么下一次getmore在什么时候发生将取决于你的应用在多长时间内消耗完了当前缓存中的数据。换句话说,你的应用处理得越慢,下一次getmore发生的时间就越晚。很多驱动中batchSize的默认值是1000,这也代表着你的应用必须至少能够在10分钟内处理1000条数据,否则就会得到游标超时错误。所以诸如每一条数据需要查询其他数据库1次,需要通过RESTful API到互联网上获取相关的数据,或者需要进行一系列复杂的运算,这样的场景下,问题的关键其实不在于MongoDB怎么样,而在于你的应用到底能够处理多快。假设问题还是发生了,你的应用遇到了游标超时错误,怎么办呢?你至少可以有以下一些选择:延长游标超时时间,请参考cursorTimeoutMillis;加速应用的处理速度,处理得快了,下一次getmore自然就发生得更早;不是那么直观,但是减小batchSize也可以达到同样的目的;禁用超时时间(noCursorTimeout)——绝对不推荐使用。虽然可以达到目的,你也可以说我会在最后主动关闭游标的,但事实上总会发生这样那样的意外,导致你最终没有正确关闭游标,最后服务器上塞满了游标的情况也是很常见的。例外情况上面已经解释过,在游标超时的时候你得到的实际是“游标不存在”错误,而不是超时。那么反过来是不是也成立呢,“游标不存在”一定是超时了吗?离散数学告诉我们,一个命题的逆命题不一定成立。事实上也是如此。“游标不存在”的另一种可能性是有些用户热衷于在MongoDB前面加上负载均衡/自动故障恢复的软/硬件。我们已经知道游标是存在于一台服务器上的,如果你的负载均衡毫无原则地将请求转发到任意服务器上,getmore同时会因为找不到游标而出现“游标不存在”的错误。事实上MongoDB和其驱动本身就已经能够完成高可用和负载均衡,并不需要额外画蛇添足。 ...

December 29, 2018 · 2 min · jiezi

玩转Mongo计算

MongoDB属于 NoSql 中的基于分布式文件存储的文档型数据库,是非关系数据库当中功能最丰富,最像关系数据库的。它支持的数据结构非常松散,是类似 json 的 bson 格式,因此可以存储比较复杂的数据类型。Mongo 最大的特点是它支持的查询语言非常强大,其语法有点类似于面向对象的查询语言,几乎可以实现类似关系数据库单表查询的绝大部分功能,但是写起来并不简单。若能集算器 SPL 语言结合,处理起来就相对容易多了。 现在我们针对 MongoDB 在计算方面的问题进行讨论分析,通过集算器 SPL 语言加以改进,方便用户使用 MongoDB。现从如下情况加以说明:1. 单表内嵌数组结构的统计……………………………………….. 12. 单表内嵌文档求和………………………………………………… 33. 分段分组结构………………………………………………………. 54. 同构表合并…………………………………………………………. 65. 关联嵌套结构情况 1……………………………………………… 86. 关联嵌套结构情况 2…………………………………………….. 107. 关联嵌套结构情况 3…………………………………………….. 118. 多字段分组统计………………………………………………….. 149. 两表关联查询……………………………………………………… 1610. 多表关联查询……………………………………………………. 1711. 指定数组查找……………………………………………………. 1912. 关联表中的数组查找…………………………………………… 201. 单表内嵌数组结构的统计对嵌套数组结构中的数据统计处理。查询考试科目的平均分及每个学生的总成绩情况。测试数据:期待统计结果:脚本: db.student.aggregate( [ {$unwind : “$scroe”}, {$group: { “_id”: {“lesson”:"$scroe.lesson"} , “qty”:{"$avg": “$scroe.mark”} } } ] ) db.student.aggregate( [ {$unwind : “$scroe”}, {$group: { “_id”: {“name” :"$name"} , “qty”:{"$sum" : “$scroe.mark”} } } ] ) 由于各科分数 scroe 是按课目、成绩记录的数组结构,统计前需要将它拆解,将每科成绩与学生对应,然后再实现分组计算。这需要熟悉 unwind 与 group 组合的应用。SPL 脚本:按课目统计的总分数每个学生的总成绩脚本说明: A1:连接 mongo 数据库。 A2:获取 student 表中的数据。 A3:将 scroe 数据合并成序表,再按课程分组,计算平均分。 A4:统计每个学生的成绩后返回列名为 NAME、TOTAL 的序表。new 函数表示生成新序表。 A5:关闭数据库连接。 这个比较常用嵌套结构统计的例子许多人遭遇过、需要先拆解,主要是熟悉 mongodb 对嵌套数据结构的处理。2. 单表内嵌文档求和对内嵌文档中的数据求和处理, 下面要统计每条记录的 income,output 的数量和。测试数据:期待统计结果 Mongodb脚本:var fields = [ “income”, “output”]; db.computer.aggregate([ { $project:{ “values”:{ $filter:{ input:{ “$objectToArray”:"$$ROOT" }, cond:{ $in:[ “$$this.k”, fields ] } } } } }, { $unwind:"$values" }, { $project:{ key:"$values.k", values:{ “$sum”:{ “$let”:{ “vars”:{ “item”:{ "$objectToArray":"$values.v" } }, “in”:"$$item.v" } } } } }, {$sort: {"_id":-1}}, { “$group”: { “_id”: “$_id”, ‘income’:{"$first": “$values”}, “output”:{"$last": “$values”} }}, ]); filter将income,output 部分信息存放到数组中,用 unwind 拆解成记录,再累计各项值求和,按 _id 分组合并数据。SPL 脚本:统计结果脚本说明: A1:连接数据库 A2:获取 computer 表中的数据 A3:将 income、output 字段中的数据分别转换成序列求和,再与 ID 组合生成新序表 A4:关闭数据库连接。 获取子记录的字段值,然后求和,相对于 mongo 脚本简化了不少。这个内嵌文档与内嵌数组在组织结构上有点类似,不小心容易混淆,注意与上例中的 scroe 数组结构比较,写出的脚本有所不同。3. 分段分组结构统计各段内的记录数量。下面按销售量分段,统计各段内的数据量,数据如下:分段方法:0-3000;3000-5000;5000-7500;7500-10000;10000 以上。期望结果:Mongo 脚本 var a_count=0; var b_count=0; var c_count=0; var d_count=0; var e_count=0; db.sales.find({ }).forEach( function(myDoc) { if (myDoc.SALES <3000) { a_count += 1; } else if (myDoc.SALES <5000) { b_count += 1; } else if (myDoc.SALES <7500) { c_count += 1; } else if (myDoc.SALES <10000) { d_count += 1; } else { e_count += 1; } } ); print(“a_count="+a_count) print(“b_count="+b_count) print(“c_count="+c_count) print(“d_count="+d_count) print(“e_count="+e_count) 这个需求按条件分段分组,mongodb 没有提供对应的 api,实现起来有点繁琐,上面的程序是其中实现的一个例子参考,当然也可以写成其它实现形式。下面看看集算器脚本的实现。 SPL 脚本:脚本说明: A1:定义 SALES 分组区间。 A2:连接 mongodb 数据库。 A3:获取 sales 表中的数据。 A4:根据 SALES 区间分组统计员工数。其中函数 pseg()表示返回成员在序列中的区段序号,int() 表示转换成整数。 A5:关闭数据库连接。pseg 的使用让 SPL 脚本精简了不少。4. 同构表合并具有相同结构的多表数据合并。下面将两个员工表数据合并。Emp1:Emp2:合并数据结果:Mongo 脚本: db.emp1.aggregate([ { “$limit”: 1}, { “$facet”: { “collection1”: [ {”$limit”: 1}, { “$lookup”: { “from”: “emp1”, “pipeline”: [{”$match”: {} }], “as”: “collection1” }} ], “collection2”: [ {”$limit": 1}, { “$lookup”: { “from”: “emp2”, “pipeline”: [{"$match": {} }], “as”: “collection2” }} ] }}, { “$project”: { “data”: { “$concatArrays”: [ {"$arrayElemAt": ["$collection1.collection1", 0] }, {"$arrayElemAt": ["$collection2.collection2", 0] }, ] } }}, { “$unwind”: “$data”}, { “$replaceRoot”: { “newRoot”: “$data”} } ]) 通过 facet 将两表数据先存入各自的数组中,然后 concatArrays 将数组合并,unwind 拆解子记录后,并将它呈现在最外层。SPL 脚本实现则没有那么多“花样”。SPL 脚本:脚本说明: A1:连接 mongodb 数据库。 A2:获取 emp1 表中的数据。 A3:获取 emp2 表中的数据。 A4:合并两表数据。 A5:关闭数据库连接。 熟悉 sql 语句的 mongo 初学者面对数据合并的 mongo 脚本,估计首次遇到时有点“懵”,SPL 脚本就显得自然易懂了。5. 关联嵌套结构情况 1两个关联表,表 A 与表 B 中的内嵌文档信息关联, 且返回的信息在内嵌文档中。表 childsgroup 字段 childs 是嵌套数组结构,需要合并的信息 name 在其下。history:childsgroup:表History中的child_id与表childsgroup中的childs.id关联,希望得到下面结果: { “_id” : ObjectId(“5bab2ae8ab2f1bdb4f434bc3”), “id” : “001”, “history” : “today worked”, “child_id” : “ch001”, “childInfo” : { “name” : “a” } ……………… } Mongo 脚本 db.history.aggregate([ {$lookup: { from: “childsgroup”, let: {child_id: “$child_id”}, pipeline: [ {$match: { $expr: { $in: [ “$$child_id”, “$childs.id”] } } }, {$unwind: “$childs”}, {$match: { $expr: { $eq: [ “$childs.id”, “$$child_id”] } } }, {$replaceRoot: { newRoot: “$childs.info”} } ], as: “childInfo” }}, {"$unwind": “$childInfo”} ]) 这个脚本用了几个函数lookup、pipeline、match、unwind、replaceRoot处理,一般 mongodb 用户不容易写出这样复杂脚本;那我们再看看 spl 脚本的实现:SPL 脚本:关联查询结果:脚本说明: A1:连接 mongodb 数据库。 A2:获取 history 表中的数据。 A3:获取 childsgroup 表中的数据。 A4:将 childsgroup 中的 childs 数据提取出来合并成序表。 A5:表 history 中的 child_id 与表 childs 中的 id 关联查询,追加 name 字段, 返回序表。 A6:关闭数据库连接。相对 mongodb 脚本写法,SPL 脚本的难度降低了不少,省去了熟悉有关 mongo 函数的用法,如何去组合处理数据等,节约了不少时间。6. 关联嵌套结构情况 2两个关联表,表 A 与表 B 中的内嵌文档信息关联, 将信息合并到内嵌文档中。表 txtPost 字段 comment 是嵌套数组结构,需要把 comment_content 合并到其下。txtComment:txtPost期望结果:Mongo 脚本 db.getCollection(“txtPost”).aggregate([ { “$unwind”: “$comment”}, { “$lookup”: { “from”: “txtComment”, “localField”: “comment.comment_no”, “foreignField”: “comment_no”, “as”: “comment.comment_content” }}, { “$unwind”: “$comment.comment_content”}, { “$addFields”: { “comment.comment_content”: “$comment.comment_content.comment_content”}}, { “$group”: { “_id”: “$_id”, ‘post_no’:{"$first": “$post_no”}, “comment”: {"$push": “$comment”} }}, ]).pretty() 表txtPost 按 comment 拆解成记录,然后与表 txtComment 关联查询,将其结果放到数组中,再将数组拆解成记录,将comment_content 值移到 comment 下,最后分组合并。SPL 脚本:脚本说明: A1:连接 mongodb 数据库。 A2:获取 txtPost 表中的数据。 A3:获取 txtComment 表中的数据。 A4:将序表 A2 下的 comment 与 post_no 组合成序表,其中 post_no 改名为 pno。 A5:序表 A4 通过 comment_no 与序表 A3 关联,追加字段 comment_content,将其改名为 Content。 A6:按 pno 分组返回序表,~ 表示当前记录。 A7:关闭数据库连接。7. 关联嵌套结构情况 3两个关联表,表 A 与表 B 中的内嵌文档信息关联, 且返回的信息在记录上。表 collection2 字段 product 是嵌套数组结构,返回的信息是 isCompleted 等字段。测试数据:collection1: { _id: ‘5bc2e44a106342152cd83e97’, description: { status: ‘Good’, machine: ‘X’ }, order: ‘A’, lot: ‘1’ }; collection2: { _id: ‘5bc2e44a106342152cd83e80’, isCompleted: false, serialNo: ‘1’, batchNo: ‘2’, product: [ // note the subdocuments here {order: ‘A’, lot: ‘1’}, {order: ‘A’, lot: ‘2’} ] } 期待结果 { _id: 5bc2e44a106342152cd83e97, description: { status: ‘Good’, machine: ‘X’, }, order: ‘A’, lot: ‘1’ , isCompleted: false, serialNo: ‘1’, batchNo: ‘2’ } Mongo 脚本 db.collection1.aggregate([{ $lookup: { from: “collection2”, let: {order: “$order”, lot: “$lot”}, pipeline: [{ $match: { $expr:{ $in: [ { order: “$$order”, lot: “$$lot”}, “$product”] } } }], as: “isCompleted” } }, { $addFields: { “isCompleted”: {$arrayElemAt: [ “$isCompleted”, 0] } } }, { $addFields: { // add the required fields to the top level structure “isCompleted”: “$isCompleted.isCompleted”, “serialNo”: “$isCompleted.serialNo”, “batchNo”: “$isCompleted.batchNo” } }]) lookup 两表关联查询,首个 addFields获取isCompleted数组的第一个记录,后一个addFields 转换成所需要的几个字段信息SPL 脚本:脚本说明: A1:连接 mongodb 数据库。 A2:获取 collection1 表中的数据。 A3:获取 collection2 表中的数据。 A4:根据条件 order, lot 从序表 A2 中查询记录,然后追加序表 A3 中的字段serialNo, batchNo,返回合并后的序表。 A5:关闭数据库连接。实现从数据记录中的内嵌结构中筛选,将符合条件的数据合并成新序表。8. 多字段分组统计统计分类项下的总数及各子项数。下面统计按 addr 分类 book 数及其下不同的 book 数。期望结果:Mongo 脚本 db.books.aggregate([ { “$group”: { “_id”: { “addr”: “$addr”, “book”: “$book” }, “bookCount”: {"$sum": 1} }}, { “$group”: { “_id”: “$_id.addr”, “books”: { “$push”: { “book”: “$_id.book”, “count”: “$bookCount” }, }, “count”: {"$sum": “$bookCount”} }}, { “$sort”: { “count”: -1} }, { “$project”: { “books”: {"$slice": [ “$books”, 2] }, “count”: 1 }} ]).pretty() 先按 addr,book 分组统计 book 数,再按 addr 分组统计 book 数,调整显示顺序SPL脚本:计算结果: 脚本说明: A1:连接 mongodb 数据库。 A2:获取books表中的数据。 A3:按 addr,book 分组统计 book 数, A4:再按 addr 分组统计 book 数。 A5:将 A4 中的 Total 按 addr 关联后合并到序表中。 A6:关闭数据库连接。9. 两表关联查询从关联表中选择所需要的字段组合成新表。Collection1:collection2:期望结果: Mongo 脚本 db.c1.aggregate([ { “$lookup”: { “from”: “c2”, “localField”: “user1”, “foreignField”: “user1”, “as”: “collection2_doc” }}, { “$unwind”: “$collection2_doc”}, { “$redact”: { “$cond”: [ {"$eq": [ “$user2”, “$collection2_doc.user2”] }, “$$KEEP”, “$$PRUNE” ] }}, { “$project”: { “user1”: 1, “user2”: 1, “income”: “$income”, “output”: “$collection2_doc. output” }} ]).pretty() lookup 两表进行关联查询,redact 对记录根据条件进行遍历处理,project 选择要显示的字段。SPL脚本:脚本说明: A1:连接 mongodb 数据库。 A2:获取c1表中的数据。 A3:获取c2表中的数据。 A4:两表按字段 user1,user2 关联,追加序表 A3 中的 output 字段,返回序表。 A5:关闭数据库连接。 通过 join 把两个关联表不同的字段合并成新表。10. 多表关联查询多于两个表的关联查询,结合成一张大表。Doc1:Doc2:Doc3:合并后的结果: { “_id” : ObjectId(“5901a4c63541b7d5d3293766”), “firstName” : “shubham”, “lastName” : “verma”, “address” : { “address” : “Gurgaon” }, “social” : { “fbURLs” : “http://www.facebook.com”, “twitterURLs” : “http://www.twitter.com” } } Mongo 脚本 db.doc1.aggregate([ {$match: { _id: ObjectId(“5901a4c63541b7d5d3293766”) } }, { $lookup: { from: “doc2”, localField: “_id”, foreignField: “userId”, as: “address” } }, { $unwind: “$address” }, { $project: { “address._id”: 0, “address.userId”: 0, “address.mob”: 0 } }, { $lookup: { from: “doc3”, localField: “_id”, foreignField: “userId”, as: “social” } }, { $unwind: “$social” }, { $project: { “social._id”: 0, “social.userId”: 0 } } ]).pretty(); 由于 Mongodb 数据结构原因,写法也多样化,展示也各不相同。SPL 脚本:此脚本与上面例子类似,只是多了一个关联表,每次 join 就新增加字段,最后叠加构成一张大表。.SPL 脚本的简洁性、统一性就非常明显。11. 指定数组查找从指定的数组中查找符合条件的记录。所给的数组为:[“Chemical”, “Biology”, “Math”]。测试数据:期望结果: Mongodb 脚本 var field = [“Chemical”, “Biology”, “Math”] db.student.aggregate([ { “$project”: { “name”:1, “lessons”: { “$filter”: { “input”: “$lesson”, “cond”: { “$in”: [ “$$this”, field ] } } }, }}, { “$project”: {“name”:1,“lessons”:1,“sizeOflesson”: {"$size": “$lessons”} }}, { $match: { “sizeOflesson”:{ $gt: 0}}} ]) 查询选修课包含[“Chemical”, “Biology”, “Math”]的同学。SPL 脚本:脚本说明: A1:定义查询条件科目数组。 A2:连接 mongodb 数据库。 A3:获取 student 表中的数据。 A4:查询存在数组中的科目记录。 A5:生成字段为 name, lesson 的新序表,其中符合条件的值存放在字段 lesson 中 A6:关闭数据库连接。 集算器对给定数组中查询记录的实现更简明易懂。12. 关联表中的数组查找从关联表记录数据组中查找符合条件的记录, 用给定的字段组合成新表。测试数据:users: workouts: 期望结果:Mongo 脚本 db.users.aggregate([ { “$lookup”: { “from” : “workouts”, “localField” : “workouts”, “foreignField” : “_id”, “as” : “workoutDocumentsArray” }}, {$project: { _id:0,workouts:0} } , {"$unwind": “$workoutDocumentsArray”},; {"$replaceRoot": { “newRoot”: { $mergeObjects: [ “$$ROOT”, “$workoutDocumentsArray”] } } }, {$project: { workoutDocumentsArray: 0} } ]).pretty() 把关联表 users,workouts 查询结果放到数组中,再将数组拆解,提升子记录的位置,去掉不需要的字段。SPL 脚本:脚本说明: A1:连接 mongodb 数据库。 A2:获取 users 表中的数据。 A3:获取 workouts 表中的数据。 A4:查询序表 A3 的 _id 值存在于序表 A2 中 workouts 数组的记录, 并追加 name 字段, 返回合并的序表。 A5:关闭数据库连接。由于需要获取序列的交集不为空为条件,故将 _id 转换成序列。 Mongo 存储的数据结构相对关联数据库更复杂、更灵活,其提供的查询语言也非常强、能适应不同的情况,需要了解函数也不少,函数之间的结合更是变化无穷,因此要掌握并熟悉应用它并非易事。集算器的离散性、易用性恰好能弥补 Mongo 这方面的不足,它降低了 mongo 学习成本及使用 mongo 操作的复杂度、难度,让 mongo 的功能得到更充分的展现,同时也希望 mongo 越来越受到广大爱好者的青睐。 ...

December 29, 2018 · 6 min · jiezi

zanePerfor中一套简单通用的Node前后端Token登录机制和github授权登录方式

HI!,你好,我是zane,zanePerfor是一款我开发的一个前端性能监控平台,现在支持web浏览器端和微信小程序端。我定义为一款完整,高性能,高可用的前端性能监控系统,这是未来会达到的目的,现今的架构也基本支持了高可用,高性能的部署。实际上还不够,在很多地方还有优化的空间,我会持续的优化和升级。开源不易,如果你也热爱技术,拥抱开源,希望能小小的支持给个star。项目的github地址:https://github.com/wangweiang…项目开发文档说明:https://blog.seosiwei.com/per…谈起Token登录机制,相信绝大部分人都不陌生,相信很多的前端开发人员都有实际的开发实践。此文章的Token登录机制主要针对于无实际开发经验或者开发过简单登录机制的人员,如果你是大佬几乎可以略过了,如果你感兴趣或者闲来无事也可以稍微瞅它一瞅。此文章不会教你一步一步的实现一套登录逻辑,只会结合zanePerfor项目阐述它的登录机制,讲明白其原理比写一堆代码来的更实在和简单。zanePerfor项目的主要技术栈是 egg.js、redis和mongodb, 如果你不懂没关系,因为他们都只是简单使用,很容易理解。登录实现结果:如果用户未注册时先注册然后直接登录用户每次登录都会动态生成session令牌同一账号在同一时刻只能在一个地方登录cookie在项目中的作用我们知道http是无状态的,因此如果要知道用户某次请求是否登录就需要带一定的标识,浏览器端http请求带标识常用的方式有两种:1、使用cookie附带标识,2、使用header信息头附带标识。这里我们推荐的方式是使用cooke附带标识,因为它相当于来说更安全和更容易操作。更安全体现在:cookie只能在同域下传输,还可以设置httpOnly来禁止js的更改。更容易操作体现在:cookie传输是浏览器请求时自带的传输头信息,我们不需要额外的操作,cookie还能精确到某一个路径,并且可以设置过期时间自动过期,这样就显得更可控。当然header信息头也有它的优势和用武之地,这里不做阐述。redis在项目中的作用一般的项目我们会把识别用户的标识放存放在Session中,但是Session有其使用的局限性。Session的局限:Session 默认存放在 Cookie 中,但是如果我们的 Session 对象过于庞大,浏览器可能拒绝保存,这样就失去了数据的完整性。当 Session 过大时还会对每次http请求带来额外的开销。还有一个比较大的局限性是Session存放在单台服务器中,当有多台服务器时无法保证统一的登录态。还会带来代码的强耦合性,不能使得登录逻辑代码解耦。因此这里引入redis进行用户身份识别的储存。redis的优势:redis使用简单,redis性能足够强悍,储存空间无限制,多台服务器可以使用统一的登录态,登录逻辑代码的解耦。前端统一登录态封装前端统一登录态应该是每位前端童鞋都做过的事情,下面以zanePerfor的Jquery的AJAX为例做简单的封装为例:// 代码路径:app/public/js/util.jsajax(json) { // …代码略… return $.ajax({ type: json.type || “post”, url: url, data: json.data || “”, dataType: “json”, async: asyncVal, success: function(data) { // …代码略… // success 时统一使用this.error方法进行处理 if (typeof(data) == ‘string’) { This.error(JSON.parse(data), json); } else { This.error(data, json); } }, // …代码略… });};error(data, json) { //判断code 并处理 var dataCode = parseInt(data.code); // code 为1004表示未登录 需要统一走登录页面 if (!json.isGoingLogin && dataCode == 1004) { //判断app或者web if (window.location.href.indexOf(config.loginUrl) == -1) { location.href = config.loginUrl + ‘?redirecturl=’ + encodeURIComponent(location.href); } else { popup.alert({ type: ‘msg’, title: ‘用户未登陆,请登录!’ }); } } else { switch (dataCode) { // code 为1000表示请求成功 case 1000: json.success && json.success(data); break; default: if (json.goingError) { //走error回调 json.error && json.error(data); } else { //直接弹出错误信息 popup.alert({ type: ‘msg’, title: data.desc }); }; } };}前端的逻辑代码很简单,就是统一的判断返回code, 如果未登录则跳转到登录页面。User表结构说明// 代码路径 app/model/user.jsconst UserSchema = new Schema({ user_name: { type: String }, // 用户名称 pass_word: { type: String }, // 用户密码 system_ids: { type: Array }, // 用户所拥有的系统Id is_use: { type: Number, default: 0 }, // 是否禁用 0:正常 1:禁用 level: { type: Number, default: 1 }, // 用户等级(0:管理员,1:普通用户) token: { type: String }, // 用户秘钥 usertoken: { type: String }, // 用户登录态秘钥 create_time: { type: Date, default: Date.now }, // 用户访问时间 });用户表中 usertoken 字段比较重要,它表示每次用户登录时动态生成的Token令牌key, 也是存在在redis中用户信息的key值,此值每次用户登录时都会更新,并且是随机和唯一的。Node Servers端登录逻辑我们先来一张登录的页面业务代码如下:// 代码路径 app/service/user.js// 用户登录 async login(userName, passWord) { // 检测用户是否存在 const userInfo = await this.getUserInfoForUserName(userName); if (!userInfo.token) throw new Error(‘用户名不存在!’); if (userInfo.pass_word !== passWord) throw new Error(‘用户密码不正确!’); if (userInfo.is_use !== 0) throw new Error(‘用户被冻结不能登录,请联系管理员!’); // 清空以前的登录态 if (userInfo.usertoken) this.app.redis.set(${userInfo.usertoken}_user_login, ‘’); // 设置新的redis登录态 const random_key = this.app.randomString(); this.app.redis.set(${random_key}_user_login, JSON.stringify(userInfo), ‘EX’, this.app.config.user_login_timeout); // 设置登录cookie this.ctx.cookies.set(‘usertoken’, random_key, { maxAge: this.app.config.user_login_timeout * 1000, httpOnly: true, encrypt: true, signed: true, }); // 更新用户信息 await this.updateUserToken({ username: userName, usertoken: random_key }); return userInfo; }对照user表来进行逻辑的梳理。每次登录前都会清除上一次在redis中的登录态信息,所以上一次的登录令牌对应的redis信息会失效,因此我们只需要做一个校验用户Token的信息在redis中是否存在即可判断用户当前登录态是否有效。清除上一次登录态信息之后立即生成一个随机并唯一的key值做为新的Token令牌,并更新redis中Token的令牌信息 和 设置新的cookie令牌,这样就保证了以前的登录态失效,当前的登录态有效。redis 和 cookie 都设置相同的过期时间,以保证Token的时效性和安全性。cookie的httpOnly 我们需要开启,这样就保证的Token的不可操作性,encrypt 和 signed参数是egg.js 的参数,主要负责对cookie进行加密,让前端的cookie不已明文的方式呈现,提高安全性。最后再更新用户的Token令牌信息,以保证用户的Token每次都是最新的,也用以下次登录时的清除操作。Servers 端用户登录校验中间件中间件的概念相信大家都不陌生,用过koa,express和redux都应该知道,egg.js的中间件来自于与koa,在这里就不说概念了。在zanePerfor项目中我们只需要对所有需要进行登录校验的路由(请求)进行中间件校验即可。在egg中可这样使用:// 代码来源 app/router/api.js// 获得controller 和 middleware(中间件)const { controller, middleware } = app;// 对需要校验的路由进行校验// 退出登录apiV1Router.get(‘user/logout’, tokenRequired, user.logout);业务代码如下:// 代码路径 app/middleware/token_required.js// Token校验中间件module.exports = () => { return async function(ctx, next) { const usertoken = ctx.cookies.get(‘usertoken’, { encrypt: true, signed: true, }) || ‘’; if (!usertoken) { ctx.body = { code: 1004, desc: ‘用户未登录’, }; return; } const data = await ctx.service.user.finUserForToken(usertoken); if (!data || !data.user_name) { ctx.cookies.set(‘usertoken’, ‘’); const descr = data && !data.user_name ? data.desc : ‘登录用户无效!’; ctx.body = { code: 1004, desc: descr, }; return; } await next(); };};// finUserForToken方法代码路径// 代码路径 app/service/user.js// 根据token查询用户信息async finUserForToken(usertoken) { let user_info = await this.app.redis.get(${usertoken}_user_login); if (user_info) { user_info = JSON.parse(user_info); if (user_info.is_use !== 0) return { desc: ‘用户被冻结不能登录,请联系管理员!’ }; } else { return null; } return await this.ctx.model.User.findOne({ token: user_info.token }).exec();}逻辑梳理:首先会获得上传的token令牌,这里cookie.get方法的 encrypt 和 signed 需要为true,这会把Token解析为明文。在finUserForToken方法中主要是获取Token令牌对应的redis用户信息,只有当用户的信息为真值时才会通过校验在中间件这一环节还有一个比较常规的验证 就是 验证请求的 referer, referer也是浏览器请求时自带的,在浏览器端不可操作,这相对的增加了一些安全性(项目中暂未做,这个验证比较简单,如果有需要的自己去实现)。到此zanePerfor的Token校验机制其实已经完全实现完了,只是未做整体的总结,下面来继续的完成注册的逻辑。用户注册逻辑实现业务代码如下:// 代码路径 app/service/user.js// 用户注册async register(userName, passWord) { // 检测用户是否存在 const userInfo = await this.getUserInfoForUserName(userName); if (userInfo.token) throw new Error(‘用户注册:用户已存在!’); // 新增用户 const token = this.app.randomString(); const user = this.ctx.model.User(); user.user_name = userName; user.pass_word = passWord; user.token = token; user.create_time = new Date(); user.level = userName === ‘admin’ ? 0 : 1; user.usertoken = token; const result = await user.save(); // 设置redis登录态 this.app.redis.set(${token}_user_login, JSON.stringify(result), ‘EX’, this.app.config.user_login_timeout); // 设置登录cookie this.ctx.cookies.set(‘usertoken’, token, { maxAge: this.app.config.user_login_timeout * 1000, httpOnly: true, encrypt: true, signed: true, }); return result;}用户注册的代码比较简单,首先检测用户是否存在,不存在则储存生成动态并唯一的Token令牌,并保持数据到redis 和设置 cookie令牌信息, 这里都设置相同的过期时间,并加密cookie信息和httpOnly。退出登录逻辑退出登录逻辑很简单,直接清除用户Token对应的redis信息和cookie token令牌即可。// 登出logout(usertoken) { this.ctx.cookies.set(‘usertoken’, ‘’); this.app.redis.set(${usertoken}_user_login, ‘’); return {};}冻结用户逻辑冻结用户的逻辑也比较简单,唯一需要注意的是,冻结的时候需要清除用户Token对应的redis信息。// 冻结解冻用户async setIsUse(id, isUse, usertoken) { // 冻结用户信息 isUse = isUse * 1; const result = await this.ctx.model.User.update( { _id: id }, { is_use: isUse }, { multi: true } ).exec(); // 清空登录态 if (usertoken) this.app.redis.set(${usertoken}_user_login, ‘’); return result;}删除用户逻辑删除用户逻辑跟冻结用户逻辑一致,也需要注意清除用户Token对应的redis信息。// 删除用户async delete(id, usertoken) { // 删除 const result = await this.ctx.model.User.findOneAndRemove({ _id: id }).exec(); // 清空登录态 if (usertoken) this.app.redis.set(${usertoken}_user_login, ‘’); return result;}第三方github登录说明根据zanePerfor的登录校验机制可以得出以下的结论:User表的用户名必须存在,密码可无,并且用户名在代码中强校验不能重复,但是在数据库中用户名是可以重复的。usertoken字段很重要,是实现所有Token机制的核心字段,每次登录和注册都会是随机并唯一的值基于以上两点做第三方登录我们只需要实现以下几点即可:只要给用户名赋值即可,因为用户密码登录和第三方登录是两套逻辑,因此用户名可以重复,这就解决了第三方登录一定不会存在用户已注册的提示。第一次登录时注册用户,并把第三方的用户名当做表的用户名,第三方的secret作为用户的token字段。第二次登录时使用token字段检测用户是否已注册,已注册走登录逻辑,未注册走注册逻辑。// 代码地址 app/service/user.js// github register 核心注册逻辑async githubRegister(data = {}) { // 此字段为github用户名 const login = data.login; // 此字段为github 唯一用户标识 const token = data.node_id; let userInfo = {}; if (!login || !token) { userInfo = { desc: ‘github 权限验证失败, 请重试!’ }; return; } // 通过token去查询用户是否存在 userInfo = await this.getUserInfoForGithubId(token); // 身材Token随机并唯一令牌 const random_key = this.app.randomString(); if (userInfo.token) { // 存在则直接登录 if (userInfo.is_use !== 0) { userInfo = { desc: ‘用户被冻结不能登录,请联系管理员!’ }; } else { // 清空以前的登录态 if (userInfo.usertoken) this.app.redis.set(${userInfo.usertoken}_user_login, ‘’); // 设置redis登录态 this.app.redis.set(${random_key}_user_login, JSON.stringify(userInfo), ‘EX’, this.app.config.user_login_timeout); // 设置登录cookie this.ctx.cookies.set(‘usertoken’, random_key, { maxAge: this.app.config.user_login_timeout * 1000, httpOnly: true, encrypt: true, signed: true, }); // 更新用户信息 await this.updateUserToken({ username: login, usertoken: random_key }); } } else { // 不存在 先注册 再登录 const user = this.ctx.model.User(); user.user_name = login; user.token = token; user.create_time = new Date(); user.level = 1; user.usertoken = random_key; userInfo = await user.save(); // 设置redis登录态 this.app.redis.set(${random_key}_user_login, JSON.stringify(userInfo), ‘EX’, this.app.config.user_login_timeout); // 设置登录cookie this.ctx.cookies.set(‘usertoken’, random_key, { maxAge: this.app.config.user_login_timeout * 1000, httpOnly: true, encrypt: true, signed: true, }); } return userInfo;}详细的github第三方授权方式请参考:https://blog.seosiwei.com/per…总结:前端封装统一的登录验证,项目中 code 1004 为用户未登录,1000为成功。user数据表中储存一个usertoken字段,此字段是随机并唯一的标识,在注册时存入此字段,在每次登录时更新此字段。浏览器端的Token令牌即usertoken字段,redis的每个Token存储的是相应的用户信息。每次登录时清除上一次用户的登录信息,即清除redis登录校验信息,这样就能保证同一用户同一时间只能在一个地方登录。usertoken字段是随时在变的,redis用户信息和cookie Token令牌都有过期时间,cookie经过加密和httpOnly,更大的保证了Token的安全性。对所有需要校验的http请求做中间件校验,通过Token令牌获取redis用户信息并验证,验证即通过,验证失败则重新去登录。第三方登录使用token做用户是否重复校验,第一次时登录注册,第二次登录时则走登录逻辑。原文地址:https://blog.seosiwei.com/det… ...

December 27, 2018 · 4 min · jiezi

MongoDB固定集合(capped collection)

一 . 什么是固定集合MongoDB中有一种特殊类型的集合,值得我们特别留意,那就是固定集合(capped collection)。固定集合可以声明collection的容量大小,其行为类似于循环队列。数据插入时,新文档会被插入到队列的末尾,如果队列已经被占满,那么最老的文档会被之后插入的文档覆盖。固定集合特性:固定集合很像环形队列,如果空间不足,最早的文档就会被删除,为新的文档腾出空间。一般来说,固定集合适用于任何想要自动淘汰过期属性的场景。固定集合应用场景比如日志文件,聊天记录,通话信息记录等只需保留最近某段时间内的应用场景,都会使用到MongoDB的固定集合。固定集合的优点1.写入速度提升。固定集合中的数据被顺序写入磁盘上的固定空间,所以,不会因为其他集合的一些随机性的写操作而“中断”,其写入速度非常快(不建立索引,性能更好)。2.固定集合会自动覆盖掉最老的文档,因此不需要再配置额外的工作来进行旧文档删除。设置Job进行旧文档的定时删除容易形成性能的压力毛刺。固定集合非常实用与记录日志等场景。二 . 固定集合的创建不同于普通集合,固定集合必须在使用前显式创建。例如,创建固定集合coll_testcapped,大小限制为1024个字节。db.createCollection(“coll_testcapped”,{capped:true,size:1024});除了大小,创建时还可以指定固定集合中文档的数据量。例如,创建固定集合coll_testcapped,大小限制为1024个字节,文档数量限制为100。db.createCollection(“coll_testcapped2”,{capped:true,size:1024,max:100});创建固定集合还有另一途径,就是将普通集合装换为固定集合,使用的命令是convertToCapped。例如将testcol1集合转换为一个大小为1024字节的固定集合:db.runCommand({“convertToCapped”:“testcol1”,“size”:1024})三 . 固定集合信息的查看(1)判断集合是否为固定集合,其判定命令为:db.集合.isCapped() 。例如判断前面已创建的固定集合coll_testcapped2是否为固定集合:(2) 从集合信息中获取 有关固定集合的属性,查看集合的指令为:db.集合.stats()例如查看集合coll_testcapped2的信息:四 . 注意事项:(1) 固定集合创建之后就不可以改变,只能将其删除重建。(2) 普通集合可以使用convertToCapped转换固定集合,但是固定集合不可以转换为普通集合。(3) 创建固定集合,为固定集合指定文档数量限制时(指参数max),必须同时指定固定集合的大小(指参数size)。不管先达到哪一个限制,之后插入的新文档都会把最老的文档移除集合。(4) 使用convertToCapped命令将普通集合转换固定集合时,既有的索引会丢失,需要手动创建。并且,此转换命令没有限制文档数量的参数(即没有max的参数选项)。(5) 不可以对 固定集合 进行分片。(6) 对固定集合中的文档可以进行更新(update)操作,但更新不能导致文档的Size增长或缩小,否则更新失败。假如集合中有一个key,其value 对应的数据长度为100个字节,如果要更新这个key 对应的value,更新后的值也必须为100个字节,大于100个字节不可以,小于100个字节也不可以。报错信息为:Cannot change the size of a document in a capped collection : XXXX(XXXX代表某个数据字) !=XXXX。(7) 不可以对固定集合执行删除文档操作,但可以删除整个集合。删除文档时,报错信息为:cannot remove from a capped collection:XXXX(8) 还有一定需要注意,对集合估算size时,不要依据集合的storageSize ,而是依据集合的size。storageSize是wiredTiger存储引擎采用高压缩算法压缩后的。例如通过db.集合.stats()命令查看某集合的数据,“size” 和 “storageSize” 二者相差还是很大的。您可能感兴趣的文章:MongoDB在不同主机间复制数据库和集合的教程详解MongoDB中创建集合与删除集合的操作方法文章同步发布: https://www.geek-share.com/de…

December 27, 2018 · 1 min · jiezi

mongoose再认识(二)

在开发中,除了使用mongoose进行一些基本的操作外,就是一些技巧的使用。文章接续mongoose再认识(一),下文中使用代码可参考这篇文章中的。虚拟字段虚拟字段,从字面意思就可以明白,它不是真正的字段,不存在与数据库中,但是当使用model实例查询时,却可以灵活的运用这个字段。注:这个特性是mongoose自己的,与mongo无关。…// 添加了一个虚拟的fullname字段// get fullnameUserSchema .virtual(‘fullname’) .get(() => this.firstname + ’ ’ + this.lastname)// set fullnameUserSchema .virtual(‘fullname’) .set((name) => let arr = name.split(’ ‘), this.firstname = arr[0], this.lastname = arr[1] )// readUserModel .find({}) .exec() .then(doc => { console.log(doc[0]) })查询的结果如下:{ _id: 5c1dc7248aaf9c2c80fee915, firstname: ‘东坡’, lastname: ‘苏’, __v: 0 }那么,如何获取到结果fullname呢?可以通过doc[0].fullname来获取。如何对数据进行保存呢?代码如下:// 模拟AJAX请求保存数据let person2 = new UserModel()person2.fullname = ‘白 李’person2 .save() .then(doc => console.log(doc)) .catch(err => console.log(err))返回结果:{ _id: 5c1dd7ef535df51980e9fd98, firstname: ‘白’, lastname: ‘李’, __v: 0 }这样,在开发的过程中,就不用担心因为字段不匹配而需要修改数据库的问题。这也是它存在的意义。有兴趣的同学可参考node club中对user.js中用户的分级,不需要在建立一个字段用来保存用户的等级,可以用virtual Type通过socre计算来得出来。在Schema定义一些Model实例常用的方法熟悉mongoose的原理的都知道,Model的构造函数是在Schema实例的基础上创造出来的。所以,对于频繁操作的Model实例方法,可以在Schema的实例上进行定义(具体的可参考JavaScript的prototype)。在一个Schema中经常会带有updateAt和createAt这样的字段,通常的情况下,会给它们一个默认的值。userSchema代码修改如下:let UserSchema = new mongoose.Schema({ firstname: String, lastname: String, createAt: { type: Date, default: Date.now }, updateAt: { type: Date, default: Date.now }})在开发中,开发者往往不会手动的处理它们,但是对于跟踪记录一个数据来说又很必要,也不允许用对这些数据任意的修改。那么,应该如何操作它才是最好的呢?当然,最好就是在执行post请求的时候,会有一些方法会根据一定机制自动保存。而mongoose就存在这样的机制,可以在Schema的实例上添加pre的方法,代码如下:UserSchema.pre(‘save’, function(next) { let now = Date.now() this.updateAt = now; if (!this.createAt) this.createAt = now; next()})模拟AJAX请求保存数据:let person3 = new UserModel()person3.fullname = ‘甫 杜’person3 .save() .then(doc => console.log(doc)) .catch(err => console.log(err))返回结果:{ _id: 5c1e006204bad42224374aea, createAt: 2018-12-22T09:14:10.862Z, updateAt: 2018-12-22T09:14:10.877Z, firstname: ‘甫’, lastname: ‘杜’, __v: 0 }这个觉过并不能说明问题,它是Schema定义时和pre方法共同作用的结果。尝试更新数据来验证定义的方法,代码如下:UserModel.findOne({ lastname: ‘杜’}).exec().then(function(doc) { doc.lastname = ‘杜’ doc.firstname = ‘甫’ doc.save() .then(doc => { console.log(doc) }) .catch(err => { console.error(err) })}).catch(err => console.log(err))返回结果:{ _id: 5c1e006204bad42224374aea, createAt: 2018-12-22T09:14:10.862Z, updateAt: 2018-12-22T09:15:04.398Z, firstname: ‘牧’, lastname: ‘杜’, __v: 0 }这里,我们使用save对数据进行更新,当然这对于跟踪用户的操作行为很有好处,但是并不是所有的数据都需要的,而对于哪些不需要的,还是可以考虑使用findOneAndUpdate,updae,updateMany的。细心的同学会发现,其实它和shell命令的db.users.insert({})类似,user.save({})是插入一条数据,而后者则可以插入多条数据。注:在使用操作数据库中的数据时一定要注意,要操作的时user.find()或user.findOne()返回的一整条数据,如果是实例化了一个UserModel,则会造成数据库中的数据丢失。 ...

December 23, 2018 · 1 min · jiezi

前端工程师的 2018 年总结

前言时间过得很快,2018 年已经接近尾声了。离开大学校园已经一年半,正式工作也一年半了。2018 年,我的本命年,今年 24 岁,离 “而立之年” —— 30 岁, 又近了一步。今年对我而言,是人生的一个重要节点。今年是我觉得过得最快的一年,也是成长最多的一年。2. 技术作为一名代码搬运工,技术做为安身立命的本钱,今年技术上有了挺大的见长。技术上,前端和后端都接触到当前流行的技术栈,前端方面有: vue.js 、react.js ;后端方面有:python 3 、node、express、mongodb、mysql。但是这些应用层的知识都是次要的,学到的编程能力和编程思维才是最重要的,毕竟一门通,门门通。况且对于程序员来说,编程能力和编程思维占了 80%,其他 api 的运用只占了 20%。2.1 前端对于 vue 的相关技术栈,虽然之前也有在用,但今年是技术上达到熟练的一年,做过 公众号、pc 端管理后台、H 5 应用。经过几个的项目的锤炼,应用上应该达到了熟练程度,也学到了不少好用的技巧。而 react 相关技术栈 ,是今年后半年学的。学而不用,等于没学。 所以要实战一下才行,所以做了个博客网站的项目,也就是本人现在的个人网站,并把项目源码开源在 github 上。这个过程中,也学到了一些常用的、基本的 api ,对一般的 react 项目,也能自行搭建和开发了。今年还看完了一本书:【WebKit 技术内幕】。看的不是纸质版的,是 pdf 的电子版,对浏览器和 WebKit 也有了一丢丢深入的了解,随着时间的久远,忘得差不多了 ????。2.2 后端python 3 和 mysql 是前半年学的,最初想着边做前端边能用 python 的,不过没找到相应的工作,最后还是做前端,现在很久没用,也忘得差不多了啦 ????。对于 node、express、mongodb 是今年后半年学的,主要是为了快速搭建博客网站后端的。虽然还有很多要优化的地方,特别是数据的查询方面,但是最终还是搭建出来了。过程中,发现 node 比 python 好学,毕竟是用的是 javaScript 语言。对于编程也有了一丢丢的理解。之前看到阮一峰老师的一篇文章内容,说得好有道理。他的原文是这样说的:在此引用一个开发者对年轻程序员的告诫:在软件开发中,技术变化如此之快,你花费了大量时间学习技术和工具,一旦这些技术被取代,你的知识将变得毫无价值,因为它们大部分都是实施的细节。我最近总是在想这段话,软件开发算不算是真正的知识 ?如果它是一种真正的知识,那么理论上,我们学到的东西大部分应该不会过时,就好像微积分不会过时一样。可是实际上,我们都知道,软件开发技能有时效性,十年前学习的编程知识,十年后几乎肯定不能用于生产。那样的话,软件开发就不能算真正的知识,只是一种实施的细节。公司旁边有一家税务所,每天都有很多人排队交税。如果你是第一次来交税,肯定搞不清楚怎么交,交税是一门学问,必须有人教你,要带哪些证件,要填哪些表,去哪些窗口排队等等。我现在认为,学习编程跟学习交税是一样的,都是学习实施的细节。一旦外部环境变了,原来的实施细节就没用了。 当代编程由于层层的抽象和封装,我们已经不必接触底层真正具有通用性的知识了。大部分时候,所谓编程就是在写某个抽象层的配置。比如,网页样式就是在写 CSS 配置,你很难说这到底是真正的知识,还是像《办税指南》那样的实施细节。实施细节并不是知识,而是操作步骤。如果技术栈发生变更,实施细节就会毫无用处。但是,你又不能不学习它,不知道实施细节,就没法做出项目。我觉得,程序员应该要警惕,不要落入实施细节的陷阱,不要把全部精力花在实施细节上面,然后以为自己学到了真正的知识。对待各种语言和工具,正确的态度应该是“进得去,出得来”,既要了解足够的细节,也要能够站在宏观的角度看待它,探寻底层到底是怎么实现的。3. 工作今年 5 月份的时候,换了东家。在上一家东家那里学到了很多东西,毕竟是刚毕业后工作的第一年。上一家东家的两位前端老大和另外二位后端开发,无论是技术还是做事上,对我都产生了比较大的影响,感谢。上一家东家的工作氛围还是很好的,特别怀念的是每周五一次的运动啊。在现在的公司,也不错,也有不少学习的榜样,就少了活动与运动节目。我一直认为一个合格的程序员,正常的工作安排,应该都是在上班时间高效的做完的,下班了就准时下班的。所以很多时候,我都是下午 5 点半 准时下班的,毕竟回去之后,想做的事还有一堆呢。但是非正常的工作安排就不一定了,比如项目很紧。因为项目时间紧,今年试过那么几次加班修 bugger 到凌晨 3 点的,然后早上七点多起来继续的。还试过一次项目中的数据被同事误删了,要配合后端开发抢救的,抢救到接近凌晨 4 点,第二天早上 8 点多照常起来正常上班的。只能感叹一声:修仙真棒,年轻真好!!!4. 运动身体是一,金钱、地位、荣誉则是零,只有有了前面的一,后面的零才会有用;反之,则都是做了无用功。这一年来,还是和往年一样,时不时会进行各种运动,运动的项目一般有:跑步,健身,羽毛球,骑行等。跑步的频率大概每月平均有 3 次吧,每次一般都是 5 公里;健身大概每周 2 次;羽毛球就得看有没有合适的时机了;而骑行呢,现在是只要天气许可,下班都是骑车回去,因为比搭公交车实在是快太多了,时间宝贵啊。不间断的运动也慢慢成为了一种习惯。正因为一直有不间断的运动,所以这一年来又没有感冒过,身体还算健康。图一是 2018-07-17 到 2018-12-18 期间,所有运动的数据,以骑行为主。图二是 2017-03-20 到 2018-07-15 期间,所有运动的数据,以跑步为主。两图的总路程加起来,够回家两趟了 ????。这不间断的运动,也不算什么坚持,只是觉得应该做的,又刚好是喜欢做的事情而已。一直做着就成了习惯,能做自己喜欢的事情是一件幸福的事情。当然,现在正值冬季,户外运动的频率要相应减少好一点。运动带来的益处真的是没法估量,大学四年在校期间都没有生病过,只在大一寒假在家的时候,感冒了一次。近 5 年来,还有一次感冒是一年多前,刚毕业找工作的时候,被两同学轮流感冒传染,最终没能顶住。还有的益处就是保持着一个健康的身形,腹肌,胸肌都还在,只是这一年感觉肚皮比之前厚了一点了 ????。一天坐十几个小时,来程序员来说真的很伤。当运动成为一种习惯,终将会是受益一生的事情。5. 额外技能在 21 世纪, 写作、英语和编程 估计是最有前途的技能。5.1 写作今年掌握的最有用的技能应该就是 写作 了,估计这个是受用一生的技能。今年 7 月份的时候,我的同学,外号:陈经理,开了个公众号 【 一人优秀的废人 】,并在上面写博客。他也叫我写,一直坚持会有很大的收获。之前一直都想写技术博客文章的,但是没写过,也没下定决心去做。7 月份的时候,我也下定决心开始写技术博客,并开了个公众号 【 BiaoChenXuYing 】,分享自己的技术与成长,目前粉丝有几百人。逐渐地,写作又成了一个爱好与习惯。当一项技能变成爱好的时候,就能产生很巨大的能量(就像很多人喜欢玩的王者农药)。自从写作以来,利用在学习上的时间比之前多了,学习知识的时候有了一定的深入,毕竟要写给别人看的,自己如果都不理解,别人又怎会能懂呢。这半年时间陆续写了 30 多篇文章,其中包含读书的笔记、随笔、技术文章,有写得不怎么样的,有写的挺好的,获得多人点赞的。虽然有时会参考一下别人的文章,但还是一直鼓励原创与坚持原创。大概只有作者才能懂原创的不易。写博客半年以来,也见到了不一样的风景。文章写的好,会有编辑找你出书;会有猎头顾问找上你,给你介绍大厂的好工作;会有好公司的团队负责人找你,想你加入他们的团队等等。这些都有遇到过,但自知现在能力还欠缺,还要不断积累与沉淀。写作是展现自己才华与能力很好的方式,当积累到一定的程度,好机会自会找上门来。如果 30 岁之后,不想写代码了或者写不动了,还能有一个额外技能可以谋生,且这个技能还是自己的一个兴趣来的,是多么快乐的一件事。努力成为一个斜杠青年才是正途。(斜杠青年:不仅指那些有着多重身份,多重收入的人。它代表的是一种全新的人生价值,核心在于多元化的人生。)花一样的年纪,该奋斗的年纪不要安逸,实现梦想的同时顺便赚点钱,何乐而不为?5.2 做一顿好吃的今年后半年里,还尝试自己做饭、煲粥和煲汤。虽然这些都会,但是刚出来工作后的一年里,还没做过呢。不过都很少做饭做菜,因为实在是太耗费时间了啊,还要在旁边看着。大多都是煲粥煲汤,放好各种材料就可以了,接着就是:一边玩电脑,一边等着吃就可以了,非常方便啊。做一顿好吃的饭菜,也是一个必备的技能,毕竟 自己动手,丰衣足食。5.3 理财理财 – 人生必会技能。今年开始尝试用其他方式理财了,比如买股票,买基金,买活期产品等,虽然还一直在亏,但是都尝试一下,才知道这些东西好不好嘛。当然理财的水也很深,不是一朝一日就可学会的,需要长期研究才行。出来社会之后,对钱的理解越来越深刻了,长大后 99% 的烦恼是因为没钱。6. 娱乐今年大多数空闲的时间都是和电脑度过了。看电影,看电视剧,看动漫还有运动。今年看了很多动漫,热门的 3D 国漫都看过了,或者在追着看。国漫真的强势掘起了。今年是外出游玩最少的一年了,好像没有主动去过哪里游玩,仅有的几次都是身边的朋友叫去的。其实想去的地方真的还很多。只是还没有找到合适的那个人,和其一起去。祖国山河那么秀丽,还是得努力挣钱,去看一看。7. 期望 20192018 年完成的事:[x] 上手 react 技术栈[x] 上手 node 技术栈[x] 完成了自己的个人博客网站[x] 在 github 上开源了博客网站的源码[x] 把写作培养成了一种习惯[x] 运营个人公众号 【 BiaoChenXuYing 】[x] 不间断的运动,保持健康的体魄[x] 看完了一本书【WebKit 技术内幕】[x] 初尝其他理财方式[x] 做一顿好吃的2019 的目标:[ ] 把个人博客网站接入到公众号里面[ ] 深入 vue 技术栈的原理与内在实现[ ] 熟练 react 和 node 技术栈,可能还要学 java。[ ] 学习算法与数据结构[ ] 英语词汇量达到 7000[ ] 加大运动量,增重 5 斤[ ] 坚持写作,运营好公众号[ ] 多看书与文章(书到用时方恨少)[ ] 逐渐深入其他理财方式[ ] 培养其他技能当一个目标需要很长远的时间来实现时,那就将每天要做的事培养成习惯,就会变得很容易了(比如:英语)。现在正值冬天,天气冷,什么都不想干。最近在学英语,跟着水滴阅读看英语原著,总是想看就看,不想看就不看了,觉得每天的任务只是个任务,还没习惯。得把每天花 20 分钟看英语原著培养成习惯才行。最终目标是 尽早实现个人财富自由,做自己喜欢的事情。我比较赞成的财务自由的解释是:所谓的财务自由,指的是某人再也不用为了满足生活必需而出售自己的时间了。不是生活所迫,谁特么想努力!目标还是要有的,不然和咸鱼有什么区别 ?虽然 努力了不一定有结果,但是不努力一定很舒服。没被生活折磨过只有两种可能,其一是有人替你扛了,其二是别着急,还没轮到你。8. 总结致敬将要过去的 2018 ,期望 2019。要么不努力,让生活选择你,随波逐流;要么自己选择生活,做自己喜欢的事。无论在哪里工作,无论做什么工作,握住能掌控的生活,遇见更好的自己,便是一件特别幸运又足够幸福的事。往后余生,愿你我都能,从前生活是工作,今后工作是生活。时光正好,未来还有无限可能,加油!9. 最后微信公众号:BiaoChenXuYing分享 前端、后端开发等相关的技术文章,热点资源,随想随感,全栈程序员的成长之路。关注公众号并回复 福利 便免费送你视频资源,绝对干货。福利详情请点击: 免费资源分享–Python、Java、Linux、Go、node、vue、react、javaScript

December 19, 2018 · 1 min · jiezi

Mongodb 与 MySQL对比

在数据库存放的数据中,有一种特殊的键值叫做主键,它用于惟一地标识表中的某一条记录。也就是说,一个表不能有多个主键,并且主键不能为空值。无论是MongoDB还是MySQL,都存在着主键的定义。对于MongoDB来说,其主键名叫”_id”,在生成数据的时候,如果用户不主动为其分配一个主键的话,MongoDB会自动为其生成一个随机分配的值。在MySQL中,主键的指定是在MySQL插入数据时指明PRIMARY KEY来定义的。当没有指定主键的时候,另一种工具 —— 索引,相当于替代了主键的功能。索引可以为空,也可以有重复,另外有一种不允许重复的索引叫惟一索引。如果既没有指定主键也没有指定索引的话,MySQL会自动为数据创建一个。存储速度对比1. 数据库的平均插入速率:MongoDB不指定_id插入 > MySQL不指定主键插入 > MySQL指定主键插入 > MongoDB指定_id插入。2. MongoDB在指定_id与不指定_id插入时速度相差很大,而MySQL的差别却小很多。分析:1. 在指定_id或主键时,两种数据库在插入时要对索引值进行处理,并查找数据库中是否存在相同的键值,这会减慢插入的速率。2. 在MongoDB中,指定索引插入比不指定慢很多,这是因为,MongoDB里每一条数据的_id值都是唯一的。当在不指定_id插入数据的时候,其_id是系统自动计算生成的。MongoDB通过计算机特征值、时间、进程ID与随机数来确保生成的_id是唯一的。而在指定_id插入时,MongoDB每插一条数据,都需要检查此_id可不可用,当数据库中数据条数太多的时候,这一步的查询开销会拖慢整个数据库的插入速度。3. MongoDB会充分使用系统内存作为缓存,这是一种非常优秀的特性。我们的测试机的内存有64G,在插入时,MongoDB会尽可能地在内存快写不进去数据之后,再将数据持久化保存到硬盘上。这也是在不指定_id插入的时候,MongoDB的效率遥遥领先的原因。但在指定_id插入时,当数据量一大内存装不下时,MongoDB就需要将磁盘中的信息读取到内存中来查重,这样一来其插入效率反而慢了。4. MySQL不愧是一种非常稳定的数据库,无论在指定主键还是在不指定主键插入的情况下,其效率都差不了太多。插入稳定性分析插入稳定性是指,随着数据量的增大,每插入一定量数据时的插入速率情况。在本次测试中,我们把这个指标的规模定在10w,即显示的数据是在每插入10w条数据时,在这段时间内每秒钟能插入多少条数据。先呈现四张图上来:1. MongoDB指定_id插入:2. MongoDB不指定_id插入:3. MySQL指定PRIMARY KEY插入:4. MySQL不指定PRIMARY KEY插入:总结:整体上的插入速度还是和上一回的统计数据类似:MongoDB不指定_id插入 > MySQL不指定主键插入 > MySQL指定主键插入 > MongoDB指定_id插入。从图中可以看出,在指定主键插入数据的时候,MySQL与MongoDB在不同数据数量级时,每秒插入的数据每隔一段时间就会有一个波动,在图表中显示成为规律的毛刺现象。而在不指定插入数据时,在大多数情况下插入速率都比较平均,但随着数据库中数据的增多,插入的效率在某一时段有瞬间下降,随即又会变稳定。整体上来看,MongoDB的速率波动比MySQL的严重,方差变化较大。MongoDB在指定_id插入时,当插入的数据变多之后,插入效率有明显地下降。在其他三种的插入测试中,从开始到结束,其插入的速率在大多数的时候都固定在一个标准上。分析:毛刺现象是因为,当插入的数据太多的时候,MongoDB需要将内存中的数据写进硬盘,MySQL需要重新分表。这些操作每当数据库中的数据达到一定量级后就会自动进行,因此每隔一段时间就会有一个明显的毛刺。MongoDB毕竟还是新生事物,其稳定性没有已应用多年的MySQL优秀。MongoDB在指定_id插入的时候,其性能的下降还是很厉害的。在读取的数据规模不大时,MongoDB的查询速度真是一骑绝尘,甩开MySQL好远好远。在查询的数据量逐渐增多的时候,MySQL的查询速度是稳步下降的,而MongoDB的查询速度却有些起伏。分析:如果MySQL没有经过查询优化的话,其查询速度就不要跟MongoDB比了。MongoDB可以充分利用系统的内存资源,我们的测试机器内存是64GB的,内存越大MongoDB的查询速度就越快,毕竟磁盘与内存的I/O效率不是一个量级的。本次实验的查询的数据也是随机生成的,因此所有待查询的数据都存在MongoDB的内存缓存中的概率是很小的。在查询时,MongoDB需要多次将内存中的数据与磁盘进行交互以便查找,因此其查询速率取决于其交互的次数。这样就存在这样一种可能性,尽管待查询的数据数目较多,但这段随机生成的数据被MongoDB以较少的次数从磁盘中取出。因此,其查询的平均速度反而更快一些。这样看来,MongoDB的查询速度波动也处在一个合理的范围内。MySQL的稳定性还是毋庸置疑的。结论相比较MySQL,MongoDB数据库更适合那些读作业较重的任务模型。MongoDB能充分利用机器的内存资源。如果机器的内存资源丰富的话,MongoDB的查询效率会快很多。在带”_id”插入数据的时候,MongoDB的插入效率其实并不高。如果想充分利用MongoDB性能的话,推荐采取不带”_id”的插入方式,然后对相关字段作索引来查询。MongoDB适合那些对数据库具体数据格式不明确或者数据库数据格式经常变化的需求模型,而且对开发者十分友好。MongoDB官方就自带一个分布式文件系统,可以很方便地部署到服务器机群上。MongoDB里有一个Shard的概念,就是方便为了服务器分片使用的。每增加一台Shard,MongoDB的插入性能也会以接近倍数的方式增长,磁盘容量也很可以很方便地扩充。MongoDB还自带了对map-reduce运算框架的支持,这也很方便进行数据的统计。MongoDB的缺陷事务关系支持薄弱。这也是所有NoSQL数据库共同的缺陷,不过NoSQL并不是为了事务关系而设计的,具体应用还是很需求。稳定性有些欠缺,这点从上面的测试便可以看出。MongoDB一方面在方便开发者的同时,另一方面对运维人员却提出了相当多的要求。业界并没有成熟的MongoDB运维经验,MongoDB中数据的存放格式也很随意,等等问题都对运维人员的考验。

December 18, 2018 · 1 min · jiezi

Node.js爬取科技新闻网站cnBeta(附前端及服务端源码)

前言一直很喜欢看科技新闻,多年来一直混迹于cnBeta,以前西贝的评论区是匿名的,所以评论区非常活跃,各种喷子和段子,不过也确实很欢乐,可以说那是西贝人气最旺的时候。然而自从去年网信办出台了《互联网跟帖评论服务管理规定》,要求只有实名认证的用户,才能进行留言、评论之后,往日的活跃的的评论区瞬间沦陷,人气大跌。其实说到底,还是西贝没有跟上移动互联网的潮流,至今还止步于PC互联网时代,网页广告太多,而移动应用质量堪忧,体验极差,虽然有不少第三方的应用,但由于没有官方的支持,体验上还是不够好,例如如果官方发布一些改版,第三方的应用基本都会挂掉。所以为了方便平时阅读cnBeta的新闻,就打算通过爬虫把cnBeta的新闻爬下来,自建一个m站,这样体验可控,并且没有广告(`∀´)。其实项目很早就完成了,只是现在才有空(闲情)写一篇分享出来。概述本项目爬虫及服务端github地址:https://github.com/hudingyu/c…前端github地址:https://github.com/hudingyu/c…技术细节使用 async await 做异步逻辑的处理。使用 async库 来做循环遍历,以及并发请求操作。使用 log4js 来做日志处理使用 cheerio 来做新闻详情页的分析抓取。使用 mongoose 来连接mongoDB 做数据的保存以及操作。目录结构目录结构├── bin // 入口│ ├── article-list.js // 抓取新闻列表逻辑│ ├── content.js // 抓取新闻内容逻辑│ ├── server.js // 服务端程序入口│ └── spider.js // 爬虫程序入口├── config // 配置文件├── dbhelper // 数据库操作方法目录├── middleware // koa2 中间件├── model // mongoDB 集合操作实例├── router // koa2 路由文件├── utils // 工具函数├── package.json 方案分析首先看爬虫程序入口文件,整体逻辑其实很简单,先抓取新闻列表,存入MongoDB数据库,每十分钟抓取一次。新闻列表抓取之后,在数据库查询列表中没有新闻内容的新闻,开始抓取新闻详情,然后更新到数据库。const articleListInit = require(’./article-list’);const articleContentInit = require(’./content’);const logger = require(’../config/log’);const start = async() => { let articleListRes = await articleListInit(); if (!articleListRes) { logger.warn(’news list update failed…’); } else { logger.info(’news list update succeed!’); } let articleContentRes = await articleContentInit(); if (!articleContentRes) { logger.warn(‘article content grab error…’); } else { logger.info(‘article content grab succeed!’); }};if (typeof articleListInit === ‘function’) { start();}setInterval(start, 600000);接着看抓取新闻列表的逻辑,因为可以获取到新闻列表的Ajax接口,所以直接调用接口获取列表信息。但是也有个问题,cnBeta新闻列表的缩略图以及文章里的的图片是有防盗链的,所以你在自己的网站是没法直接使用它的图片的,所以我是直接把cnBeta的图片文件爬下来存到自己的服务器上。/** * 初始化方法 抓取文章列表 * @returns {Promise.<>} /const articleListInit = async() => { logger.info(‘grabbing article list starts…’); const pageUrlList = getPageUrlList(listBaseUrl, totalPage); if (!pageUrlList) { return; } let res = await getArticleList(pageUrlList); return res;}/* * 利用分页接口获取文章列表 * @param pageUrlList * @returns {Promise} /const getArticleList = (pageUrlList) => { return new Promise((resolve, reject) => { async.mapLimit(pageUrlList, 1, (pageUrl, callback) => { getCurPage(pageUrl, callback); }, (err, result) => { if (err) { logger.error(‘get article list error…’); logger.error(err); reject(false); return; } let articleList = _.flatten(result); downloadThumbAndSave(articleList, resolve); }) })};/* * 获取当前页面的文章列表 * @param pageUrl * @param callback * @returns {Promise.<void>} /const getCurPage = async(pageUrl, callback) => { let num = Math.random() * 1000 + 1000; await sleep(num); request(pageUrl, (err, response, body) => { if (err) { logger.info(‘current url went wrong,url address:’ + pageUrl); callback(null, null); return; } else { let responseObj = JSON.parse(body); if (responseObj.result && responseObj.result.list) { let newsList = parseObject(articleModel, responseObj.result.list, { pubTime: ‘inputtime’, author: ‘aid’, commentCount: ‘comments’, }); callback(null, newsList); return; } console.log(“出错了”); callback(null, null); } });};const downloadThumbAndSave = (list, resolve) => { const host = ‘https://static.cnbetacdn.com’; const basepath = ‘./public/data’; if (list.indexOf(null) > -1) { resolve(false); } else { try { async.eachSeries(list, (item, callback) => { let thumb_url = item.thumb.replace(host, ‘’); item.thumb = thumb_url; if (!fs.exists(thumb_url)) { mkDirs(basepath + thumb_url.substring(0, thumb_url.lastIndexOf(’/’)), () => { request .get({ url: host + thumb_url, }) .pipe(fs.createWriteStream(path.join(basepath, thumb_url))) .on(’error’, (err) => { console.log(“pipe error”, err); }); callback(null, null); }); } }, (err, result) => { if (!err) { saveDB(list, resolve); } }); } catch(err) { console.log(err); } }};/* * 将文章列表存入数据库 * @param result * @param callback * @returns {Promise.<void>} /const saveDB = async(result, callback) => { //console.log(result); let flag = await dbHelper.insertCollection(articleDbModel, result).catch(function (err){ logger.error(‘data insert falied’); }); if (!flag) { logger.error(’news list save failed’); } else { logger.info(’list saved!total:’ + result.length); } if (typeof callback === ‘function’) { callback(true); }};再来看抓取新闻内容的逻辑,这里是直接根据新闻的sid得到新闻内容页的html,然后利用cheerio库分析获取我们需要的新闻内容。当然这里也是要把文章中的图片爬下来存入服务器,并且把存入数据库的新闻内容中图片链接替换成自己服务器中的URL。/* * 抓取正文程序入口 * @returns {Promise.<>} /const articleContentInit = async() => { logger.info(‘grabbing article contents starts…’); let uncachedArticleSidList = await getUncachedArticleList(articleDbModel); // console.log(‘未缓存的文章:’+ uncachedArticleSidList.join(’,’)); const res = await batchCrawlArticleContent(uncachedArticleSidList); if (!res) { logger.error(‘grabbing article contents went wrong…’); } return res;};/* * 查询新闻列表获取sid列表 * @param Model * @returns {Promise.<void>} /const getUncachedArticleList = async(Model) => { const selectedArticleList = await dbHelper.queryDocList(Model).catch(function (err){ logger.error(err); }); return selectedArticleList.map(item => item.sid); // return selectedArticleList.map(item => item._doc.sid);};/* * 批量抓取新闻详情内容 * @param list * @returns {Promise} /const batchCrawlArticleContent = (list) => { return new Promise((resolve, reject) => { async.mapLimit(list, 3, (sid, callback) => { getArticleContent(sid, callback); }, (err, result) => { if (err) { logger.error(err); reject(false); return; } resolve(true); }); });};/* * 抓取单篇文章内容 * @param sid * @param callback * @returns {Promise.<void>} /const getArticleContent = async(sid, callback) => { let num = Math.random() * 1000 + 1000; await sleep(num); let url = contentBaseUrl + sid + ‘.htm’; request(url, (err, response, body) => { if (err) { logger.error(‘grabbing article content went wrong,article url:’ + url); callback(null, null); return; } const $ = cheerio.load(body, { decodeEntities: false }); const serverAssetPath = ${serverIp}:${serverPort}/data; let domainReg = new RegExp(‘https://static.cnbetacdn.com’,‘g’); let article = { sid, source: $(’.article-byline span a’).html() || $(’.article-byline span’).html(), summary: $(’.article-summ p’).html(), content: $(’.articleCont’).html().replace(styleReg.reg, styleReg.replace).replace(scriptReg.reg, scriptReg.replace).replace(domainReg, serverAssetPath), }; saveContentToDB(article); let imgList = []; $(’.articleCont img’).each((index, dom) => { imgList.push(dom.attribs.src); }); downloadImgs(imgList); callback(null, null); });};/* * 下载图片 * @param list /const downloadImgs = (list) => { const host = ‘https://static.cnbetacdn.com’; const basepath = ‘./public/data’; if (!list.length) { return; } try { async.eachSeries(list, (item, callback) => { let num = Math.random() * 500 + 500; sleep(num); if (item.indexOf(host) === -1) return; let thumb_url = item.replace(host, ‘’); item.thumb = thumb_url; if (!fs.exists(thumb_url)) { mkDirs(basepath + thumb_url.substring(0, thumb_url.lastIndexOf(’/’)), () => { request .get({ url: host + thumb_url, }) .pipe(fs.createWriteStream(path.join(basepath, thumb_url))) .on(“error”, (err) => { console.log(“pipe error”, err); }); callback(null, null); }); } }); } catch(err) { console.log(err); }};/* * 保存到文章内容到数据库 * @param article */const saveContentToDB = (item) => { let flag = dbHelper.updateCollection(articleDbModel, item); if (flag) { logger.info(‘grabbing article content succeeded:’ + item.sid); }};爬虫部分差不多就是这样,还有一点就自己服务器存储的爬取的图片每天都会有上百张,时间一长,图片占用的存储空间就会特别大,所以需要定时清理一下,有兴趣的可以看看项目里面的clear-expire.js文件。总结其实,虽然这个项目整体并不复杂,但是一套前后端系统搭建起来的过程中,自己的收获还是挺不少的,很多问题的解决需要自己去实践和思考的,对于性能优化考量也是一个重要的方面。下面截图就是我最终完成得m站,界面很清爽,体验上确实比cnBeta官网要好很多。这样是平时看科技新闻也确实方便很多。以上 ...

December 17, 2018 · 4 min · jiezi

Express使用mongodb管理会话储存 connect-mongo模块简介

简介在我的前一篇小文中express-session小书提到了express-session可以更换会话储存.那么这篇文章我们就来讲讲express在进行会话管理的时候如何将会话数据保存在外部数据库中,本文中我们使用mongodb用作会话储存数据库.本文中使用的模块以及版本号一览:模块名称版本号express4.16.4mongodb3.1.8express-session1.15.6connect-mongo2.0.3connect-mongo特性支持Express5支持所有版本的Connect支持Mongoose>=4.1.2+支持原生Mongodb驱动>=2.0.36支持Node.js 4 6 8 10支持Mongodb>=3.0事前分析由于mongodb客户端和服务器可以是多对多的关系,故有如下组合.一个客户端连接多个服务器多个客户端连接一个服务器多个客户端连接多个服务器一个客户端连接一个服务器本文主要讲解一个客户端连接一个服务器.这种情况下,一般服务器监听一个端口,而我们希望可以共享同一个mongodb驱动的实例.但是在一般情况下,我们的mongodb数据库不可能只用于会话管理任务,所以本文复用同一个连接(端口).只要复用同一个连接可以完成,那么使用单独的驱动实例来用作会话管理也就不在话下了.起步首先我们引入所有的模块:const Express = require(’express’)(), MongoClient = require(‘mongodb’).MongoClient, ExpressSession = require(’express-session’), MongoStore= require(‘connect-mongo’)(ExpressSession);看起来connect-mongo需要将express-session包装一下,这步是固定的.接下来我们定义几个常量用于连接数据库:const UrlOfDb = ‘mongodb://localhost:27017’, NameOfDb = ‘demo’, Client = new MongoClient(UrlOfDb);// 创建mongodb客户端客户端连接数据库:Client.connect((error) => { if (error) { throw error; } });使用一个数据表,并且查询几条数据: const DataBase = Client.db(NameOfDb), Collection = DataBase.collection(‘sessions’); Collection.find({}).toArray((error, result) => { if (error) { throw error; } for (const element of result) { console.log(element); } });到目前为止我们没有进行session管理,你可以替换本例中的数据表名称用于测试一下运行是否正常.完整代码:const Express = require(’express’)(), MongoClient = require(‘mongodb’).MongoClient,// 获取数据库驱动 ExpressSession = require(’express-session’),// 获取session中间件 MongoStore= require(‘connect-mongo’)(ExpressSession);// 获取session储存插件 const UrlOfDb = ‘mongodb://localhost:27017’, NameOfDb = ‘demo’, Client = new MongoClient(UrlOfDb);// 创建客户端 Client.connect((error) => { if (error) { throw error; } const DataBase = Client.db(NameOfDb),// 获取数据库 Collection = DataBase.collection(‘sessions’); // 获取数据表 // 查询数据表 Collection.find({}).toArray((error, result) => { if (error) { throw error; } for (const element of result) { console.log(element); } }); });现在我们来使用express-session中间件,并且替换掉默认的储存:// +++++const DataBase = Client.db(NameOfDb),// 获取数据库 Collection = DataBase.collection(‘sessions’),// 获取数据表 MongoStoreInstance = new MongoStore({ // 创建一个储存实例,传入db参数对于的数据库对象 db:DataBase });// 使用中间件Express.use(ExpressSession({ secret: ‘hello mongo’,// cookie签名 cookie: {maxAge: 1800000}, rolling:true, saveUninitialized:true, resave: false, store:MongoStoreInstance // 替换掉默认的储存}));// +++++++注意:connect-mongo会在该database下创建一个sessions的数据表(没有这个数据表的情况下).添加一个路由用于完成简单的验证,用于测试是否正常工作:Express.get(’/’,(request,response)=>{ if(request.session.name){ response.send(欢迎回来${request.session.name}); return ; } // 使用查询字符串当作保存的信息 request.session.name = request.query.name; request.session.pwd = request.query.pwd; response.send(欢迎登录${request.session.name});});// 启动服务器Express.listen(8888, function () { console.log(‘server is listening 8888 port!’);});完整代码:const Express = require(’express’)(), MongoClient = require(‘mongodb’).MongoClient, ExpressSession = require(’express-session’), MongoStore= require(‘connect-mongo’)(ExpressSession);const UrlOfDb = ‘mongodb://localhost:27017’, NameOfDb = ‘demo’, Client = new MongoClient(UrlOfDb);function destroyDb(Client) { return destroyDb = function () { const info = ‘Client has been closed!’; Client.close(); Client = null; console.log(info); return info; }}Client.connect((error) => { if (error) { throw error; } const DataBase = Client.db(NameOfDb), Collection = DataBase.collection(‘sessions’), MongoStoreInstance = new MongoStore({ db:DataBase }); Express.use(ExpressSession({ secret: ‘hello mongo’, cookie: {maxAge: 1800000}, rolling:true, saveUninitialized:true, resave: false, store:MongoStoreInstance })); // 使用闭包将关闭数据库挂载到全局 destroyDb(Client); // 展示复用一个连接 Collection.find({}).toArray((error, result) => { if (error) { throw error; } for (const element of result) { console.log(element); } }); Express.get(’/’,(request,response)=>{ if(request.session.name){ response.send(欢迎回来${request.session.name}); return ; } request.session.name = request.query.name; request.session.pwd = request.query.pwd; response.send(欢迎登录${request.session.name}); }); Express.get(’/closedatabase’, (request, respnose) => { respnose.send(destroyDb()); }); Express.listen(8888, function () { console.log(‘server is listening 8888 port!’); });});注意:我没有删除数据库表的常规输出,在这个例子启动的时候,你会发现他们共用了同一个连接,启动的时候会先输出数据表中的内容.测试在浏览器中输入如下内容:http://localhost:8888/?name=ascll&pwd=123456浏览器输出:欢迎登录ascll直接再次访问该页面:http://localhost:8888/浏览器输出:欢迎回来ascll此时在数据库中手动查询后,或者重启本项目,你会在控制台中发现上次留下的session记录:{ _id: ‘qbP36wE0nJkvtyNqx_6Amoesjjcsr-sD’, expires: 2018-12-14T08:27:19.809Z, session: ‘{“cookie”:{“originalMaxAge”:1800000,“expires”:“2018-12-14T08:20:21.519Z”,“httpOnly”:true,“path”:"/"},“name”:“ascll”,“pwd”:“123456”}’ }使用总结引入connect-mongo和express-session然后调用connect-mongo将express-sessino传入获取上一步返回的类,然后使用express-session中间件的时候对于store选传入这个类的实例对象api创建Express 4.x, 5.0 and Connect 3.x:const session = require(’express-session’);const MongoStore = require(‘connect-mongo’)(session); app.use(session({ secret: ‘foo’, store: new MongoStore(options)}));Express 2.x, 3.x and Connect 1.x, 2.x:const MongoStore = require(‘connect-mongo’)(express); app.use(express.session({ secret: ‘foo’, store: new MongoStore(options)}));连接到MongoDb使用mongooseconst mongoose = require(‘mongoose’); // 基本使用mongoose.connect(connectionOptions); app.use(session({ store: new MongoStore({ mongooseConnection: mongoose.connection })})); // 建议使用方式,这样可以复用连接const connection = mongoose.createConnection(connectionOptions); app.use(session({ store: new MongoStore({ mongooseConnection: connection })}));使用Mongo原生Node驱动这种情况下你需要将一个mongodb驱动的一个数据库实例传递给connect-mongo.如果数据库没有打开connect-mongo会自动帮你连接./* 这里有很多种方式来获取一个数据库实例,具体可以参考官网文档.*/app.use(session({ store: new MongoStore({ db: dbInstance }) // 别忘了MongoStore是connect-mongo传入express-session后返回的一个函数}));// 或者也可以使用Promise版本app.use(session({ store: new MongoStore({ dbPromise: dbInstancePromise })}));通过连接字符串创建一个连接// Basic usageapp.use(session({ store: new MongoStore({ url: ‘mongodb://localhost/test-app’ })})); // Advanced usageapp.use(session({ store: new MongoStore({ url: ‘mongodb://user12345:foobar@localhost/test-app?authSource=admins&w=1’, mongoOptions: advancedOptions // See below for details })}));事件一个MongoStore实例有如下的事件:事件名称描述回调参数createsession创建后触发sessionIdtouchsession被获取但是未修改sessionIdupdatesession被更新sessionIdsetsession创建后或者更新后(为了兼容)sessionIddestroysession被销毁后sessionId使用我们之前的例子中添加如下的代码:// +++MongoStoreInstance.on(‘create’,(sessionId)=>{ console.log(‘create’,sessionId);});MongoStoreInstance.on(’touch’,(sessionId)=>{ console.log(‘create’, sessionId);});MongoStoreInstance.on(‘update’,(sessionId)=>{ console.log(‘update’, sessionId);});MongoStoreInstance.on(‘set’,(sessionId)=>{ console.log(‘set’, sessionId);});MongoStoreInstance.on(‘destroy’,(sessionId)=>{ console.log(‘destroy’, sessionId);});// +++清空cookie后再次运行服务器,多执行几个操作你就可以看到session的创建以及修改等操作.session过期处理基本处理方式connect-mongo只会使用配置了过期时间的cookie,如果没有设置则会创建一个新的cookie并且使用tll选项来指定过期时间:app.use(session({ store: new MongoStore({ url: ‘mongodb://localhost/test-app’, ttl: 14 * 24 * 60 * 60 // 默认过期时间为14天 })}));注意:用户的每次访问都会刷新过期时间.删除过期session默认情况下connect-mongo使用MongoDB’s TTL collection特性(2.2+)用于自动的移出过期的session.但是你可以修改这种行为.connect-mongo会在开始的时候创建一个TTl索引,前提是你的Mongo db版本在(2.2+)且有权限执行这一操作.app.use(session({ store: new MongoStore({ url: ‘mongodb://localhost/test-app’, autoRemove: ’native’ // Default })}));注意:这种默认的行为不适用于高并发的情况,这种情况下你需要禁用默认模式,然后自行定义TTl索引.使用兼容模式如果你使用了Mongodb的老版本或者不希望创建TTL索引,你可以指定一个间隔时间让connect-mongo来删除这些过期的session.app.use(session({ store: new MongoStore({ url: ‘mongodb://localhost/test-app’, autoRemove: ‘interval’, autoRemoveInterval: 10 // 单位分钟 })}));禁用过期session删除app.use(session({ store: new MongoStore({ url: ‘mongodb://localhost/test-app’, autoRemove: ‘disabled’ })}));session懒更新如果你使用的express-session版本>=1.10,然后不希望用户每次浏览页面的时候或刷新页面的时候都要重新保存,你可以限制一段时间内更新session.app.use(express.session({ secret: ‘keyboard cat’, saveUninitialized: false, // 如果不保存则不会创建session resave: false, // 如果未修改则不会保存 store: new MongoStore({ url: ‘mongodb://localhost/test-app’, touchAfter: 24 * 3600 // 指定触发间隔时间 单位秒 })}));通过这样设置session只会在24小时内触发1次无论用户浏览多少次页面或者刷新多少次.修改session除外.其他选项collection 指定缓存数据表的名字默认sessionsfallbackMemory 回退处理默认使用MemoryStore进行存储stringify 默认是true,如果为true则序列化和反序列化使用原生的JSON.xxx处理.serialize 自定义序列化函数unserialize 自定义反序列化函数transformId 将sessionId转为你想要的任何键然后进行储存暗坑也不算是暗坑吧,一用有两点:Mongodb客户端正常关闭后connect-mongo会报错,虽然会被Express拦截但是这个模块没有提供error事件.Express中间件必须同步挂载?在我的例子中尝试异步加载express-session中间件,但是失败了中间件没有效果.connect-mongo模块npm地址https://www.npmjs.com/package… ...

December 14, 2018 · 3 min · jiezi

JSON数据从MongoDB迁移到MaxCompute最佳实践

摘要: 本文为您介绍如何利用DataWorks数据集成直接从MongoDB提取JSON字段到MaxCompute。数据及账号准备首先您需要将数据上传至您的MongoDB数据库。本例中使用阿里云的云数据库 MongoDB 版,网络类型为VPC(需申请公网地址,否则无法与DataWorks默认资源组互通),测试数据如下。{ “store”: { “book”: [ { “category”: “reference”, “author”: “Nigel Rees”, “title”: “Sayings of the Century”, “price”: 8.95 }, { “category”: “fiction”, “author”: “Evelyn Waugh”, “title”: “Sword of Honour”, “price”: 12.99 }, { “category”: “fiction”, “author”: “J. R. R. Tolkien”, “title”: “The Lord of the Rings”, “isbn”: “0-395-19395-8”, “price”: 22.99 } ], “bicycle”: { “color”: “red”, “price”: 19.95 } }, “expensive”: 10}登录MongoDB的DMS控制台,本例中使用的数据库为 admin,集合为 userlog,您可以在查询窗口使用db.userlog.find().limit(10)命令查看已上传好的数据,如下图所示。 此外,需提前在数据库内新建用户,用于DataWorks添加数据源。本例中使用命令db.createUser({user:“bookuser”,pwd:“123456”,roles:[“root”]}),新建用户名为 bookuser,密码为 123456,权限为root。使用DataWorks提取数据到MaxCompute新增MongoDB数据源进入DataWorks数据集成控制台,新增MongoDB类型数据源。 具体参数如下所示,测试数据源连通性通过即可点击完成。由于本文中MongoDB处于VPC环境下,因此 数据源类型需选择 有公网IP。 访问地址及端口号可通过在MongoDB管理控制台点击实例名称获取,如下图所示。 新建数据同步任务在DataWorks上新建数据同步类型节点。 新建的同时,在DataWorks新建一个建表任务,用于存放JSON数据,本例中新建表名为mqdata。 表参数可以通过图形化界面完成。本例中mqdata表仅有一列,类型为string,列名为MQ data。 完成上述新建后,您可以在图形化界面进行数据同步任务参数的初步配置,如下图所示。选择目标数据源名称为odps_first,选择目标表为刚建立的mqdata。数据来源类型为MongoDB,选择我们刚创建的数据源mongodb_userlog。完成上述配置后, 点击转换为脚本,跳转到脚本模式。 脚本模式代码示例如下。{ “type”: “job”, “steps”: [ { “stepType”: “mongodb”, “parameter”: { “datasource”: “mongodb_userlog”, //数据源名称 “column”: [ { “name”: “store.bicycle.color”, //JSON字段路径,本例中提取color值 “type”: “document.document.string” //本栏目的字段数需和name一致。假如您选取的JSON字段为一级字段,如本例中的expensive,则直接填写string即可。 } ], “collectionName //集合名称”: “userlog” }, “name”: “Reader”, “category”: “reader” }, { “stepType”: “odps”, “parameter”: { “partition”: “”, “isCompress”: false, “truncate”: true, “datasource”: “odps_first”, “column”: [ //MaxCompute表列名 “mqdata” ], “emptyAsNull”: false, “table”: “mqdata” }, “name”: “Writer”, “category”: “writer” } ], “version”: “2.0”, “order”: { “hops”: [ { “from”: “Reader”, “to”: “Writer” } ] }, “setting”: { “errorLimit”: { “record”: "" }, “speed”: { “concurrent”: 2, “throttle”: false, “dmu”: 1 } }}完成上述配置后,点击运行接即可。运行成功日志示例如下所示。 结果验证在您的业务流程中新建一个ODPS SQL节点。 您可以输入 SELECT * from mqdata;语句,查看当前mqdata表中数据。当然这一步您也可以直接在MaxCompute客户端中输入命令运行。 本文作者:付帅阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

November 28, 2018 · 1 min · jiezi

服务器小白的我,是如何成功将 node+mongodb 项目部署在服务器上并进行性能优化的

前言本文讲解的是:做为前端开发人员,对服务器的了解还是小白的我,是如何一步步将 node+mongodb 项目部署在阿里云 centos 7.3 的服务器上,并进行性能优化,达到页面 1 秒内看到 loading ,3 秒内看到首屏内容的。搭建的项目是采用了主流的前后端分离思想的,这里只讲 服务器环境搭建与性能优化。效果请看 http://乐趣区.cn/main.html1. 流程开发好前端与后端程序。购买服务器与域名服务器上安装所需环境(本项目是 node 和 mongodb )服务器上开放端口与设置规则用 nginx、apache 或者tomcat 来提供HTTP服务或者设置代理上传项目代码 或者 用码云或者 gihub 来拉取你的代码到服务器上启动 express 服务器优化页面加载2. 内容细节2.1 开发好前端与后端程序开发好前端与后端程序,这个没什么好说的,就是开发!开发!开发!再开发!2.2 购买服务器与域名本人一直觉得程序员应该有一个自己的个人网站,拥有自己的域名与服务器。学知识或者测试项目的时候可以用来测试。阿里云有个专供学生的云翼计划 阿里云学生套餐,入门级的云服务器原价1400多,学生认证后只要114一年,非常划算。还是学生的,直接购买;不是学生了,有弟弟、妹妹的,可以用他们的大学生身份,购买,非常便宜实用(我购买的就是学生优惠套餐)。当然阿里云服务器在每年双 11 时都有很大优惠,也很便宜,选什么配置与价格得看自己的用处。服务器预装环境可以选择 CentOS 或者 windows server,,为了体验和学习 linux 系统,我选择了CentOS。再次是购买域名 阿里域名购买,本人也是在阿里云购买的。域名是分 国际域名与国内域名的,国际域名是不用备案的,但是国内的域名是必须 ICP备案的 阿里云ICP代备案管理系统,不然不能用,如果是国内域名,如何备案域名,请自己上网查找教程。当然如果你的网站只用来自己用的话,可以不用买域名,因为可以通过服务器的公网 ip 来访问网站内容的。如果购买了域名了,还要设置域名映射到相应的公网 ip ,不然也不能用。3. 服务器上安装所需环境(本项目是 node 和 mongodb )3.1 登录服务器因本人用的是 MacBook Pro ,所以直接打开 mac 终端,通过下面的命令行连接到服务器。root 是阿里云服务器默认的账号名,连接时候会叫你输入密码,输入你购买时设置的或者后来设置的密码。ssh root@47.106.20.666 //你的服务器公网 ip,比如 47.106.20.666如图:window 系统的,请用 Putty 或 Xshell 来登录,可以参考一下这篇文章 把 Node.js 项目部署到阿里云服务器(CentOs)一般在新服务器创建后,建议先升级一下 CentOS:yum -y update常用的 Linux 命令cd 进入目录cd .. 返回上一个目录ls -a 查看当前目录mkdir abc 创建abc文件夹mv 移动或重命名rm 删除一个文件或者目录3.2 安装 node升级常用库文件, 安装 node.js 需要通过 g++ 进行编译。yum -y install gcc gcc-c++ autoconf跳转到目录:/usr/local/src,这个文件夹通常用来存放软件源代码:cd /usr/local/src下载 node.js 源码,也可以使用 scp 命令直接上传,因为下载实在太慢了:下载地址:Downloads,请下载最新的相应版本的源码进行下载,本人下载了 v10.13.0 版本的。https://nodejs.org/dist/v10.13.0/node-v10.13.0.tar.gz下载完成后解压:tar -xzvf node-v10.13.0.tar.gz进入解压后的文件夹:cd node-v10.13.0执行配置脚本来进行预编译处理:./configure编译源代码,这个步骤花的时间会很长,大概需要 5 到 10 分钟:make编译完成后,执行安装命令,使之在系统范围内可用:make install安装 express 推荐 global 安装npm -g install express建立超级链接, 不然 sudo node 时会报 “command not found"sudo ln -s /usr/local/bin/node /usr/bin/nodesudo ln -s /usr/local/lib/node /usr/lib/nodesudo ln -s /usr/local/bin/npm /usr/bin/npmsudo ln -s /usr/local/bin/node-waf /usr/bin/node-waf通过指令查看 node 及 npm 版本:node -vnpm -vnode.js 到这里就基本安装完成了。3.2 安装 mongodb下载地址:mongodb下载时,请选对相应的环境与版本,因为本人的服务器是 CentOS ,其实本质就是 linux 系统,所以选择了如下图环境与目前最新的版本。mongodb :软件安装位置:/usr/local/mongodb数据存放位置:/home/mongodb/data数据备份位置:/home/mongodb/bak日志存放位置:/home/mongodb/logs下载安装包> cd /usr/local> wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-4.0.4.tgz解压安装包,重命名文件夹为 mongodbtar zxvf mongodb-linux-x86_64-4.0.4.tgzmv mongodb-linux-x86_64-4.0.4 mongodb在 var 文件夹里建立 mongodb 文件夹,并分别建立文件夹 data 用于存放数据,logs 用于存放日志mkdir /var/mongodbmkdir /var/mongodb/datamkdir /var/mongodb/logs打开 rc.local 文件,添加 CentOS 开机启动项:vim /etc/rc.d/rc.local// 不懂 vim 操作的请自行查看相应的文档教程,比如: vim 模式下,要 按了 i 才能插入内容,输入完之后,要按 shift 加 :wq 才能保存退出。将 mongodb 启动命令追加到本文件中,让 mongodb 开机自启动:/usr/local/mongodb/bin/mongod –dbpath=/var/mongodb/data –logpath /var/mongodb/logs/log.log -fork启动 mongodb/usr/local/mongodb/bin/mongod –dbpath=/var/mongodb/data –logpath /var/mongodb/logs/log.log -fork看到如下信息说明已经安装完成并成功启动:forked process: 18394all output going to: /var/mongodb/logs/log.logmongodb 默认的端口号是 27017。如果你数据库的连接要账号和密码的,要创建数据库管理员,不然直接连接即可。在 mongo shell 中创建管理员及数据库。切换到 admin 数据库,创建超级管理员帐号use admindb.createUser({ user: “用户名”, pwd:“登陆密码”, roles:[{ role: “userAdminAnyDatabase”, db: “admin” }] })切换到要使用的数据库,如 taodb 数据库,创建这个数据库的管理员帐号use taodbdb.createUser({ user: “用户名”, pwd:“登陆密码”, roles:[ { role: “readWrite”, db: “taodb” }] //读写权限 })重复按两下 control+c ,退出 mongo shell。到这里 mongodb 基本已经安装设置完成了。备份与恢复 请看这篇文章:MongoDB 备份(mongodump)与恢复(mongorestore)安装 node 与 mongodb 也可以参考这篇文章:CentOs搭建NodeJs服务器—Mongodb安装3.3 服务器上开放端口与设置安全组规则如果你只放静态的网页,可以参考这个篇文章 通过云虚拟主机控制台设置默认首页但是我们是要部署后台程序的,所以要看以下的内容:安全组规则是什么鬼授权安全组规则可以允许或者禁止与安全组相关联的 ECS 实例的公网和内网的入方向和出方向的访问。 阿里云安全组应用案例文档80 端口是为 HTTP(HyperText Transport Protocol) 即超文本传输协议开放的,浏览器 HTTP 访问 IP 或域名的 80 端口时,可以省略 80 端口号如果我们没有开放相应的端口,比如我们的服务要用到 3000 ,就要开放 3000 的端口,不然是访问不了的;其他端口同理。端口都配置对了,以为能用公网 IP 进行访问了么 ? 小兄弟你太天真了 …还有 防火墙 这一关呢,如果防火墙没有关闭或者相关的端口没有开放,也是不能用公网 IP 进行访问网站内容的。和安全组端口同理,比如我们的服务要用到的是 3000 端口,就要开放 3000 的端口,不然是访问不了的;其他端口同理。出于安全考虑还是把防火墙开上,只开放相应的端口最好。怎么开放相应的端口 ? 看下面两篇文章足矣,这里就不展开了。1. 将nodejs项目部署到阿里云ESC服务器,linux系统配置80端口,实现公网IP访问2. centos出现“FirewallD is not running”怎么办3.4 用 nginx、apache 或者 tomcat 来提供 HTTP 服务或者设置代理我是用了 nginx 的,所以这里只介绍 nginx 。安装 nginx 请看这两篇文章:1. Centos7安装Nginx实战2. 阿里云Centos7安装Nginx服务器实现反向代理开启 ngnx 代理进入到目录位置cd /usr/local/nginx在 nginx 目录下有一个 sbin 目录,sbin 目录下有一个 nginx 可执行程序。./nginx关闭 nginx./nginx -s stop重启./nginx -s reload基本的使用就是这样子了。如下给出我的 nginx 代理的设置:我的两个项目是放在 /home/blog/blog-react/build/; 和 /home/blog/blog-react-admin/dist/; 下的,如果你们的路径不是这个,请修改成你们的路径。#user nobody;worker_processes 1;#error_log logs/error.log;#error_log logs/error.log notice;#error_log logs/error.log info;#pid logs/nginx.pid;events { worker_connections 1024;}http { include mime.types; default_type application/octet-stream; #log_format main ‘$remote_addr - $remote_user [$time_local] “$request” ’ # ‘$status $body_bytes_sent “$http_referer” ’ # ‘"$http_user_agent” “$http_x_forwarded_for”’; #access_log logs/access.log main; sendfile on; #tcp_nopush on; #keepalive_timeout 0; keepalive_timeout 65; #gzip on; # 如果port_in_redirect为off时,那么始终按照默认的80端口;如果该指令打开,那么将会返回当前正在监听的端口。 port_in_redirect off; # 前台展示打开的服务代理 server { listen 80; server_name localhost; #charset koi8-r; #access_log logs/host.access.log main; #root /home/blog; location / { root /home/blog/blog-react/build/; index index.html; try_files $uri $uri/ @router; autoindex on; } location @router{ rewrite ^.$ /index.html last; } location /api/ { proxy_set_header X-Real-IP $remote_addr; proxy_pass http://47.106.136.114:3000/ ; } gzip on; gzip_buffers 32 4k; gzip_comp_level 6; gzip_min_length 200; gzip_types text/css text/xml application/javascript; gzip_vary on; #error_page 404 /404.html; # redirect server error pages to the static page /50x.html # error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } } # HTTPS server # 管理后台打开的服务代理 server { listen 4444; server_name localhost; # charset koi8-r; # ssl_certificate cert.pem; # ssl_certificate_key cert.key; # ssl_session_cache shared:SSL:1m; # ssl_session_timeout 5m; # ssl_ciphers HIGH:!aNULL:!MD5; # ssl_prefer_server_ciphers on; location / { root /home/blog/blog-react-admin/dist/; index index.html index.htm; try_files $uri $uri/ @router; autoindex on; } location @router{ rewrite ^.$ /index.html last; } location /api/ { proxy_set_header X-Real-IP $remote_addr; proxy_pass http://47.106.136.114:3000/ ; } gzip on; gzip_buffers 32 4k; gzip_comp_level 6; gzip_min_length 200; gzip_types text/css text/xml application/javascript; gzip_vary on; error_page 500 502 503 504 /50x.html; }}我是开了两个代理的:前台展示打开的服务代理和管理后台打开的服务代理,这个项目是分开端口访问的。比如:我的公网 ip 是 47.106.20.666,那么可以通过 http://47.106.20.666 即可访问前台展示,http://47.106.20.666:4444 即可访问管理后台的登录界面。至于为什么要写这样的配置:try_files $uri $uri/ @router;location @router{ rewrite ^.*$ /index.html last; }因为进入到文章详情时或者前端路由变化了,再刷新浏览器,发现浏览器出现 404 。刷新页面时访问的资源在服务端找不到,因为 react-router 设置的路径不是真实存在的路径。所以那样设置是为了可以刷新还可以打到对应的路径的。刷新出现 404 问题,可以看下这篇文章 react,vue等部署单页面项目时,访问刷新出现404问题3.5 上传项目代码,或者用码云、 gihub 来拉取你的代码到服务器上我是创建了码云的账号来管理项目代码的,因为码云上可以创建免费的私有仓库,我在本地把码上传到 Gitee.com 上,再进入服务器用 git 把代码拉取下来就可以了,非常方便。具体请看:码云(Gitee.com)帮助文档 V1.2git 的安装请看: CentOS 7.4 系统安装 git如果不想用 git 进行代码管理,请用其他可以连接服务器上传文件的软件,比如 FileZilla。3.6 启动 express 服务启动 express 服务,我用了 pm2, 可以永久运行在服务器上,且不会一报错 express 服务就挂了,而且运行中还可以进行其他操作。安装:npm install -g pm2切换当前工作目录到 express 应用文件夹下,执行 pm2 命令启动 express 服务:pm2 start ./bin/www比如我操作项目时的基本操作:cd /home/blog/blog-nodepm2 start ./bin/www // 开启pm2 stop ./bin/www // 关闭pm2 list //查看所用已启动项目:3.7 页面加载优化再看刚刚的 nginx 的一些配置:server { gzip on; gzip_buffers 32 4k; gzip_comp_level 6; gzip_min_length 200; gzip_types text/css text/xml application/javascript; gzip_vary on; }这个就是利用 ngonx 开启 gzip,亲测开启之后,压缩了接近 2/3 的文件大小,本来要 1M 多的文件,开启压缩之后,变成了 300k 左右。还有其他的优化请看这篇文章 React 16 加载性能优化指南,写的很不错,我的一些优化都是参考了这个篇文章的。做完一系列的优化处理之后,在网络正常的情况下,页面首屏渲染由本来是接近 5 秒,变成了 3 秒内,首屏渲染之前的 loading 在 1 秒内可见了。4. 项目地址本人的个人博客项目地址:前台展示: https://github.com/乐趣区/blog-react管理后台:https://github.com/乐趣区/blog-react-admin后端:https://github.com/乐趣区/blog-nodeblog:https://github.com/乐趣区/blog本博客系统的系列文章:react + node + express + ant + mongodb 的简洁兼时尚的博客网站react + Ant Design + 支持 markdown 的 blog-react 项目文档说明基于 node + express + mongodb 的 blog-node 项目文档说明服务器小白的我,是如何将node+mongodb项目部署在服务器上并进行性能优化的5. 最后对 全栈开发 有兴趣的朋友,可以扫下方二维码,关注我的公众号,我会不定期更新有价值的内容。微信公众号:乐趣区分享 前端、后端开发 等相关的技术文章,热点资源,全栈程序员的成长之路。关注公众号并回复 福利 便免费送你视频资源,绝对干货。福利详情请点击: 免费资源分享–Python、Java、Linux、Go、node、vue、react、javaScript ...

November 26, 2018 · 4 min · jiezi

后端小白的我,是如何成功搭建 express+mongodb 的简洁博客网站后端的

前言blog-node 是采用了主流的前后端分离思想的,主里只讲 后端。blog-node 项目是 node + express + mongodb 的进行开发的,项目已经开源,项目地址在 github 上。效果请看 http://乐趣区.cn/main.html1. 后端1.1 已经实现功能[x] 登录[x] 文章管理[x] 标签管理[x] 评论[x] 留言管理[x] 用户管理[x] 友情链接管理[x] 时间轴管理[x] 身份验证1.2 待实现功能[ ] 点赞、留言和评论 的通知管理[ ] 个人中心(用来设置博主的各种信息)[ ] 工作台( 接入百度统计接口,查看网站浏览量和用户访问等数据 )2. 技术nodecookie-parser : “~1.4.3"crypto : “^1.0.1"express: “~4.16.0"express-session : “^1.15.6”,http-errors : “~1.6.2”,mongodb : “^3.1.8”,mongoose : “^5.3.7”,mongoose-auto-increment : “^5.0.1”,yargs : “^12.0.2"3. 主文件 app.js// modulesconst createError = require(‘http-errors’);const express = require(’express’);const path = require(‘path’);const cookieParser = require(‘cookie-parser’);const logger = require(‘morgan’);const session = require(’express-session’);// import 等语法要用到 babel 支持require(‘babel-register’);const app = express();// view engine setupapp.set(‘views’, path.join(__dirname, ‘views’));app.set(‘view engine’, ’ejs’);app.use(logger(‘dev’));app.use(express.json());app.use(express.urlencoded({ extended: false }));app.use(express.static(path.join(__dirname, ‘public’)));app.use(cookieParser(‘blog_node_cookie’));app.use( session({ secret: ‘blog_node_cookie’, name: ‘session_id’, //# 在浏览器中生成cookie的名称key,默认是connect.sid resave: true, saveUninitialized: true, cookie: { maxAge: 60 * 1000 * 30, httpOnly: true }, //过期时间 }),);const mongodb = require(’./core/mongodb’);// data servermongodb.connect();//将路由文件引入const route = require(’./routes/index’);//初始化所有路由route(app);// catch 404 and forward to error handlerapp.use(function(req, res, next) { next(createError(404));});// error handlerapp.use(function(err, req, res, next) { // set locals, only providing error in development res.locals.message = err.message; res.locals.error = req.app.get(’env’) === ‘development’ ? err : {}; // render the error page res.status(err.status || 500); res.render(’error’);});module.exports = app;4. 数据库 core/mongodb.js/** * Mongoose module. * @file 数据库模块 * @module core/mongoose * @author 乐趣区 <https://github.com/乐趣区> /const consola = require(‘consola’)const CONFIG = require(’../app.config.js’)const mongoose = require(‘mongoose’)const autoIncrement = require(‘mongoose-auto-increment’)// remove DeprecationWarningmongoose.set(‘useFindAndModify’, false)// mongoose Promisemongoose.Promise = global.Promise// mongooseexports.mongoose = mongoose// connectexports.connect = () => { // 连接数据库 mongoose.connect(CONFIG.MONGODB.uri, { useCreateIndex: true, useNewUrlParser: true, promiseLibrary: global.Promise }) // 连接错误 mongoose.connection.on(’error’, error => { consola.warn(‘数据库连接失败!’, error) }) // 连接成功 mongoose.connection.once(‘open’, () => { consola.ready(‘数据库连接成功!’) }) // 自增 ID 初始化 autoIncrement.initialize(mongoose.connection) // 返回实例 return mongoose}5. 数据模型 Model这里只介绍 用户、文章和评论 的模型。5.1 用户用户的字段都有设置类型 type,大多都设置了默认值 default ,邮箱设置了验证规则 validate,密码保存用了 crypto 来加密。用了中间件自增 ID 插件 mongoose-auto-increment。/* * User model module. * @file 权限和用户数据模型 * @module model/user * @author 乐趣区 <https://github.com/乐趣区> /const crypto = require(‘crypto’);const { argv } = require(‘yargs’);const { mongoose } = require(’../core/mongodb.js’);const autoIncrement = require(‘mongoose-auto-increment’);const adminSchema = new mongoose.Schema({ // 名字 name: { type: String, required: true, default: ’’ }, // 用户类型 0:博主 1:其他用户 type: { type: Number, default: 1 }, // 手机 phone: { type: String, default: ’’ }, //封面 img_url: { type: String, default: ’’ }, // 邮箱 email: { type: String, required: true, validate: /\w[-\w.+]@([A-Za-z0-9][-A-Za-z0-9]+.)+[A-Za-z]{2,14}/ }, // 个人介绍 introduce: { type: String, default: ’’ }, // 头像 avatar: { type: String, default: ‘user’ }, // 密码 password: { type: String, required: true, default: crypto .createHash(‘md5’) .update(argv.auth_default_password || ‘root’) .digest(‘hex’), }, // 创建日期 create_time: { type: Date, default: Date.now }, // 最后修改日期 update_time: { type: Date, default: Date.now },});// 自增 ID 插件配置adminSchema.plugin(autoIncrement.plugin, { model: ‘User’, field: ‘id’, startAt: 1, incrementBy: 1,});module.exports = mongoose.model(‘User’, adminSchema);5.2 文章文章是分类型的:文章类型 => 1: 普通文章,2: 简历,3: 管理员介绍而且简历和管理员介绍的文章只能是各自一篇(因为前台展示那里有个导航 关于我 ,就是请求管理员介绍这篇文章的,简历也是打算这样子用的),普通文章可以是无数篇。点赞的用户 like_users 那里应该只保存用户 id 的,这个后面修改一下。/** * Article model module. * @file 文章数据模型 * @module model/article * @author 乐趣区 <https://github.com/乐趣区> /const { mongoose } = require(’../core/mongodb.js’);const autoIncrement = require(‘mongoose-auto-increment’);// 文章模型const articleSchema = new mongoose.Schema({ // 文章标题 title: { type: String, required: true, validate: /\S+/ }, // 文章关键字(SEO) keyword: [{ type: String, default: ’’ }], // 作者 author: { type: String, required: true, validate: /\S+/ }, // 文章描述 desc: { type: String, default: ’’ }, // 文章内容 content: { type: String, required: true, validate: /\S+/ }, // 字数 numbers: { type: String, default: 0 }, // 封面图 img_url: { type: String, default: ‘https://upload-images.jianshu.io/upload_images/12890819-80fa7517ab3f2783.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240' }, // 文章类型 => 1: 普通文章,2: 简历,3: 管理员介绍 type: { type: Number, default: 1 }, // 文章发布状态 => 0 草稿,1 已发布 state: { type: Number, default: 1 }, // 文章转载状态 => 0 原创,1 转载,2 混合 origin: { type: Number, default: 0 }, // 文章标签 tags: [{ type: mongoose.Schema.Types.ObjectId, ref: ‘Tag’, required: true }], comments: [{ type: mongoose.Schema.Types.ObjectId, ref: ‘Comment’, required: true }], // 文章分类 category: [{ type: mongoose.Schema.Types.ObjectId, ref: ‘Category’, required: true }], // 点赞的用户 like_users: [ { // 用户id id: { type: mongoose.Schema.Types.ObjectId }, // 名字 name: { type: String, required: true, default: ’’ }, // 用户类型 0:博主 1:其他用户 type: { type: Number, default: 1 }, // 个人介绍 introduce: { type: String, default: ’’ }, // 头像 avatar: { type: String, default: ‘user’ }, // 创建日期 create_time: { type: Date, default: Date.now }, }, ], // 其他元信息 meta: { views: { type: Number, default: 0 }, likes: { type: Number, default: 0 }, comments: { type: Number, default: 0 }, }, // 创建日期 create_time: { type: Date, default: Date.now }, // 最后修改日期 update_time: { type: Date, default: Date.now },});// 自增 ID 插件配置articleSchema.plugin(autoIncrement.plugin, { model: ‘Article’, field: ‘id’, startAt: 1, incrementBy: 1,});// 文章模型module.exports = mongoose.model(‘Article’, articleSchema);5.3 评论评论功能是实现了简单的三级评论的,第三者的评论(就是别人对一级评论进行再评论)放在 other_comments 里面。/* * Comment model module. * @file 评论数据模型 * @module model/comment * @author 乐趣区 <https://github.com/乐趣区> */const { mongoose } = require(’../core/mongodb.js’);const autoIncrement = require(‘mongoose-auto-increment’);// 评论模型const commentSchema = new mongoose.Schema({ // 评论所在的文章 id article_id: { type: mongoose.Schema.Types.ObjectId, required: true }, // content content: { type: String, required: true, validate: /\S+/ }, // 是否置顶 is_top: { type: Boolean, default: false }, // 被赞数 likes: { type: Number, default: 0 }, user_id: { type: mongoose.Schema.Types.ObjectId, ref: ‘User’, required: true }, // 父评论的用户信息 user: { // 用户id user_id: { type: mongoose.Schema.Types.ObjectId }, // 名字 name: { type: String, required: true, default: ’’ }, // 用户类型 0:博主 1:其他用户 type: { type: Number, default: 1 }, // 头像 avatar: { type: String, default: ‘user’ }, }, // 第三者评论 other_comments: [ { user: { id: { type: mongoose.Schema.Types.ObjectId }, // 名字 name: { type: String, required: true, default: ’’ }, // 用户类型 0:博主 1:其他用户 type: { type: Number, default: 1 }, }, // content content: { type: String, required: true, validate: /\S+/ }, // 状态 => 0 待审核 / 1 通过正常 / -1 已删除 / -2 垃圾评论 state: { type: Number, default: 1 }, // 创建日期 create_time: { type: Date, default: Date.now }, }, ], // 状态 => 0 待审核 / 1 通过正常 / -1 已删除 / -2 垃圾评论 state: { type: Number, default: 1 }, // 创建日期 create_time: { type: Date, default: Date.now }, // 最后修改日期 update_time: { type: Date, default: Date.now },});// 自增 ID 插件配置commentSchema.plugin(autoIncrement.plugin, { model: ‘Comment’, field: ‘id’, startAt: 1, incrementBy: 1,});// 标签模型module.exports = mongoose.model(‘Comment’, commentSchema);其他模块的具体需求,都是些常用的逻辑可以实现的,也很简单,这里就不展开讲了。6. 路由接口 routes6.1 主文件/*所有的路由接口/const user = require(’./user’);const article = require(’./article’);const comment = require(’./comment’);const message = require(’./message’);const tag = require(’./tag’);const link = require(’./link’);const category = require(’./category’);const timeAxis = require(’./timeAxis’);module.exports = app => { app.post(’/login’, user.login); app.post(’/logout’, user.logout); app.post(’/loginAdmin’, user.loginAdmin); app.post(’/register’, user.register); app.post(’/delUser’, user.delUser); app.get(’/currentUser’, user.currentUser); app.get(’/getUserList’, user.getUserList); app.post(’/addComment’, comment.addComment); app.post(’/addThirdComment’, comment.addThirdComment); app.post(’/changeComment’, comment.changeComment); app.post(’/changeThirdComment’, comment.changeThirdComment); app.get(’/getCommentList’, comment.getCommentList); app.post(’/addArticle’, article.addArticle); app.post(’/updateArticle’, article.updateArticle); app.post(’/delArticle’, article.delArticle); app.get(’/getArticleList’, article.getArticleList); app.get(’/getArticleListAdmin’, article.getArticleListAdmin); app.post(’/getArticleDetail’, article.getArticleDetail); app.post(’/likeArticle’, article.likeArticle); app.post(’/addTag’, tag.addTag); app.post(’/delTag’, tag.delTag); app.get(’/getTagList’, tag.getTagList); app.post(’/addMessage’, message.addMessage); app.post(’/addReplyMessage’, message.addReplyMessage); app.post(’/delMessage’, message.delMessage); app.post(’/getMessageDetail’, message.getMessageDetail); app.get(’/getMessageList’, message.getMessageList); app.post(’/addLink’, link.addLink); app.post(’/updateLink’, link.updateLink); app.post(’/delLink’, link.delLink); app.get(’/getLinkList’, link.getLinkList); app.post(’/addCategory’, category.addCategory); app.post(’/delCategory’, category.delCategory); app.get(’/getCategoryList’, category.getCategoryList); app.post(’/addTimeAxis’, timeAxis.addTimeAxis); app.post(’/updateTimeAxis’, timeAxis.updateTimeAxis); app.post(’/delTimeAxis’, timeAxis.delTimeAxis); app.get(’/getTimeAxisList’, timeAxis.getTimeAxisList); app.post(’/getTimeAxisDetail’, timeAxis.getTimeAxisDetail);};6.2 文章各模块的列表都是用了分页的形式的。import Article from ‘../models/article’;import User from ‘../models/user’;import { responseClient, timestampToTime } from ‘../util/util’;exports.addArticle = (req, res) => { // if (!req.session.userInfo) { // responseClient(res, 200, 1, ‘您还没登录,或者登录信息已过期,请重新登录!’); // return; // } const { title, author, keyword, content, desc, img_url, tags, category, state, type, origin } = req.body; let tempArticle = null if(img_url){ tempArticle = new Article({ title, author, keyword: keyword ? keyword.split(’,’) : [], content, numbers: content.length, desc, img_url, tags: tags ? tags.split(’,’) : [], category: category ? category.split(’,’) : [], state, type, origin, }); }else{ tempArticle = new Article({ title, author, keyword: keyword ? keyword.split(’,’) : [], content, numbers: content.length, desc, tags: tags ? tags.split(’,’) : [], category: category ? category.split(’,’) : [], state, type, origin, }); } tempArticle .save() .then(data => { responseClient(res, 200, 0, ‘保存成功’, data); }) .catch(err => { console.log(err); responseClient(res); });};exports.updateArticle = (req, res) => { // if (!req.session.userInfo) { // responseClient(res, 200, 1, ‘您还没登录,或者登录信息已过期,请重新登录!’); // return; // } const { title, author, keyword, content, desc, img_url, tags, category, state, type, origin, id } = req.body; Article.update( { _id: id }, { title, author, keyword: keyword ? keyword.split(’,’): [], content, desc, img_url, tags: tags ? tags.split(’,’) : [], category:category ? category.split(’,’) : [], state, type, origin, }, ) .then(result => { responseClient(res, 200, 0, ‘操作成功’, result); }) .catch(err => { console.error(err); responseClient(res); });};exports.delArticle = (req, res) => { let { id } = req.body; Article.deleteMany({ _id: id }) .then(result => { if (result.n === 1) { responseClient(res, 200, 0, ‘删除成功!’); } else { responseClient(res, 200, 1, ‘文章不存在’); } }) .catch(err => { console.error(’err :’, err); responseClient(res); });};// 前台文章列表exports.getArticleList = (req, res) => { let keyword = req.query.keyword || null; let state = req.query.state || ‘’; let likes = req.query.likes || ‘’; let tag_id = req.query.tag_id || ‘’; let category_id = req.query.category_id || ‘’; let pageNum = parseInt(req.query.pageNum) || 1; let pageSize = parseInt(req.query.pageSize) || 10; let conditions = {}; if (!state) { if (keyword) { const reg = new RegExp(keyword, ‘i’); //不区分大小写 conditions = { $or: [{ title: { $regex: reg } }, { desc: { $regex: reg } }], }; } } else if (state) { state = parseInt(state); if (keyword) { const reg = new RegExp(keyword, ‘i’); conditions = { $and: [ { $or: [{ state: state }] }, { $or: [{ title: { $regex: reg } }, { desc: { $regex: reg } }, { keyword: { $regex: reg } }] }, ], }; } else { conditions = { state }; } } let skip = pageNum - 1 < 0 ? 0 : (pageNum - 1) * pageSize; let responseData = { count: 0, list: [], }; Article.countDocuments(conditions, (err, count) => { if (err) { console.log(‘Error:’ + err); } else { responseData.count = count; // 待返回的字段 let fields = { title: 1, author: 1, keyword: 1, content: 1, desc: 1, img_url: 1, tags: 1, category: 1, state: 1, type: 1, origin: 1, comments: 1, like_User_id: 1, meta: 1, create_time: 1, update_time: 1, }; let options = { skip: skip, limit: pageSize, sort: { create_time: -1 }, }; Article.find(conditions, fields, options, (error, result) => { if (err) { console.error(‘Error:’ + error); // throw error; } else { let newList = []; if (likes) { // 根据热度 likes 返回数据 result.sort((a, b) => { return b.meta.likes - a.meta.likes; }); responseData.list = result; } else if (category_id) { // 根据 分类 id 返回数据 result.forEach(item => { if (item.category.indexOf(category_id) > -1) { newList.push(item); } }); let len = newList.length; responseData.count = len; responseData.list = newList; } else if (tag_id) { // 根据标签 id 返回数据 result.forEach(item => { if (item.tags.indexOf(tag_id) > -1) { newList.push(item); } }); let len = newList.length; responseData.count = len; responseData.list = newList; } else { responseData.list = result; } responseClient(res, 200, 0, ‘操作成功!’, responseData); } }); } });};// 后台文章列表exports.getArticleListAdmin = (req, res) => { let keyword = req.query.keyword || null; let state = req.query.state || ‘’; let likes = req.query.likes || ‘’; let pageNum = parseInt(req.query.pageNum) || 1; let pageSize = parseInt(req.query.pageSize) || 10; let conditions = {}; if (!state) { if (keyword) { const reg = new RegExp(keyword, ‘i’); //不区分大小写 conditions = { $or: [{ title: { $regex: reg } }, { desc: { $regex: reg } }], }; } } else if (state) { state = parseInt(state); if (keyword) { const reg = new RegExp(keyword, ‘i’); conditions = { $and: [ { $or: [{ state: state }] }, { $or: [{ title: { $regex: reg } }, { desc: { $regex: reg } }, { keyword: { $regex: reg } }] }, ], }; } else { conditions = { state }; } } let skip = pageNum - 1 < 0 ? 0 : (pageNum - 1) * pageSize; let responseData = { count: 0, list: [], }; Article.countDocuments(conditions, (err, count) => { if (err) { console.log(‘Error:’ + err); } else { responseData.count = count; // 待返回的字段 let fields = { title: 1, author: 1, keyword: 1, content: 1, desc: 1, img_url: 1, tags: 1, category: 1, state: 1, type: 1, origin: 1, comments: 1, like_User_id: 1, meta: 1, create_time: 1, update_time: 1, }; let options = { skip: skip, limit: pageSize, sort: { create_time: -1 }, }; Article.find(conditions, fields, options, (error, result) => { if (err) { console.error(‘Error:’ + error); // throw error; } else { if (likes) { result.sort((a, b) => { return b.meta.likes - a.meta.likes; }); } responseData.list = result; responseClient(res, 200, 0, ‘操作成功!’, responseData); } }) .populate([ { path: ’tags’, }, { path: ‘comments’, }, { path: ‘category’, }, ]) .exec((err, doc) => {}); } });};// 文章点赞exports.likeArticle = (req, res) => { if (!req.session.userInfo) { responseClient(res, 200, 1, ‘您还没登录,或者登录信息已过期,请重新登录!’); return; } let { id, user_id } = req.body; Article.findOne({ _id: id }) .then(data => { let fields = {}; data.meta.likes = data.meta.likes + 1; fields.meta = data.meta; let like_users_arr = data.like_users.length ? data.like_users : []; User.findOne({ _id: user_id }) .then(user => { let new_like_user = {}; new_like_user.id = user._id; new_like_user.name = user.name; new_like_user.avatar = user.avatar; new_like_user.create_time = user.create_time; new_like_user.type = user.type; new_like_user.introduce = user.introduce; like_users_arr.push(new_like_user); fields.like_users = like_users_arr; Article.update({ _id: id }, fields) .then(result => { responseClient(res, 200, 0, ‘操作成功!’, result); }) .catch(err => { console.error(’err :’, err); throw err; }); }) .catch(err => { responseClient(res); console.error(’err 1:’, err); }); }) .catch(err => { responseClient(res); console.error(’err 2:’, err); });};// 文章详情exports.getArticleDetailByType = (req, res) => { let { type } = req.body; if (!type) { responseClient(res, 200, 1, ‘文章不存在 !’); return; } Article.findOne({ type: type }, (Error, data) => { if (Error) { console.error(‘Error:’ + Error); // throw error; } else { data.meta.views = data.meta.views + 1; Article.updateOne({ type: type }, { meta: data.meta }) .then(result => { responseClient(res, 200, 0, ‘操作成功 !’, data); }) .catch(err => { console.error(’err :’, err); throw err; }); } }) .populate([ { path: ’tags’, select: ‘-_id’ }, { path: ‘category’, select: ‘-_id’ }, { path: ‘comments’, select: ‘-_id’ }, ]) .exec((err, doc) => { // console.log(“doc:”); // aikin // console.log(“doc.tags:",doc.tags); // aikin // console.log(“doc.category:",doc.category); // undefined });};// 文章详情exports.getArticleDetail = (req, res) => { let { id } = req.body; let type = Number(req.body.type) || 1; //文章类型 => 1: 普通文章,2: 简历,3: 管理员介绍 console.log(’type:’, type); if (type === 1) { if (!id) { responseClient(res, 200, 1, ‘文章不存在 !’); return; } Article.findOne({ _id: id }, (Error, data) => { if (Error) { console.error(‘Error:’ + Error); // throw error; } else { data.meta.views = data.meta.views + 1; Article.updateOne({ _id: id }, { meta: data.meta }) .then(result => { responseClient(res, 200, 0, ‘操作成功 !’, data); }) .catch(err => { console.error(’err :’, err); throw err; }); } }) .populate([ { path: ’tags’, }, { path: ‘category’, }, { path: ‘comments’, }, ]) .exec((err, doc) => { // console.log(“doc:”); // aikin // console.log(“doc.tags:",doc.tags); // aikin // console.log(“doc.category:",doc.category); // undefined }); } else { Article.findOne({ type: type }, (Error, data) => { if (Error) { console.log(‘Error:’ + Error); // throw error; } else { if (data) { data.meta.views = data.meta.views + 1; Article.updateOne({ type: type }, { meta: data.meta }) .then(result => { responseClient(res, 200, 0, ‘操作成功 !’, data); }) .catch(err => { console.error(’err :’, err); throw err; }); } else { responseClient(res, 200, 1, ‘文章不存在 !’); return; } } }) .populate([ { path: ’tags’, }, { path: ‘category’, }, { path: ‘comments’, }, ]) .exec((err, doc) => {}); }};6.3 评论评论是有状态的:状态 => 0 待审核 / 1 通过正常 / -1 已删除 / -2 垃圾评论。管理一级和三级评论是设置前台能不能展示的,默认是展示,如果管理员看了,是条垃圾评论就 设置为 -1 或者 -2 ,进行隐藏,前台就不会展现了。import { responseClient } from ‘../util/util’;import Comment from ‘../models/comment’;import User from ‘../models/user’;import Article from ‘../models/article’;//获取全部评论exports.getCommentList = (req, res) => { let keyword = req.query.keyword || null; let comment_id = req.query.comment_id || null; let pageNum = parseInt(req.query.pageNum) || 1; let pageSize = parseInt(req.query.pageSize) || 10; let conditions = {}; if (comment_id) { if (keyword) { const reg = new RegExp(keyword, ‘i’); //不区分大小写 conditions = { _id: comment_id, content: { $regex: reg }, }; } else { conditions = { _id: comment_id, }; } } else { if (keyword) { const reg = new RegExp(keyword, ‘i’); //不区分大小写 conditions = { content: { $regex: reg }, }; } } let skip = pageNum - 1 < 0 ? 0 : (pageNum - 1) * pageSize; let responseData = { count: 0, list: [], }; Comment.countDocuments(conditions, (err, count) => { if (err) { console.error(‘Error:’ + err); } else { responseData.count = count; // 待返回的字段 let fields = { article_id: 1, content: 1, is_top: 1, likes: 1, user_id: 1, user: 1, other_comments: 1, state: 1, create_time: 1, update_time: 1, }; let options = { skip: skip, limit: pageSize, sort: { create_time: -1 }, }; Comment.find(conditions, fields, options, (error, result) => { if (err) { console.error(‘Error:’ + error); // throw error; } else { responseData.list = result; responseClient(res, 200, 0, ‘操作成功!’, responseData); } }); } });};// 添加一级评论exports.addComment = (req, res) => { if (!req.session.userInfo) { responseClient(res, 200, 1, ‘您还没登录,或者登录信息已过期,请重新登录!’); return; } let { article_id, user_id, content } = req.body; User.findById({ _id: user_id, }) .then(result => { // console.log(‘result :’, result); if (result) { let userInfo = { user_id: result._id, name: result.name, type: result.type, avatar: result.avatar, }; let comment = new Comment({ article_id: article_id, content: content, user_id: user_id, user: userInfo, }); comment .save() .then(commentResult => { Article.findOne({ _id: article_id }, (errors, data) => { if (errors) { console.error(‘Error:’ + errors); // throw errors; } else { data.comments.push(commentResult._id); data.meta.comments = data.meta.comments + 1; Article.updateOne({ _id: article_id }, { comments: data.comments, meta: data.meta }) .then(result => { responseClient(res, 200, 0, ‘操作成功 !’, commentResult); }) .catch(err => { console.error(’err :’, err); throw err; }); } }); }) .catch(err2 => { console.error(’err :’, err2); throw err2; }); } else { responseClient(res, 200, 1, ‘用户不存在’); } }) .catch(error => { console.error(’error :’, error); responseClient(res); });};// 添加第三者评论exports.addThirdComment = (req, res) => { if (!req.session.userInfo) { responseClient(res, 200, 1, ‘您还没登录,或者登录信息已过期,请重新登录!’); return; } let { article_id, comment_id, user_id, content } = req.body; Comment.findById({ _id: comment_id, }) .then(commentResult => { User.findById({ _id: user_id, }) .then(userResult => { if (userResult) { let userInfo = { user_id: userResult._id, name: userResult.name, type: userResult.type, avatar: userResult.avatar, }; let item = { user: userInfo, content: content, }; commentResult.other_comments.push(item); Comment.updateOne( { _id: comment_id }, { other_comments: commentResult, }, ) .then(result => { responseClient(res, 200, 0, ‘操作成功’, result); Article.findOne({ _id: article_id }, (errors, data) => { if (errors) { console.error(‘Error:’ + errors); // throw errors; } else { data.meta.comments = data.meta.comments + 1; Article.updateOne({ _id: article_id }, { meta: data.meta }) .then(result => { // console.log(‘result :’, result); responseClient(res, 200, 0, ‘操作成功 !’, result); }) .catch(err => { console.log(’err :’, err); throw err; }); } }); }) .catch(err1 => { console.error(’err1:’, err1); responseClient(res); }); } else { responseClient(res, 200, 1, ‘用户不存在’); } }) .catch(error => { console.error(’error :’, error); responseClient(res); }); }) .catch(error2 => { console.error(’error2 :’, error2); responseClient(res); });};// 管理一级评论exports.changeComment = (req, res) => { if (!req.session.userInfo) { responseClient(res, 200, 1, ‘您还没登录,或者登录信息已过期,请重新登录!’); return; } let { id, state } = req.body; Comment.updateOne( { _id: id }, { state: Number(state), }, ) .then(result => { responseClient(res, 200, 0, ‘操作成功’, result); }) .catch(err => { console.error(’err:’, err); responseClient(res); });};// 管理第三者评论exports.changeThirdComment = (req, res) => { if (!req.session.userInfo) { responseClient(res, 200, 1, ‘您还没登录,或者登录信息已过期,请重新登录!’); return; } let { comment_id, state, index } = req.body; Comment.findById({ _id: comment_id, }) .then(commentResult => { let i = index ? Number(index) : 0; if (commentResult.other_comments.length) { commentResult.other_comments[i].state = Number(state); Comment.updateOne( { _id: comment_id }, { other_comments: commentResult, }, ) .then(result => { responseClient(res, 200, 0, ‘操作成功’, result); }) .catch(err1 => { console.error(’err1:’, err1); responseClient(res); }); } else { responseClient(res, 200, 1, ‘第三方评论不存在!’, result); } }) .catch(error2 => { console.log(’error2 :’, error2); responseClient(res); });};其他模块的具体需求,都是些常用的逻辑可以实现的,也很简单,这里就不展开讲了。7. Build Setup ( 构建安装 )# install dependenciesnpm install # serve with hot reload at localhost: 3000npm start # build for production with minification请使用 pm2 ,可以永久运行在服务器上,且不会一报错 node 程序就挂了。8. 项目地址如果觉得该项目不错或者对你有所帮助,欢迎到 github 上给个 star,谢谢。项目地址:前台展示: https://github.com/乐趣区/blog-react管理后台:https://github.com/乐趣区/blog-react-admin后端:https://github.com/乐趣区/blog-nodeblog:https://github.com/乐趣区/blog本博客系统的系列文章:react + node + express + ant + mongodb 的简洁兼时尚的博客网站react + Ant Design + 支持 markdown 的 blog-react 项目文档说明基于 node + express + mongodb 的 blog-node 项目文档说明服务器小白的我,是如何将node+mongodb项目部署在服务器上并进行性能优化的9. 最后小汪也是第一次搭建 node 后端项目,也参考了其他项目。参考项目:1. nodepress2. React-Express-Blog-Demo对 全栈开发 有兴趣的朋友,可以扫下方二维码,关注我的公众号,我会不定期更新有价值的内容。微信公众号:乐趣区分享 前端、后端开发 等相关的技术文章,热点资源,全栈程序员的成长之路。关注公众号并回复 福利 便免费送你视频资源,绝对干货。福利详情请点击: 免费资源分享–Python、Java、Linux、Go、node、vue、react、javaScript ...

November 25, 2018 · 16 min · jiezi

react + node + express + ant + mongodb 的简洁兼时尚的博客网站

前言此项目是用于构建博客网站的,由三部分组成,包含前台展示、管理后台和后端。此项目是基于 react + node + express + ant + mongodb 的,项目已经开源,项目地址在 github 上,喜欢的,欢迎给个 star 。项目地址:前台展示: https://github.com/乐趣区/blog-react管理后台:https://github.com/乐趣区/blog-react-admin后端:https://github.com/乐趣区/blog-node1. 效果图1.1 前台展示前台展示目前只支持 pc 端。1.2 管理后台管理后台是在蚂蚁金服用户开源的 ANT DESIGN PRO 基础上进行开发的。2. 体验地址网站主页: http://乐趣区.cn/main.html 网站首页:http://乐趣区.cn/管理后台:https://preview.pro.ant.design/user/login3. 计划这次是一个完整的全栈式开发,只要部署了这三个项目的代码,是完全可以搭建好博客网站的。作为一个后端的小白,在这次开发中,小汪也遇到了很多问题。往后的时间里,我会就这三个项目,推出相应的三篇文章教程或者说明和踩到的坑,敬请期待。4. 收获与感触学而不用,基本等于没学,所以为了有 react 相关的技术栈的实战经验,所以用了 react ,而且后端技术 node.js 和 mongodb 也是这一个多月里现学现用的,所以项目中肯定还有很多我不知道的实用技巧,如果写的不好的地方,请大家指出。网站前端部分如果用 vue 相关技术栈来完成的话,会更好更快,因为本人专长的是 vue 相关的技术栈。因为最近一直在做自己的个人博客网站,所以好久没更新技术文章了;而且是利用业余时间做的,所以经过差不多两个月的搬砖,现在网站终于都上线了。开发网站的这段时间里,每天晚上几乎都搬砖到接近 11 点,周末的时间大多也在搬砖,今晚写完这篇文章,也快 12 点了,搬砖不易啊,喜欢或者觉得不错的,欢迎到 github 上给个 star,谢谢。最后对 全栈开发 有兴趣的朋友可以扫下方二维码关注我的公众号,我会不定期更新有价值的内容。微信公众号:乐趣区分享 前端、后端开发等相关的技术文章,热点资源,全栈程序员的成长之路。关注公众号并回复 福利 便免费送你视频资源,绝对干货。福利详情请点击: 免费资源分享–Python、Java、Linux、Go、node、vue、react、javaScript

November 22, 2018 · 1 min · jiezi

认识 MongoDB 4.0 的新特性——事务(Transactions)

前言相信使用过主流的关系型数据库的朋友对“事务(Transactions)”不会太陌生,它可以让我们把对多张表的多次数据库操作整合为一次原子操作,这在高并发场景下可以保证多个数据操作之间的互不干扰;并且一旦在这些操作过程任一环节中出现了错误,事务会中止并且让数据回滚,这使得同时在多张表中修改数据的时候保证了数据的一致性。以前 MongoDB 是不支持事务的,因此开发者在需要用到事务的时候,不得不借用其他工具,在业务代码层面去弥补数据库的不足。随着 4.0 版本的发布,MongoDB 也为我们带来了原生的事务操作,下面就让我们一起来认识它,并通过简单的例子了解如何去使用。介绍事务和副本集(Replica Sets)副本集是 MongoDB 的一种主副节点架构,它使数据得到最大的可用性,避免单点故障引起的整个服务不能访问的情况的发生。目前 MongoDB 的多表事务操作仅支持在副本集上运行,想要在本地环境安装运行副本集可以借助一个工具包——run-rs,以下的文章中有详细的使用说明:https://thecodebarbarian.com/…事务和会话(Sessions)事务和会话(Sessions)关联,一个会话同一时刻只能开启一个事务操作,当一个会话断开,这个会话中的事务也会结束。事务中的函数Session.startTransaction()在当前会话中开始一次事务,事务开启后就可以开始进行数据操作。在事务中执行的数据操作是对外隔离的,也就是说事务中的操作是原子性的。Session.commitTransaction()提交事务,将事务中对数据的修改进行保存,然后结束当前事务,一次事务在提交之前的数据操作对外都是不可见的。Session.abortTransaction()中止当前的事务,并将事务中执行过的数据修改回滚。重试当事务运行中报错,catch 到的错误对象中会包含一个属性名为 errorLabels 的数组,当这个数组中包含以下2个元素的时候,代表我们可以重新发起相应的事务操作。TransientTransactionError:出现在事务开启以及随后的数据操作阶段UnknownTransactionCommitResult:出现在提交事务阶段示例经过上面的铺垫,你是不是已经迫不及待想知道究竟应该怎么写代码去完成一次完整的事务操作?下面我们就简单写一个例子:场景描述: 假设一个交易系统中有2张表——记录商品的名称、库存数量等信息的表 commodities,和记录订单的表 orders。当用户下单的时候,首先要找到 commodities 表中对应的商品,判断库存数量是否满足该笔订单的需求,是的话则减去相应的值,然后在 orders 表中插入一条订单数据。在高并发场景下,可能在查询库存数量和减少库存的过程中,又收到了一次新的创建订单请求,这个时候可能就会出问题,因为新的请求在查询库存的时候,上一次操作还未完成减少库存的操作,这个时候查询到的库存数量可能是充足的,于是开始执行后续的操作,实际上可能上一次操作减少了库存后,库存的数量就已经不足了,于是新的下单请求可能就会导致实际创建的订单数量超过库存数量。以往要解决这个问题,我们可以用给商品数据“加锁”的方式,比如基于 Redis 的各种锁,同一时刻只允许一个订单操作一个商品数据,这种方案能解决问题,缺点就是代码更复杂了,并且性能会比较低。如果用数据库事务的方式就可以简洁很多:commodities 表数据(stock 为库存):{ “_id” : ObjectId(“5af0776263426f87dd69319a”), “name” : “灭霸原味手套”, “stock” : 5 }{ “_id” : ObjectId(“5af0776263426f87dd693198”), “name” : “雷神专用铁锤”, “stock” : 2 }orders 表数据:{ “_id” : ObjectId(“5af07daa051d92f02462644c”), “commodity”: ObjectId(“5af0776263426f87dd69319a”), “amount”: 2 }{ “_id” : ObjectId(“5af07daa051d92f02462644b”), “commodity”: ObjectId(“5af0776263426f87dd693198”), “amount”: 3 }通过一次事务完成创建订单操作(mongo Shell):// 执行 txnFunc 并且在遇到 TransientTransactionError 的时候重试function runTransactionWithRetry(txnFunc, session) { while (true) { try { txnFunc(session); // 执行事务 break; } catch (error) { if ( error.hasOwnProperty(’errorLabels’) && error.errorLabels.includes(‘TransientTransactionError’) ) { print(‘TransientTransactionError, retrying transaction …’); continue; } else { throw error; } } }}// 提交事务并且在遇到 UnknownTransactionCommitResult 的时候重试function commitWithRetry(session) { while (true) { try { session.commitTransaction(); print(‘Transaction committed.’); break; } catch (error) { if ( error.hasOwnProperty(’errorLabels’) && error.errorLabels.includes(‘UnknownTransactionCommitResult’) ) { print(‘UnknownTransactionCommitResult, retrying commit operation …’); continue; } else { print(‘Error during commit …’); throw error; } } }}// 在一次事务中完成创建订单操作function createOrder(session) { var commoditiesCollection = session.getDatabase(‘mall’).commodities; var ordersCollection = session.getDatabase(‘mall’).orders; // 假设该笔订单中商品的数量 var orderAmount = 3; // 假设商品的ID var commodityID = ObjectId(‘5af0776263426f87dd69319a’); session.startTransaction({ readConcern: { level: ‘snapshot’ }, writeConcern: { w: ‘majority’ }, }); try { var { stock } = commoditiesCollection.findOne({ _id: commodityID }); if (stock < orderAmount) { print(‘Stock is not enough’); session.abortTransaction(); throw new Error(‘Stock is not enough’); } commoditiesCollection.updateOne( { _id: commodityID }, { $inc: { stock: -orderAmount } } ); ordersCollection.insertOne({ commodity: commodityID, amount: orderAmount, }); } catch (error) { print(‘Caught exception during transaction, aborting.’); session.abortTransaction(); throw error; } commitWithRetry(session);}// 发起一次会话var session = db.getMongo().startSession({ readPreference: { mode: ‘primary’ } });try { runTransactionWithRetry(createOrder, session);} catch (error) { // 错误处理} finally { session.endSession();}上面的代码看着感觉很多,其实 runTransactionWithRetry 和 commitWithRetry 这两个函数都是可以抽离出来成为公共函数的,不需要每次操作都重复书写。用上了事务之后,因为事务中的数据操作都是一次原子操作,所以我们就不需要考虑分布并发导致的数据一致性的问题,是不是感觉简单了许多?你可能注意到了,代码中在执行 startTransaction 的时候设置了两个参数——readConcern 和 writeConcern,这是 MongoDB 读写操作的确认级别,在这里用于在副本集中平衡数据读写操作的可靠性和性能,如果在这里展开就太多了,所以感兴趣的朋友建议去阅读官方文档了解一下:readConcern:https://docs.mongodb.com/mast…writeConcern:https://docs.mongodb.com/mast…我们正在进行限时有奖读者调查,欢迎参加:创宇前端期待听到你的声音文 / Xss本文已由作者授权发布,版权属于创宇前端。欢迎注明出处转载本文。本文链接:https://knownsec-fed.com/2018…想要订阅更多来自知道创宇开发一线的分享,请搜索关注我们的微信公众号:乐趣区。欢迎留言讨论,我们会尽可能回复。感谢您的阅读。 ...

November 13, 2018 · 2 min · jiezi

有坑勿踩(一):MongoDB PSS vs PSA

前言在技术社区混了这么长时间,因为一些常见的技术问题反复被问到,总是想写写文章把它们讲清楚。无奈很多时候看似基础的技术问题背后都隐藏着很深的原因,想要一次性说清楚太花时间,而平时又没有很多时间能花在上面(主要是懒),所以产生了写一系列文章的想法,讲讲我或我的客户使用MongoDB过程中经常遇到的各种“坑”。话虽如此,难者不会会者不难,希望看了这些讲解你就不再认为这些是“坑”了。在讲解这些问题前,我会假设读者已经对MongoDB有了最基础的了解,因此一些基本名词和概念就不做过多的解释,请自己查阅相关资料。PSS vs PSA什么是PSS/PSA?在MongoDB复制集中,存在三种类型的角色:PRIMARY: 主节点(P)SECONDARY: 从节点(S)ARBITER: 仲裁节点(A)构建一个复制集至少需要3个节点,所以用户就有了两种选择,即PSS和PSA。注意:记住A的作用始终是把集群中具有投票权的节点总数凑成奇数用,防止“脑裂”。因此诸如PAA,PSSAA之类的配置是没有存在的意义的,极端情况下还会扰乱集群的正常工作。PSA有什么好处?最直接的好处:省钱啊!随便找台机器,不消耗什么资源就可以运行一个A,比一个S的成本小多了。PSA有什么问题?读写失效最直接的问题来自于MongoDB中的一个配置选项{w: “majority”},这个配置决定了一次成功的写入操作需要到达多少个节点才算真正的成功,w可以定义为1,2,…n(n<=集群节点总数)或majority。而majority是保证在集群故障时不丢失数据的必要配置(关于majority和w以后再专门写文章讨论)。其代表的意义是:集群中必须有大多数节点收到并确认了一个写操作,这个写操作才算成功。在三个节点的集群中,{w: “majority”} == {w: 2}。因此如果集群配置是PSA,由于A是不存数据的,所以集群中能够确认写操作的节点只有P和S,刚好是2。到这里可能有人已经看出问题了:在PSA中如果有一个数据节点宕机,则再也不能满足{w: “majority”},所有使用这种配置的写操作都会失败。因此可以说,PSA在一定程度上丢失了高可用性,因为任何一个数据节点的失效都会导致{w: “majority”}类型写入的失败。引申一下,ReadConcern同样有可选值majority,因此同样可能因为一个数据节点的失效而失效。集群部分功能失效可能你会觉得:什么{w: “majority”}没听说过啊,我也不在乎丢失数据,那用PSA是不是就没有问题了?当然不是!在很多你没注意到的场景都存在着{w: “majority”}。比如:分片集群数据迁移。无论源或者目标片中不能够满足大多数时,迁移都会失败。分片集群管理。包括但不限于以下这些操作实际上都隐含着{w: “majority”}。一旦不能满足,这些操作都会失败:db.dropDatabase()db.collection.drop()db.collection.dropIndex({…})sh.shardCollection(…)db.createUser(…)结论majority比你想象的更重要,PSA不能够提供足够的可用数据节点来保证majority,因此在很多场景下会引发隐藏的错误。在有可能的情况下,应尽量使用PSS代替PSA。

November 13, 2018 · 1 min · jiezi

让云服务器性能提升10倍的方法,再也不用担心周报没有干货了!

欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~本文由腾讯云数据库 TencentDB发表于云+社区专栏随着国内服务共享化的热潮普及,共享单车,共享雨伞,共享充电宝等各种服务如雨后春笋,随之而来的LBS服务定位问题成为了后端服务的一个挑战。MongoDB对LBS查询的支持较为友好,也是各大LBS服务商的首选数据库。腾讯云MongoDB团队在运营中发现,原生MongoDB在LBS服务场景下有较大的性能瓶颈,经腾讯云团队专业的定位分析与优化后,云MongoDB在LBS服务的综合性能上,有10倍以上的提升。LBS业务特点以共享单车服务为例,LBS业务具有2个特点,分别是时间周期性和坐标分布不均匀。一.时间周期性高峰期与低谷期的QPS量相差明显,并且高峰期和低峰期的时间点相对固定。 二.坐标分布不均匀坐地铁的上班族,如果留意可能会发现,在上班早高峰时,地铁周围摆满了共享单车,而下班 时段,地铁周围的共享单车数量非常少。如下图,经纬度(121,31.44)附近集中了99%以上 的坐标。此外,一些特殊事件也会造成点的分布不均匀,例如深圳湾公园在特殊家假日涌入大量的客户,同时这个地域也会投放大量的单车。部分地域单车量非常集中,而其他地域就非常稀疏。MongoDB的LBS服务原理MongoDB中使用2d_index 或2d_sphere_index来创建地理位置索引(geoIndex),两者差别不大,下面我们以2d_index为例来介绍。一.2D索引的创建与使用db.coll.createIndex({“lag”:“2d”}, {“bits”:int}))通过上述命令来创建一个2d索引,索引的精度通过bits来指定,bits越大,索引的精度就越高。更大的bits带来的插入的overhead可以忽略不计db.runCommand({geoNear: tableName,maxDistance: 0.0001567855942887398,distanceMultiplier: 6378137.0,num: 30,near: [ 113.8679388183982, 22.58905429302385 ],spherical: true|false})通过上述命令来查询一个索引,其中spherical:true|false 表示应该如何理解创建的2d索引,false表示将索引理解为平面2d索引,true表示将索引理解为球面经纬度索引。这一点比较有意思,一个2d索引可以表达两种含义,而不同的含义是在查询时被理解的,而不是在索引创建时。二.2D索引的理论 MongoDB 使用GeoHash的技术来构建2d索引(见wiki geohash 文字链 https://en.wikipedia.org/wiki...RecordId的索引映射方式存储在Btree中 很显然的,一个2bits的精度能把平面分为4个grid,一个4bits的精度能把平面分为16个grid。 2d索引的默认精度是长宽各为26,索引把地球分为(2^26)(2^26)块,每一块的边长估算为2PI6371000/(1<<26) = 0.57 米 mongodb的官网上说的60cm的精度就是这么估算出来的 By default, a 2d index on legacy coordinate pairs uses 26 bits of precision, which isroughly equivalent to 2 feet or 60 centimeters of precision using the default range of-180 to 180三.2D索引在Mongodb中的存储上面我们讲到Mongodb使用平面四叉树的方式计算Geohash。事实上,平面四叉树仅存在于运算的过程中,在实际存储中并不会被使用到。插入 对于一个经纬度坐标[x,y],MongoDb计算出该坐标在2d平面内的grid编号,该编号为是一个52bit的int64类型,该类型被用作btree的key,因此实际数据是按照 {GeoHashId->RecordValue}的方式被插入到btree中的。查询 对于geo2D索引的查询,常用的有geoNear和geoWithin两种。geoNear查找距离某个点最近的N个点的坐标并返回,该需求可以说是构成了LBS服务的基础(陌陌,滴滴,摩拜),geoWithin是查询一个多边形内的所有点并返回。我们着重介绍使用最广泛的geoNear查询。geoNear的查询过程,查询语句如下db.runCommand({geoNear: “places”, //table Namenear: [ -73.9667, 40.78 ] , // central pointspherical: true, // treat the index as a spherical indexquery: { category: “public” } // filtersmaxDistance: 0.0001531 // distance in about one kilometer}geoNear可以理解为一个从起始点开始的不断向外扩散的环形搜索过程。如下图所示: 由于圆自身的性质,外环的任意点到圆心的距离一定大于内环任意点到圆心的距离,所以以圆 环进行扩张迭代的好处是: 1)减少需要排序比较的点的个数 2)能够尽早发现满足条件的点从而返回,避免不必要的搜索 MongoDB在实现的细节中,如果内环搜索到的点数过少,圆环每次扩张的步长会倍增MongoDB LBS服务遇到的问题部分大客户在使用MongoDB的geoNear功能查找附近的对象时,经常会发生慢查询较多的问题,早高峰压力是低谷时段的10-20倍,坐标不均匀的情况慢查询严重,濒临雪崩。初步分析发现,这些查询扫描了过多的点集。 如下图,查找500米范围内,距离最近的10条记录,花费了500ms,扫描了24000+的记录。类似的慢查询占据了高峰期5%左右的查询量测试环境复现与定位 排查数据库的性能问题,主要从锁等待,IO等待,CPU消耗三封面分析。上面的截图扫描了过多的记录,直觉上是CPU或者IO消耗性的瓶颈。为了严谨起见,我们在测试环境复现后,发现慢日志中无明显的timeAcquiringMicroseconds项排除了MongoDB执行层面的锁竞争问题,并选用较大内存的机器使得数据常驻内存,发现上述用例依旧需要200ms以上的执行时间。10核的CPU资源针对截图中的case,只能支持50QPS。 为何扫描集如此大 上面我们说过,MongoDB搜索距离最近的点的过程是一个环形扩张的过程,如果内环满足条件的点不够多,每次的扩张半径都会倍增。因此在遇到内环点稀少,外环有密集点的场景时,容易陷入BadCase。如下图,我们希望找到离中心点距离最近的三个点。由于圆环扩张太快,外环做了很多的无用扫描与排序。 这样的用例很符合实际场景,早高峰车辆聚集在地铁周围,你从家出发一路向地铁,边走边找,共享单车软件上动态搜索距你最近的10辆车,附近只有三两辆,于是扩大搜索半径到地铁周围,将地铁周围的所有几千辆车都扫描计算一遍,返回距离你最近的其余的七八辆 问题的解决问题我们已经知道了,我们对此的优化方式是控制每一圈的搜索量,为此我们为geoNear命令增加了两个参数,将其传入NearStage中。hintCorrectNum可以控制结果品质的下限,返回的前N个一定是最靠近中心点的N个点。hintScan用以控制扫描集的大小的上限。hintScan: 已经扫描的点集大小大于hintScan后,做模糊处理。 hintCorrectNum:已经返回的结果数大于hintCorrectNum后,做模糊处理。该优化本质上是通过牺牲品质来尽快返回结果。对于国内大部分LBS服务来说,完全的严格最近并不是必要的。且可以通过控制参数获得严格最近的效果。在搜索过程中,密集的点落到一个环内,本身距离相差也不会不大。该优化在上线后,将部分大客户的MongoDB性能上限从单机1000QPS提升了10倍到10000QPS以上。和Redis3.2的对比Redis3.2也加入了地理位置查询的功能,我们也将开源Redis和云数据库MongoDB进行对比。 Redis使用方式:GEORADIUS appname 120.993965 31.449034 500 m count 30 asc。在密集数据集场景下,使用腾讯云MongoDB和开源的Redis进行了性能对比。下图是在密集数据集上,在24核CPU机器上,MongoDB单实例与Redis单实例的测试对比。需要注意的是Redis本身是单线程的内存缓存数据库。MongoDB是多线程的高可用持久化的数据库,两者的使用场景有较大不同。 总结MongoDB原生的geoNear接口是国内各大LBS应用的主流选择。原生MongoDB在点集稠密的情况下,geoNear接口效率会急剧下降,单机甚至不到1000QPS。腾讯云MongoDB团队对此进行了持续的优化,在不影响效果的前提下,geoNear的效率有10倍以上的提升,为我们的客户如摩拜提供了强力的支持,同时相比Redis3.2也有较大的性能优势。相关阅读【每日课程推荐】机器学习实战!快速入门在线广告业务及CTR相应知识 ...

October 18, 2018 · 1 min · jiezi

MongoDB常用语句

如果觉得 Mongodb 语句不太好理解,可以和 SQL 语句进行对比,学起来要容易很多。1. 查询(find)查询所有结果select * from articledb.article.find()指定返回哪些键select title, author from articledb.article.find({}, {“title”: 1, “author”: 1})where条件select * from article where title = “mongodb"db.article.find({“title”: “mongodb”})and条件select * from article where title = “mongodb” and author = “god"db.article.find({“title”: “mongodb”, “author”: “god”})or条件select * from article where title = “mongodb” or author = “god"db.article.find({"$or”: [{“title”: “mongodb”}, {“author”: “god”}]})比较条件select * from article where read >= 100;db.article.find({“read”: {"$gt”: 100}})> $gt(>)、$gte(>=)、$lt(<)、$lte(<=) select * from article where read >= 100 and read <= 200 db.article.find({“read”: {"$gte”: 100, “lte”: 200}})in条件select * from article where author in (“a”, “b”, “c”)db.article.find({“author”: {"$in": [“a”, “b”, “c”]}})likeselect * from article where title like “%mongodb%“db.article.find({“title”: /mongodb/})countselect count(*) from articledb.article.count()不等于select * from article where author != “a"db.article.find({ “author”: { “$ne”: “a” }})排序升序:select * from article where type = “mongodb” order by read descdb.article.find({“type”: “mongodb”}).sort({“read”: -1})降序:select * from article where type = “mongodb” order by read ascdb.article.find({“type”: “mongodb”}).sort({“read”: 1})findOne():除了只返回一个查询结果外,使用方法与find()一样。2.创建(insert)insert into article(title, author, content) values(“mongodb”, “tg”, “haha”)db.article.insert({“title”: “mongodb”, “author”: “tg”, “content”: “haha”})3.更新(update)update()语法:db.collecion.update(query, update[, options] ) query : 必选,查询条件,类似find中的查询条件。 update : 必选,update的对象和一些更新的操作符(如$,$inc…)等 options:可选,一些更新配置的对象。 upsert:可选,这个参数的意思是,如果不存在update的记录,是否插入objNew,true为插入,默认是false,不插入。 multi:可选,mongodb 默认是false,只更新找到的第一条记录,如果这个参数为true,就把按条件查出来多条记录全部更新。 writeConcern:可选,抛出异常的级别。简单更新:update article set title = “mongodb” where read > 100db.article.update({“read”: {"$gt”: 100}}, {"$set”: { “title”: “mongodb”}})save()db.article.save({_id: 123, title: “mongodb”})执行上面的语句,如果集合中已经存在一个_id为123的文档,则更新对应字段;否则插入。注:如果更新对象不存在_id,系统会自动生成并作为新的文档插入。更新操作符MongoDB提供一些强大的更新操作符。更新特定字段($set):update game set count = 10000 where _id = 123db.game.update({"_id”: 123}, { “$set”: {“count”: 10000}})删除特定字段($unset):注:$unset指定字段的值只需是任意合法值即可。递增或递减($inc) db.game.update({"_id": 123}, { “$inc”: {“count”: 10}}) // 每次count都加10> 注意:$inc对应的字段必须是数字,而且递增或递减的值也必须是数字。数组追加($push): db.game.update({"_id": 123}, { “$push”: {“score”: 123}})还可以一次追加多个元素: db.game.update({"_id": 123}, {"$push": {“score”: [12,123]}})注:追加字段必须是数组。如果数组字段不存在,则自动新增,然后追加。一次追加多个元素($pushAll): db.game.update({"_id": 123}, {"$pushAll": {“score”: [12,123]}})追加不重复元素($addToSet):$addToSet类似集合Set,只有当这个值不在元素内时才增加: db.game.update({"_id": 123}, {"$addToSet": {“score”: 123}})删除元素($pop):db.game.update({"_id": 123}, {"$pop": {“score”: 1}}) // 删除最后一个元素db.game.update({"_id": 123}, {"$pop": {“score”: -1}}) // 删除第一个元素注:$pop每次只能删除数组中的一个元素,1表示删除最后一个,-1表示删除第一个。删除特定元素($pull):db.game.update({"_id": 123}, {"$pull": {“score”: 123}})上面的语句表示删除数组score内值等于123的元素。删除多个特定元素($pullAll):db.game.update({"_id": 123}, {"$pullAll": {score: [123,12]}})上面的语句表示删除数组内值等于123或12的元素。更新嵌套数组的值:使用数组下标(从0开始):{ address: [{place: “nanji”, tel: 123}, {place: “dongbei”, tel: 321}]} db.game.update({"_id": 123}, {"$set": {“address.0.tel”: 213}})如果你不知道要更新数组哪项,我们可以使用$操作符( $表示自身,也就是按查询条件找出的数组里面的项自身,而且只会应用找到的第一条数组项): db.game.update({“address.place”: “nanji”}, {"$set": {“address.$.tel”: 123}})在上面的语句中,$就是查询条件{“address.place”: “nanji”}的查询结果,也就是{place: “nanji”, tel: 123},所以{“address.$.tel”: 123}也就是{“address.{place: “nanji”, tel: 123}.tel”: 123}4. 删除(remove)删除所有文档:delete from articledb.article.remove()删除指定文档: delete from article where title = “mongodb” db.article.remove({title: “mongodb”})更多参考:MongoDB常用语句 ...

October 17, 2018 · 2 min · jiezi