欢迎光临
我们一直在努力

15个最好用的JavaScript代码压缩工具

mumupudding阅读(8)

JavaScript 代码压缩是指去除源代码里的所有不必要的字符,而不改变其功能的过程。这些不必要的字符通常包括空格字符,换行字符,注释以及块分隔符等用来增加可读性的代码,但并不需要它来执行。

在这篇文章中,我们选择了15个最好用的 JavaScript 压缩工具,有简单的在线转换器,GUI工具和命令行界面等。

1. JavaScript Minifier

它是一个很好的工具,带有API来缩小js代码。

2. JSMIni

如果您想快速轻松地缩小JavaScript或jQuery文件,请使用jsMini。只需复制和粘贴源代码,选择要基本压缩还是完全压缩,然后缩小代码。

3. JSCompress

JSCompress.com是一个在线javascript压缩器,允许您压缩和缩小javascript文件。压缩的javascript文件是生产环境的理想选择,因为它们通常会将文件的大小减少30-90%。大多数文件大小的减少是通过删除Web浏览器或访问者不需要的注释和额外的空白字符来实现的。

4. Minifier

一个简化CSS/JS的简单工具,没有大的设置。它将CSS中的URL从原来的位置重新工作到输出位置。它会自动解析CSS中的@import语句。

5. Gulp.js

js是流构建系统。它使用流和代码对配置,使一个更简单和更直观的构建。通过更喜欢代码而不是配置,GUMP使简单的事情变得简单,并使复杂的任务易于管理。通过利用节点流的强大功能,您可以获得不将中间文件写入磁盘的快速构建。GUP的严格插件指南确保插件保持简单,并按您预期的方式工作。

6. Uglifyjs

这个包实现了一个通用的JavaScript解析器/压缩器/美化工具包。它是在NodeJS上开发的,但是它应该在任何支持CommonJS模块系统的JavaScript平台上工作(如果您选择的平台不支持CommonJS,那么您可以很容易地实现它,或者放弃导出。

7. Grunt

grunt是一个用于JavaScript项目的基于任务的命令行构建工具。它有以下可以在项目中使用的预定义任务:连接文件、使用JSHint验证文件、使用UGIFIFYJS执行minify文件、使用节点单元运行单元测试等等。

8. Koala

koala是一个GUI应用程序,用于Less、Sass、Compass和CoffeeScript编译,以帮助Web开发人员更有效地使用它们。考拉可以在Windows、Linux和Mac上运行。

9. Prepros

PreProfessional是一个用于编译更少的工具,Sass、Compass、Stylus、Jade以及更多的带有自动CSS前缀的工具,它带有内置的服务器,用于跨浏览器测试。它运行在Windows、Mac和Linux上。

10. Ajax Minifier

此工具是一个Windows应用程序,允许您在不使用命令行或VisualStudio的情况下运行MicrosoftAjaxMinifier。它缩小了文件夹和嵌套文件夹中的所有javascript文件,缩小了单个javascript文件,启用/禁用了小型程序的超压缩和分析选项等等。

11. Smaller

更小的是一个强大的HTML,CSS和JavaScript压缩器在OSX上,它也有能力将多个文件组合成一个。压缩您的文件,使您的网站加载更快。

12. Ultra Minifier

超迷你是最简单的YUI压缩机GUI,以缩小Javascript和CSS代码,而不使用终端。

13. Require JS

RequireJS是一个JavaScript文件和模块加载器。它是为浏览器内使用而优化的,但它可以用于其他JavaScript环境,如Rhino和Node。使用像RequireJS这样的模块化脚本加载程序将提高代码的速度和质量。它包括一个优化工具,可以作为部署代码的打包步骤的一部分运行。优化工具可以组合和缩小JavaScript文件,以实现更好的性能。

14. Online JavaScript/CSS Compressor

这是一个用于压缩JavaScript或CSS的Web接口。该工具使用UgulifyJS 2、Clean-CSS和HTML缩略符.

15. Minify

minify是一个PHP 5应用程序,它可以帮助你遵循雅虎的一些高性能网站规则,它结合了多个css或Javascript文件,删除了不必要的空白和注释,并为它们提供gzip编码和最佳客户端缓存头。

深度解析React服务端渲染

mumupudding阅读(10)

React 服务端渲染

服务端渲染的基本套路就是用户请求过来的时候,在服务端生成一个我们希望看到的网页内容的HTML字符串,返回给浏览器去展示。

浏览器拿到了这个HTML之后,渲染出页面,但是并没有事件交互,这时候浏览器发现HTML中加载了一些js文件(也就是浏览器端渲染的js),就直接去加载。

加载好并执行完以后,事件就会被绑定上了。这时候页面被浏览器端接管了。也就是到了我们熟悉的js渲染页面的过程。

需要实现的目标:

  • React组件服务端渲染
  • 路由的服务端渲染
  • 保证服务端和浏览器的数据唯一
  • css的服务端渲染(样式直出)

一般的渲染方式

  • 服务端渲染:服务端生成html字符串,发送给浏览器进行渲染。
  • 浏览器端渲染:服务端返回空的html文件,内部加载js完全由js与css,由js完成页面的渲染

优点与缺点

服务端渲染解决了首屏加载速度慢以及seo不友好的缺点(Google已经可以检索到浏览器渲染的网页,但不是所有搜索引擎都可以)但增加了项目的复杂程度,提高维护成本。如果非必须,尽量不要用服务端渲染//在此我向大家推荐一个前端全栈开发交流圈:619586920 突破技术瓶颈,提升思维能力整体思路

需要两个端:服务端、浏览器端(浏览器渲染的部分)

第一: 打包浏览器端代码

第二: 打包服务端代码并启动服务

第三: 用户访问,服务端读取浏览器端打包好的index.html文件为字符串,将渲染好的组件、样式、数据塞入html字符串,返回给浏览器

第四: 浏览器直接渲染接收到的html内容,并且加载打包好的浏览器端js文件,进行事件绑定,初始化状态数据,完成同构

React组件的服务端渲染

让我们来看一个最简单的React服务端渲染的过程。要进行服务端渲染的话那必然得需要一个根组件,来负责生成HTML结构

import React from 'react';import ReactDOM from 'react-dom'; ReactDOM.hydrate(<Container />, document.getElementById('root'));

当然这里用ReactDOM.render也是可以的,只不过hydrate会尽量复用接收到的服务端返回的内容,来补充事件绑定和浏览器端其他特有的过程引入浏览器端需要渲染的根组件,利用react的 renderToString API进行渲染

import { renderToString } from 'react-dom/server'import Container from '../containers'// 产生htmlconst content = renderToString(<Container/>)const html = `  <html>   <body>${content}</body>  </html>res.send(html)

在这里,renderToString也可以替换成renderToNodeStream,区别在于前者是同步地产生HTML,也就是如果生成HTML用了1000毫秒,那么就会在1000毫秒之后才将内容返回给浏览器,显然耗时过长。而后者则是以流的形式,将渲染结果塞给response对象,就是出来多少就返回给浏览器多少,可以相对减少耗时//在此我向大家推荐一个前端全栈开发交流圈:619586920 突破技术瓶颈,提升思维能力路由的服务端渲染

一般场景下,我们的应用不可能只有一个页面,肯定会有路由跳转。我们一般这么用:

import { BrowserRouter, Route } from 'react-router-dom'const App = () => (  <BrowserRouter>    {/*...Routes*/}  <BrowserRouter/>)

但这是浏览器端渲染时候的用法。在做服务端渲染时,需要使用将BrowserRouter 替换为 StaticRouter区别在于,BrowserRouter 会通过HTML5 提供的 history API来保持页面与URL的同步,而StaticRouter则不会改变URL

import { createServer } from 'http'import { StaticRouter } from 'react-router-dom'createServer((req, res) => {  const html = renderToString(    <StaticRouter      location={req.url}      context={{}}    >      <Container />    <StaticRouter/>) })

这里,StaticRouter要接收两个属性:location: StaticRouter 会根据这个属性,自动匹配对应的React组件,所以才会实现刷新页面,服务端返回的对应路由的组与浏览器端保持一致

context: 一般用来传递一些数据,相当于一个载体,之后讲到样式的服务端渲染的时候会用到

Redux同构

数据的预获取以及脱水与注水我认为是服务端渲染的难点。这是什么意思呢?也就是说首屏渲染的网页一般要去请求外部数据,我们希望在生成HTML之前,去获取到这个页面需要的所有数据,然后塞到页面中去,这个过程,叫做“脱水”(Dehydrate),生成HTML返回给浏览器。浏览器拿到带着数据的HTML,去请求浏览器端js,接管页面,用这个数据来初始化组件。这个过程叫“注水”(Hydrate)。完成服务端与浏览器端数据的统一。//在此我向大家推荐一个前端全栈开发交流圈:619586920 突破技术瓶颈,提升思维能力为什么要这么做呢?试想一下,假设没有数据的预获取,直接返回一个没有数据,只有固定内容的HTML结构,会有什么结果呢?

第一:由于页面内没有有效信息,不利于SEO。

第二:由于返回的页面没有内容,但浏览器端JS接管页面后回去请求数据、渲染数据,页面会闪一下,用户体验不好。

我们使用Redux来管理状态,因为有服务端代码和浏览器端代码,那么就分别需要两个store来管理服务端和浏览器端的数据。

组件的配置

组件要在服务端渲染的时候去请求数据,可以在组件上挂载一个专门发异步请求的方法,这里叫做loadData,接收服务端的store作为参数,然后store.dispatch去扩充服务端的store。

class Home extends React.Component {  componentDidMount() {    this.props.callApi()  }  render() {    return <div>{this.props.state.name}</div>  }}Home.loadData = store => { return store.dispatch(callApi())}const mapState = state => stateconst mapDispatch = {callApi}export default connect(mapState, mapDispatch)(Home)

路由的改造

因为服务端要根据路由判断当前渲染哪个组件,可以在这个时候发送异步请求。所以路由也需要配置一下来支持loadData方法。服务端渲染的时候,路由的渲染可以使用react-router-config这个库,用法如下(重点关注在路由上挂载loadData方法):

import { BrowserRouter } from 'react-router-dom'import { renderRoutes } from 'react-router-config'import Home from './Home'export const routes = [ {  path: '/',  component: Home,  loadData: Home.loadData,  exact: true, }]const Routers = <BrowserRouter>  {renderRoutes(routes)}<BrowserRouter/>

服务端获取数据

到了服务端,需要判断匹配的路由内的所有组件各自都有没有loadData方法,有就去调用,传入服务端的store,去扩充服务端的store。同时还要注意到,一个页面可能是由多个组件组成的,会发各自的请求,也就意味着我们要等所有的请求都发完,再去返回HTML。

import express from 'express'import serverRender from './render'import { matchRoutes } from 'react-router-config'import { routes } from '../routes'import serverStore from "../store/serverStore" const app = express()app.get('*', (req, res) => { const context = {css: []} const store = serverStore() // 用matchRoutes方法获取匹配到的路由对应的组件数组 const matchedRoutes = matchRoutes(routes, req.path) const promises = [] for (const item of matchedRoutes) {  if (item.route.loadData) {   const promise = new Promise((resolve, reject) => {    item.route.loadData(store).then(resolve).catch(resolve)   })   promises.push(promise)  } } // 所有请求响应完毕,将被HTML内容发送给浏览器 Promise.all(promises).then(() => {  // 将生成html内容的逻辑封装成了一个函数,接收req, store, context  res.send(serverRender(req, store, context)) })})

细心的同学可能注意到了上边我把每个loadData都包了一个promise。

const promise = new Promise((resolve, reject) => { item.route.loadData(store).then(resolve).catch(resolve) console.log(item.route.loadData(store));})//在此我向大家推荐一个前端全栈开发交流圈:619586920 突破技术瓶颈,提升思维能力promises.push(promise)

这是为了容错,一旦有一个请求出错,那么下边Promise.all方法则不会执行,所以包一层promise的目的是即使请求出错,也会resolve,不会影响到Promise.all方法,也就是说只有请求出错的组件会没数据,而其他组件不会受影响。

注入数据

我们请求已经发出去了,并且在组件的loadData方法中也扩充了服务端的store,那么可以从服务端的数据取出来注入到要返回给浏览器的HTML中了。来看 serverRender 方法

const serverRender = (req, store, context) => { // 读取客户端生成的HTML const template = fs.readFileSync(process.cwd() + '/public/static/index.html', 'utf8') const content = renderToString(  <Provider store={store}>   <StaticRouter location={req.path} context={context}>    <Container/>   </StaticRouter>  </Provider> ) // 注入数据 const initialState = `<script>  window.context = {   INITIAL_STATE: ${JSON.stringify(store.getState())}  }</script>` return template.replace('<!--app-->', content)  .replace('<!--initial-state-->', initialState)}

浏览器端用服务端获取到的数据初始化store

经过上边的过程,我们已经可以从window.context中拿到服务端预获取的数据了,此时需要做的事就是用这份数据去初始化浏览器端的store。保证两端数据的统一。

import { createStore, applyMiddleware, compose } from 'redux'import thunk from 'redux-thunk'import rootReducer from '../reducers' const defaultStore = window.context && window.context.INITIAL_STATEconst clientStore = createStore( rootReducer, defaultStore,// 利用服务端的数据初始化浏览器端的store compose(  applyMiddleware(thunk),  window.devToolsExtension ? window.devToolsExtension() : f=>f ))

至此,服务端渲染的数据统一问题就解决了,再来回顾一下整个流程:

  • 用户访问路由,服务端根据路由匹配出对应路由内的组件数组
  • 循环数组,调用组件上挂载的loadData方法,发送请求,扩充服务端store
  • 所有请求完成后,通过store.getState,获取到服务端预获取的数据,注入到window.context中
  • 浏览器渲染返回的HTML,加载浏览器端js,从window.context中取数据来初始化浏览器端的store,渲染组件

这里还有个点,也就是当我们从路由进入到其他页面的时候,组件内的loadData方法并不会执行,它只会在刷新,服务端渲染路由的时候执行。

这时候会没有数据。所以我们还需要在componentDidMount中去发请求,来解决这个问题。因为componentDidMount不会在服务端渲染执行,所以不用担心请求重复发送。

样式的服务端渲染

以上我们所做的事情只是让网页的内容经过了服务端的渲染,但是样式要在浏览器加载css后才会加上,所以最开始返回的网页内容没有样式,页面依然会闪一下。为了解决这个问题,我们需要让样式也一并在服务端渲染的时候返回。

首先,服务端渲染的时候,解析css文件,不能使用style-loader了,要使用isomorphic-style-loader。

{  test: /\.css$/,  use: [    'isomorphic-style-loader',    'css-loader',    'postcss-loader'  ],}

但是,如何在服务端获取到当前路由内的组件样式呢?回想一下,我们在做路由的服务端渲染时,用到了StaticRouter,它会接收一个context对象,这个context对象可以作为一个载体来传递一些信息。我们就用它!思路就是在渲染组件的时候,在组件内接收context对象,获取组件样式,放到context中,服务端拿到样式,插入到返回的HTML中的style标签中。来看看组件是如何读取样式的吧:

import style from './style/index.css'class Index extends React.Component {  componentWillMount() {   if (this.props.staticContext) {    const css = styles._getCss()    this.props.staticContext.css.push(css)   }  }}

在路由内的组件可以在props里接收到staticContext,也就是通过StaticRouter传递过来的context,isomorphic-style-loader 提供了一个 _getCss() 方法,让我们能读取到css样式,然后放到staticContext里。不在路由之内的组件,可以通过父级组件,传递props的方法,或者用react-router的withRouter包裹一下其实这部分提取css的逻辑可以写成高阶组件,这样就可以做到复用了

import React, { Component } from 'react' export default (DecoratedComponent, styles) => { return class NewComponent extends Component {  componentWillMount() {   if (this.props.staticContext) {    const css = styles._getCss()    this.props.staticContext.css.push(css)   }  }  render() {   return <DecoratedComponent {...this.props}/>  } }}

在服务端,经过组件的渲染之后,context中已经有内容了,我们这时候把样式处理一下,返回给浏览器,就可以做到样式的服务端渲染了

const serverRender = (req, store) => { const context = {css: []} const template = fs.readFileSync(process.cwd() + '/public/static/index.html', 'utf8') const content = renderToString(  <Provider store={store}>   <StaticRouter location={req.path} context={context}>    <Container/>   </StaticRouter>  </Provider> ) // 经过渲染之后,context.css内已经有了样式 const cssStr = context.css.length ? context.css.join('\n') : '' const initialState = `<script>  window.context = {   INITIAL_STATE: ${JSON.stringify(store.getState())}  }</script>` return template.replace('<!--app-->', content)  .replace('server-render-css', cssStr)  .replace('<!--initial-state-->', initialState)}

至此,服务端渲染就全部完成了。

React的服务端渲染,最好的解决方案就是Next.js。如果你的应用没有SEO优化的需求,又或者不太注重首屏渲染的速度,那么尽量就不要用服务端渲染。

因为会让项目变得复杂。此外,除了服务端渲染,SEO优化的办法还有很多,比如预渲染(pre-render)

redis哨兵模式

mumupudding阅读(12)


Redis sentinel介绍

Redis Sentinel是Redis高可用的实现方案。Sentinel是一个管理多个Redis实例的工具,它可以实现对Redis的监控、通知、自动故障转移。

Redis Sentinel的主要功能

Sentinel的主要功能包括主节点存活检测、主从运行情况检测、自动故障转移(failover)、主从切换。Redis的Sentinel最小配置是一主一从。 Redis的Sentinel系统可以用来管理多个Redis服务器,该系统可以执行以下四个任务:

  • 监控

    Sentinel会不断的检查主服务器和从服务器是否正常运行。

  • 通知

    当被监控的某个Redis服务器出现问题,Sentinel通过API脚本向管理员或者其他的应用程序发送通知。

  • 自动故障转移

    当主节点不能正常工作时,Sentinel会开始一次自动的故障转移操作,它会将与失效主节点是主从关系的其中一个从节点升级为新的主节点, 并且将其他的从节点指向新的主节点。

  • 配置提供者

    在Redis Sentinel模式下,客户端应用在初始化时连接的是Sentinel节点集合,从中获取主节点的信息。

Redis Sentinel的工作流程

参考链接(含图):http://www.cnblogs.com/jifeng/p/5138961.html

Sentinel负责监控集群中的所有主、从Redis,当发现主故障时,Sentinel会在所有的从中选一个成为新的主。并且会把其余的从变为新主的从。同时那台有问题的旧主也会变为新主的从,也就是说当旧的主即使恢复时,并不会恢复原来的主身份,而是作为新主的一个从。在Redis高可用架构中,Sentinel往往不是只有一个,而是有3个或者以上。目的是为了让其更加可靠,毕竟主和从切换角色这个过程还是蛮复杂的。

相关概念

  • 主观失效

    SDOWN(subjectively down),直接翻译的为”主观”失效,即当前sentinel实例认为某个redis服务为”不可用”状态.

  • 客观失效

    ODOWN(objectively down),直接翻译为”客观”失效,即多个sentinel实例都认为master处于”SDOWN”状态,那么此时master将处于ODOWN,ODOWN可以简单理解为master已经被集群确定为”不可用”,将会开启failover

环境准备

准备3台机器,其中每台机器上都有两个角色,分配如下:

主机名 IP:Port 角色
wangzb01 192.168.153.133:6379 Redis Master
wangzb02 192.168.153.134:6379 Redis Slave1
wangzb03 192.168.153.135:6379 Redis Slave2
wangzb01 192.168.153.133:26379 Sentinel1
wangzb02 192.168.153.134:26379 Sentinel2
wangzb03 192.168.153.135:26379 Sentinel3

部署

安装Redis

步骤略

部署Redis主从

步骤略

部署Sentinel

三台Sentinel配置文件是一样的,编辑配置文件

vi /etc/sentinel.conf #内容如下

# 端口port 26379# 是否后台启动daemonize yes# pid文件路径pidfile /var/run/redis-sentinel.pid# 日志文件路径logfile "/var/log/sentinel.log"# 定义工作目录dir /tmp# 定义Redis主的别名, IP, 端口,这里的2指的是需要至少2个Sentinel认为主Redis挂了才最终会采取下一步行为sentinel monitor mymaster 127.0.0.1 6379 2# 如果mymaster 30秒内没有响应,则认为其主观失效sentinel down-after-milliseconds mymaster 30000# 如果master重新选出来后,其它slave节点能同时并行从新master同步数据的台数有多少个,显然该值越大,所有slave节##点完成同步切换的整体速度越快,但如果此时正好有人在访问这些slave,可能造成读取失败,影响面会更广。最保守的设置##为1,同一时间,只能有一台干这件事,这样其它slave还能继续服务,但是所有slave全部完成缓存更新同步的进程将变慢。sentinel parallel-syncs mymaster 1# 该参数指定一个时间段,在该时间段内没有实现故障转移成功,则会再一次发起故障转移的操作,单位毫秒sentinel failover-timeout mymaster 180000# 不允许使用SENTINEL SET设置notification-script和client-reconfig-script。sentinel deny-scripts-reconfig yes

启动服务

启动顺序:主Redis -> 从Redis -> Sentinel1/2/3

Sentinel 启动命令

redis-sentinel /etc/sentinel.conf 

Sentinel操作

  • sentinel master mymaster

    输出被监控的主节点的状态信息

  • sentinel slaves mymaster

    查看mymaster的从信息

  • sentinel sentinels mymaster

    查看其他Sentinel信息

测试

停止Redis从

停止Redis主

停止sentinel1

客户端连接问题

使用sentinel后,客户端(如,php)如何连Redis呢?

参考:https://blog.51cto.com/chenql/1958910

Mybatis中SqlNode的组合模式

mumupudding阅读(18)

组合( Composite )模式就是把对象组合成树形结构,以表示“部分-整体”的层次结构,用户可以像处理一个简单对象一样来处理一个复杂对象,从而使得调用者无需了解复杂元素的内部结构。

组合模式中的角色有:

  • 抽象组件(容器):定义了树形结构中所有类的公共行为,例如add(),remove()等方法。
  • 树叶:最终实现类,没有子类。
  • 树枝:有子类的管理类,并通过管理方法调用其管理的子类的相关操作。
  • 调用者:通过容器接口操作整个树形结构。

具体组合模式的例子可以参考 设计模式整理

现在我们来说一下SqlNode是什么,来看这么一段配置文件

<select id="findByGameTypeCount" resultType="java.lang.Long">
   select count(*)
   from betdetails a inner join UserBetOrder b on a.orderId = b.id
   <where>
      <if test="gameType != null and gameType > 0">
         a.gameType = #{gameType} and
      </if>
      <if test="currDrawno != null">
         b.currentDrawno = #{currDrawno} and
      </if>
      <if test="orderId != null and orderId > 0">
         a.orderId = #{orderId} and
      </if>
      <if test="status != null and status >= 0">
         a.status = #{status} and
      </if>
      <if test="userId != null and userId > 0">
         b.userId = #{userId} and
      </if>
      <if test="start != null">
         a.createTime &gt;= #{start} and
      </if>
      <if test="end != null">
         a.createTime &lt;= #{end} and
      </if>
      1 = 1
   </where>
</select>
<insert id="insertBetdetailsByBatch" parameterType="java.util.List">
   insert into betdetails(id,orderId,actorIndex,createTime,ballIndex,ballValue,betAmount,rate1,rate2,rate3,gameType,status,betResult,awardAmount,ballName) values
   <foreach collection="list" item="item" index="index" separator=",">
      (#{item.id},#{item.orderId},#{item.actorIndex},#{item.createTime},#{item.ballIndex},#{item.ballValue},#{item.betAmount},#{item.rate1},#{item.rate2},#{item.rate3},#{item.gameType},#{item.status},#{item.betResult},#{item.awardAmount},#{item.ballName})
   </foreach>
</insert>

这其中的<if><where><foreach>节点就是SqlNode节点,SqlNode是一个接口,代表着组合模式中的容器。只要是有SqlNode,那就代表着一定是一个动态的SQL,里面就有可能会有参数#{}

public interface SqlNode {
  //SqlNode接口中定义的唯一方法,该方法会根据用户传入的实参,解析该SqlNode所记录的动态SQL节点,并调用DynamicContext.appendSql()方法将解析后的SQL片段追加到
  //DynamicContext.sqlBuilder中保存
  //当SQL节点下的所有SqlNode完成解析后,就可以从DynamicContext中获取一条动态生成的完整的SQL语句
  boolean apply(DynamicContext context);
}

我们先来看一下DynamicContext是什么,它的核心字段如下

private final ContextMap bindings; //参考上下文
//在SqlNode解析动态SQL时,会将解析后的SQL语句片段添加到该属性中保存,最终拼凑出一条完成的SQL语句
private final StringBuilder sqlBuilder = new StringBuilder();

ContextMap是一个内部类,继承于HashMap,重写了get方法

static class ContextMap extends HashMap<String, Object> {
  private static final long serialVersionUID = 2977601501966151582L;
  //将用户传入的参数封装成MetaObject对象(类实例中检查类的属性是否包含getter,setter方法)
  private MetaObject parameterMetaObject;
  public ContextMap(MetaObject parameterMetaObject) {
    this.parameterMetaObject = parameterMetaObject;
  }

  @Override
  public Object get(Object key) {
    String strKey = (String) key;
    //如果ContextMap中已经包含了该key,则直接返回
    if (super.containsKey(strKey)) {
      return super.get(strKey);
    }
    //如果不包含该key,从parameterMetaObject中查找对应属性
    if (parameterMetaObject != null) {
      // issue #61 do not modify the context when reading
      return parameterMetaObject.getValue(strKey);
    }

    return null;
  }
}
public void appendSql(String sql) {
  sqlBuilder.append(sql);
  sqlBuilder.append(" ");
}

SqlNode的实现类如下

Mybatis中SqlNode的组合模式

其中MixedSqlNode是树枝,TextSqlNode是树叶….

我们先来看一下TextSqlNode,TextSqlNode表示的是包含${}占位符的动态SQL节点。它的接口实现方法如下

@Override
public boolean apply(DynamicContext context) {
  //将动态SQL(带${}占位符的SQL)解析成完成SQL语句的解析器,即将${}占位符替换成实际的变量值
  GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
  //将解析后的SQL片段添加到DynamicContext中
  context.appendSql(parser.parse(text));
  return true;
}

BindingTokenParser是TextNode中定义的内部类,继承了TokenHandler接口,它的主要作用是根据DynamicContext.bindings集合中的信息解析SQL语句节点中的${}占位符。

private DynamicContext context;
private Pattern injectionFilter; //需要匹配的正则表达式
@Override
public String handleToken(String content) {
  //获取用户提供的实参
  Object parameter = context.getBindings().get("_parameter");
  //如果实参为null
  if (parameter == null) {
    //将参考上下文的value key设为null
    context.getBindings().put("value", null);
    //如果实参是一个常用数据类型的类(Integer.class,String.class,Byte.class等等)
  } else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
    //将参考上下文的value key设为该实参
    context.getBindings().put("value", parameter);
  }
  //通过OGNL解析参考上下文的值
  Object value = OgnlCache.getValue(content, context.getBindings());
  String srtValue = (value == null ? "" : String.valueOf(value)); // issue #274 return "" instead of "null"
  //检测合法性
  checkInjection(srtValue);
  return srtValue;
}
private void checkInjection(String value) {
  if (injectionFilter != null && !injectionFilter.matcher(value).matches()) {
    throw new ScriptingException("Invalid input. Please conform to regex" + injectionFilter.pattern());
  }
}

在OgnlCache中,对原生的OGNL进行了封装。OGNL表达式的解析过程是比较耗时的,为了提高效率,OgnlCache中使用了expressionCashe字段(ConcurrentHashMap<String,Object>类型)对解析后的OGNL表达式进行缓存。为了说明OGNL,我们先来看一个例子

@Data
@ToString
public class User {
    private int id;
    private String name;
}
public class OGNLDemo {
    public void testOgnl1() throws OgnlException {
        OgnlContext context = new OgnlContext();
        context.put("cn","China");
        String value = (String) context.get("cn");
        System.out.println(value);

        User user = new User();
        user.setId(100);
        user.setName("Jack");
        context.put("user",user);
        Object u = context.get("user");
        System.out.println(u);
        Object ognl = Ognl.parseExpression("#user.id");
        Object value1 = Ognl.getValue(ognl,context,context.getRoot());
        System.out.println(value1);

        User user1 = new User();
        user1.setId(200);
        user1.setName("Mark");
        context.setRoot(user1);
        Object ognl1 = Ognl.parseExpression("id");
        Object value2 = Ognl.getValue(ognl1,context,context.getRoot());
        System.out.println(value2);

        Object ognl2 = Ognl.parseExpression("@@floor(10.9)");
        Object value3 = Ognl.getValue(ognl2, context, context.getRoot());
        System.out.println(value3);
    }

    public static void main(String[] args) throws OgnlException {
        OGNLDemo demo = new OGNLDemo();
        demo.testOgnl1();
    }
}

运行结果:

China
User(id=100, name=Jack)
100
200
10.0

private static final Map<String, Object> expressionCache = new ConcurrentHashMap<String, Object>();
public static Object getValue(String expression, Object root) {
  try {
    //创建OgnlContext对象
    Map<Object, OgnlClassResolver> context = Ognl.createDefaultContext(root, new OgnlClassResolver());
    //使用OGNL执行expression表达式
    return Ognl.getValue(parseExpression(expression), context, root);
  } catch (OgnlException e) {
    throw new BuilderException("Error evaluating expression '" + expression + "'. Cause: " + e, e);
  }
}
private static Object parseExpression(String expression) throws OgnlException {
  //查找缓存
  Object node = expressionCache.get(expression);
  if (node == null) {
    //解析表达式
    node = Ognl.parseExpression(expression);
    //将表达式的解析结果添加到缓存中
    expressionCache.put(expression, node);
  }
  return node;
}

StaticTextSqlNode很简单,就是直接返回SQL语句

public class StaticTextSqlNode implements SqlNode {
  private final String text;

  public StaticTextSqlNode(String text) {
    this.text = text;
  }

  @Override
  public boolean apply(DynamicContext context) {
    context.appendSql(text);
    return true;
  }

}

IfSqlNode是解析<if>节点,字段含义如下

//用于解析<if>节点的test表达式的值
private final ExpressionEvaluator evaluator;
//记录<if>节点中test表达式
private final String test;
//记录了<if>节点的子节点
private final SqlNode contents;

接口方法如下

@Override
public boolean apply(DynamicContext context) {
  //检测test属性中记录的表达式
  if (evaluator.evaluateBoolean(test, context.getBindings())) {
    //如果test表达式为true,则执行子节点的apply()方法
    contents.apply(context);
    return true; //返回test表达式的结果为true
  }
  return false; //返回test表达式的结果为false
}

在ExpressionEvaluator中

public boolean evaluateBoolean(String expression, Object parameterObject) {
  //用OGNL解析expression表达式
  Object value = OgnlCache.getValue(expression, parameterObject);
  //处理Boolean类型
  if (value instanceof Boolean) {
    return (Boolean) value;
  }
  //处理数字类型
  if (value instanceof Number) {
    return new BigDecimal(String.valueOf(value)).compareTo(BigDecimal.ZERO) != 0;
  }
  return value != null;
}

TrimSqlNode会根据子节点的解析结果,添加或删除响应的前缀或后缀,比如有这么一段配置

<insert id="insertNotNullBetdetails" parameterType="com.cloud.model.game.Betdetails">
   insert into betdetails
   <trim prefix="(" suffix=")" suffixOverrides=",">
      <if test="id != null">id,</if>
      <if test="orderId != null">orderId,</if>
      <if test="actorIndex != null">actorIndex,</if>
      <if test="ballIndex != null">ballIndex,</if>
      <if test="ballValue != null">ballValue,</if>
      <if test="betAmount != null">betAmount,</if>
      <if test="createTime != null">createTime,</if>
      <if test="rate1 != null">rate1,</if>
      <if test="rate2 != null">rate2,</if>
      <if test="rate3 != null">rate3,</if>
      <if test="gameType != null">gameType,</if>
      <if test="status != null">status,</if>
      <if test="betResult != null">betResult,</if>
      <if test="awardAmount != null">awardAmount,</if>
      <if test="ballName != null">ballName,</if>
   </trim>
   <trim prefix="values (" suffix=")" suffixOverrides=",">
      <if test="id != null">#{id},</if>
      <if test="orderId != null">#{orderId},</if>
      <if test="actorIndex != null">#{actorIndex},</if>
      <if test="createTime != null">#{createTime},</if>
      <if test="ballIndex != null">#{ballIndex},</if>
      <if test="ballValue != null">#{ballValue},</if>
      <if test="betAmount != null">#{betAmount},</if>
      <if test="rate1 != null">#{rate1},</if>
      <if test="rate2 != null">#{rate2},</if>
      <if test="rate3 != null">#{rate3},</if>
      <if test="gameType != null">#{gameType},</if>
      <if test="status != null">#{status},</if>
      <if test="betResult != null">#{betResult},</if>
      <if test="awardAmount != null">#{awardAmount},</if>
      <if test="ballName != null">#{ballName},</if>
   </trim>
</insert>

TrimSqlNode中字段含义如下

private final SqlNode contents; //该<trim>节点的子节点
private final String prefix; //记录了前缀字符串(为<trim>节点包裹的SQL语句添加的前缀)
private final String suffix; //记录了后缀字符串(为<trim>节点包裹的SQL语句添加的后缀)
//如果<trim>节点包裹的SQL语句是空语句,删除指定的前缀,如where
private final List<String> prefixesToOverride;
//如果<trim>节点包裹的SQL语句是空语句,删除指定的后缀,如逗号
private final List<String> suffixesToOverride;

它的接口方法如下

@Override
public boolean apply(DynamicContext context) {
  //创建FilteredDynamicContext对象,FilteredDynamicContext是TrimSqlNode的内部类,继承于DynamicContext
  FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
  //调用子节点的apply()方法进行解析,注意收集SQL语句的是filteredDynamicContext
  boolean result = contents.apply(filteredDynamicContext);
  //处理前缀和后缀
  filteredDynamicContext.applyAll();
  return result;
}

FilteredDynamicContext的字段属性含义如下

private DynamicContext delegate; //底层封装的DynamicContext对象
private boolean prefixApplied; //是否已经处理过前缀
private boolean suffixApplied; //是否已经处理过后缀
private StringBuilder sqlBuffer; //用于记录子节点解析后的结果

FilteredDynamicContext的applyAll()方法

public void applyAll() {
  //获取子节点解析后的结果,并全部转化为大写
  sqlBuffer = new StringBuilder(sqlBuffer.toString().trim());
  String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH);
  if (trimmedUppercaseSql.length() > 0) {
    //处理前缀
    applyPrefix(sqlBuffer, trimmedUppercaseSql);
    //处理后缀
    applySuffix(sqlBuffer, trimmedUppercaseSql);
  }
  //将解析后的结果SQL片段添加到DynamicContext的StringBuilder中
  delegate.appendSql(sqlBuffer.toString());
}
private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) {
  if (!prefixApplied) { //如果还没有处理过前缀
    prefixApplied = true; //更新为已处理
    if (prefixesToOverride != null) { //如果需要删除的前缀列表不为null
      //遍历该前缀列表
      for (String toRemove : prefixesToOverride) {
        //如果<trim>子节点收集上来的SQL语句以该前缀开头
        if (trimmedUppercaseSql.startsWith(toRemove)) {
          //从<trim>子节点收集上来的StringBuilder中删除该前端
          sql.delete(0, toRemove.trim().length());
          break;
        }
      }
    }
    //如果有前缀字符串(比如说"("),将前缀字符串插入StringBuilder最前端
    if (prefix != null) {
      sql.insert(0, " ");
      sql.insert(0, prefix);
    }
  }
}
private void applySuffix(StringBuilder sql, String trimmedUppercaseSql) {
  if (!suffixApplied) { //如果还没有处理过后缀
    suffixApplied = true; //更新为已处理后缀
    if (suffixesToOverride != null) { //如果需要处理的后缀列表不为null
      //遍历该后缀列表
      for (String toRemove : suffixesToOverride) {
        //如果从<trim>子节点收集上来的SQL语句以该后缀结尾
        if (trimmedUppercaseSql.endsWith(toRemove) || trimmedUppercaseSql.endsWith(toRemove.trim())) {
          //获取该后缀的起始位置
          int start = sql.length() - toRemove.trim().length();
          //获取该后缀的终止位置
          int end = sql.length();
          //从<trim>子节点收集上来的StringBuilder中删除该后端
          sql.delete(start, end);
          break;
        }
      }
    }
    //如果有后缀字符串(比如说")"),将前缀字符串拼接上StringBuilder最后端
    if (suffix != null) {
      sql.append(" ");
      sql.append(suffix);
    }
  }
}

WhereSqlNode和SetSqlNode都继承于TrimSqlNode,他们只是在TrimSqlNode的属性中指定了固定的标记。

public class WhereSqlNode extends TrimSqlNode {

  private static List<String> prefixList = Arrays.asList("AND ","OR ","AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t");

  public WhereSqlNode(Configuration configuration, SqlNode contents) {
    super(configuration, contents, "WHERE", prefixList, null, null);
  }

}
public class SetSqlNode extends TrimSqlNode {

  private static List<String> suffixList = Arrays.asList(",");

  public SetSqlNode(Configuration configuration,SqlNode contents) {
    super(configuration, contents, "SET", null, null, suffixList);
  }

}

ForEachSqlNode,在动态SQL语句中,通常需要对一个集合进行迭代,Mybatis提供了<foreach>标签实现该功能。在使用<foreach>标签迭代集合时,不仅可以使用集合的元素和索引值,还可以在循环开始之前或结束之后添加指定的字符串,也允许在迭代过程中添加指定的分隔符。配置样例如下

<insert id="insertBetdetailsByBatch" parameterType="java.util.List">
   insert into betdetails(id,orderId,actorIndex,createTime,ballIndex,ballValue,betAmount,rate1,rate2,rate3,gameType,status,betResult,awardAmount,ballName) values
   <foreach collection="list" item="item" index="index" separator=",">
      (#{item.id},#{item.orderId},#{item.actorIndex},#{item.createTime},#{item.ballIndex},#{item.ballValue},#{item.betAmount},#{item.rate1},#{item.rate2},#{item.rate3},#{item.gameType},#{item.status},#{item.betResult},#{item.awardAmount},#{item.ballName})
   </foreach>
</insert>

ForEachSqlNode中各个字段含义如下:

private final ExpressionEvaluator evaluator;
private final String collectionExpression;
private final SqlNode contents;
private final String open;
private final String close;
private final String separator;
private final String item;
private final String index;
private final Configuration configuration;

MySQL数据库优化

mumupudding阅读(8)

前言

数据库优化一方面是找出系统的瓶颈,提高MySQL数据库的整体性能,而另一方面需要合理的结构设计和参数调整,以提高用户的相应速度,同时还要尽可能的节约系统资源,以便让系统提供更大的负荷.

1. 优化一览图

2. 优化

笔者将优化分为了两大类,软优化和硬优化,软优化一般是操作数据库即可,而硬优化则是操作服务器硬件及参数设置.

2.1 软优化

2.1.1 查询语句优化

1.首先我们可以用EXPLAIN或DESCRIBE(简写:DESC)命令分析一条查询语句的执行信息.
2.例:

DESC SELECT * FROM `user`

显示:

其中会显示索引和查询数据读取数据条数等信息.

2.1.2 优化子查询

在MySQL中,尽量使用JOIN来代替子查询.因为子查询需要嵌套查询,嵌套查询时会建立一张临时表,临时表的建立和删除都会有较大的系统开销,而连接查询不会创建临时表,因此效率比嵌套子查询高.

2.1.3 使用索引

索引是提高数据库查询速度最重要的方法之一,关于索引可以参高笔者<MySQL数据库索引>一文,介绍比较详细,此处记录使用索引的三大注意事项:

  1. LIKE关键字匹配’%’开头的字符串,不会使用索引.
  2. OR关键字的两个字段必须都是用了索引,该查询才会使用索引.
  3. 使用多列索引必须满足最左匹配.

2.1.4 分解表

对于字段较多的表,如果某些字段使用频率较低,此时应当,将其分离出来从而形成新的表,

2.1.5 中间表

对于将大量连接查询的表可以创建中间表,从而减少在查询时造成的连接耗时.

2.1.6 增加冗余字段

类似于创建中间表,增加冗余也是为了减少连接查询.

2.1.7 分析表,,检查表,优化表

分析表主要是分析表中关键字的分布,检查表主要是检查表中是否存在错误,优化表主要是消除删除或更新造成的表空间浪费.

1. 分析表: 使用 ANALYZE 关键字,如ANALYZE TABLE user;

  1. Op:表示执行的操作.
  2. Msg_type:信息类型,有status,info,note,warning,error.
  3. Msg_text:显示信息.

2. 检查表: 使用 CHECK关键字,如CHECK TABLE user [option]

option 只对MyISAM有效,共五个参数值:

  1. QUICK:不扫描行,不检查错误的连接.
  2. FAST:只检查没有正确关闭的表.
  3. CHANGED:只检查上次检查后被更改的表和没被正确关闭的表.
  4. MEDIUM:扫描行,以验证被删除的连接是有效的,也可以计算各行关键字校验和.
  5. EXTENDED:最全面的的检查,对每行关键字全面查找.

3. 优化表:使用OPTIMIZE关键字,如OPTIMIZE [LOCAL|NO_WRITE_TO_BINLOG] TABLE user;

LOCAL|NO_WRITE_TO_BINLOG都是表示不写入日志.,优化表只对VARCHAR,BLOB和TEXT有效,通过OPTIMIZE TABLE语句可以消除文件碎片,在执行过程中会加上只读锁.

2.2 硬优化

2.2.1 硬件三件套

1.配置多核心和频率高的cpu,多核心可以执行多个线程.
2.配置大内存,提高内存,即可提高缓存区容量,因此能减少磁盘I/O时间,从而提高响应速度.
3.配置高速磁盘或合理分布磁盘:高速磁盘提高I/O,分布磁盘能提高并行操作的能力.

2.2.2 优化数据库参数

优化数据库参数可以提高资源利用率,从而提高MySQL服务器性能.MySQL服务的配置参数都在my.cnf或my.ini,下面列出性能影响较大的几个参数.

  • key_buffer_size:索引缓冲区大小
  • table_cache:能同时打开表的个数
  • query_cache_size和query_cache_type:前者是查询缓冲区大小,后者是前面参数的开关,0表示不使用缓冲区,1表示使用缓冲区,但可以在查询中使用SQL_NO_CACHE表示不要使用缓冲区,2表示在查询中明确指出使用缓冲区才用缓冲区,即SQL_CACHE.
  • sort_buffer_size:排序缓冲区

传送门:更多参数

2.2.3 分库分表

因为数据库压力过大,首先一个问题就是高峰期系统性能可能会降低,因为数据库负载过高对性能会有影响。另外一个,压力过大把你的数据库给搞挂了怎么办?所以此时你必须得对系统做分库分表 + 读写分离,也就是把一个库拆分为多个库,部署在多个数据库服务上,这时作为主库承载写入请求。然后每个主库都挂载至少一个从库,由从库来承载读请求。

2.2.4 缓存集群

如果用户量越来越大,此时你可以不停的加机器,比如说系统层面不停加机器,就可以承载更高的并发请求。然后数据库层面如果写入并发越来越高,就扩容加数据库服务器,通过分库分表是可以支持扩容机器的,如果数据库层面的读并发越来越高,就扩容加更多的从库。但是这里有一个很大的问题:数据库其实本身不是用来承载高并发请求的,所以通常来说,数据库单机每秒承载的并发就在几千的数量级,而且数据库使用的机器都是比较高配置,比较昂贵的机器,成本很高。如果你就是简单的不停的加机器,其实是不对的。所以在高并发架构里通常都有缓存这个环节,缓存系统的设计就是为了承载高并发而生。所以单机承载的并发量都在每秒几万,甚至每秒数十万,对高并发的承载能力比数据库系统要高出一到两个数量级。所以你完全可以根据系统的业务特性,对那种写少读多的请求,引入缓存集群。具体来说,就是在写数据库的时候同时写一份数据到缓存集群里,然后用缓存集群来承载大部分的读请求。这样的话,通过缓存集群,就可以用更少的机器资源承载更高的并发。

结语

一个完整而复杂的高并发系统架构中,一定会包含:各种复杂的自研基础架构系统。各种精妙的架构设计.因此一篇小文顶多具有抛砖引玉的效果,但是数据库优化的思想差不多就这些了.

mongoDB进阶

mumupudding阅读(11)

通过配置文件启动mongo服务器

参数 含义
–dbpath 指定数据库文件存放的目录
–port 端口默认是27017
–fork 以后台守护的方式进行启动
–logpath 指定日志文件输出路径
–config 指定一个配置文件
–auth 以安全方式启动数据库,需要验证账号和密码

直接在命令行中通过mongod --dbpath ...这样启动服务器,如果参数太多的话,就比较麻烦

So,我们可以选择通过运行配置文件的方式启动服务器

首先,要在一个目录下创建一个mongo.config(后缀名无所谓)

注意: log文件会自动生成,但data目录必须优先创建好

//mongo.config文件

dbpath=E:\mongouse\data
#数据库日志存放目录
logpath=E:\mongouse\log
#以追加的方式记录日志
logappend = true
#端口号 默认为27017
port=27017 
#以后台方式运行进程
fork=true 
#开启用户认证
auth=false
#关闭http接口,默认关闭http端口访问
nohttpinterface=true
#mongodb所绑定的ip地址
bind_ip = 127.0.0.1 
#启用日志文件,默认启用
journal=true 
#这个选项可以过滤掉一些无用的日志信息,若需要调试使用请设置为false
quiet=true 

然后在命令行中输入

mongod --config mongo.config

导入导出数据

  • mongoimport 导入
  • mongoexport 导出
参数 含义
-h[–host] 链接的数据库
–port 端口号
-u 用户名
-p 密码
-d 指定哪个数据库
-c 指定导出的集合
-o 导出的路径
-q 进行过滤的

方法一

mongoexport与导出数据库

mongoexport -d school -c students -o ./stu.bak

导出的是一个文件

整个文件是一个json

mongoimport与导入数据库

mongoimport -h 127.0.0.1 --port 27017 -d school -c students --file stu.bak

默认-h-p--file都可以省略

方法二

mongodump与导出数据库

和上面的区别在于不会转换(上面的会转换成json),适用于数据库中存在二进制数据的情况(二进制是转换不成json的)

导出整个数据库

mongodump -o mdmp

导出其中一个数据库

mongodump -d school -o school.dmp

导出来的样子

(school.dmp文件夹下有一个school文件)

mongorestore与导入数据库

如果想要导入整个数据库

mongorestore mdmp

如果只想导入其中一个数据库

mongorestore -d school mdmp/school

方法三:直接拷贝数据

将整个文件夹拷贝到指定目录下,然后在启动数据库服务器时将其指定为--dbpath

锁定和解锁数据库

强制将缓存区中的数据真正写入后锁住数据库

必须在admin数据库中使用命令

db.runCommand({fsync:1,lock:1}); //类似于node中的fs.fsync

解锁

db.fsyncUnlock();

示例:

打开一个命令行,先锁住

再打开一个命令行,像数据库中写入,会发现

迟迟不返回,说明正在等待写入

但当我们解锁

会发现原本正在等待的数据已经写入

安全措施

  • 物理隔离:电都不插
  • 网络隔离:区域网
  • 防火墙(IP/IP段/白名单/黑名单)
  • 用户名和密码验证

用户管理

要使用户生效,需要在启动服务器时加上--auth

mongod ... --auth

这样我们就不能裸连数据库了,必须要使用账号登录。

查看角色

show roles;

内置角色

  • 数据库用户角色:read、readWrite;
  • 数据库管理角色:dbAdmin、dbOwner、userAdmin
  • 集群管理角色:clusterAdmin、clusterManager、clusterMonitor、hostManage;
  • 备份恢复角色:backup、restore
  • 所有数据库角色:readAnyDatabase、readWriteAnyDatabase、userAdminAnyDatabase、dbAdminAnyDatabase
  • 超级用户角色:root
  • 内部角色:_system

用户的操作都需要在admin下面进行操作

如果在某个数据库下面执行操作,那么只对当前数据库生效

addUser已经废弃,默认会创建root用户,不安全,不再建议使用

创建用户

针对school数据库可以读

db.createUser({user:'ahhh',pwd:'123',roles:[{db:'school',role:'read'}]});

roles中不加db表示对所有数据库都有权限

显示用户权限

use admin;

var r = db.runCommand({usersInfo:'ahhh',showPrivileges:true})

printjson(r);

修改密码

修改密码

db.changeUserPassword({'ahhh','123456'});

验证密码是否正确

db.auth('ahhh','123456')

添加个人信息

db.runCommand({updateUser:'ahhh',pwd:'123',customData:{name:'ahuang',age:111,telephone:'123123xxx'}});

高级命令

首先runCommand中的参数是一个文档(JSON),但在runCommand中它是具有特殊意义的一些字段。

load(”)

路径分隔符必须使用/而不是\

D:\WEB\database\data //-->错误的

D:/WEB/database/data //-->正确的

group:分组

有以下数据

var stus = [
  {province:'北京',home:'北京',age:1}
  ,{province:'北京',home:'北京',age:2}
  ,{province:'北京',home:'北京',age:3}
  ,{province:'广东',home:'广州',age:1}
  ,{province:'广东',home:'佛山',age:2}
  ,{province:'广东',home:'东莞',age:3}   
]

我们这样执行命令进行分组

db.runCommand({
  group:{
    ns:'students' //namespace
    ,key:{home:1} //按照哪个key分组 可以写很多个
    ,query:{age:{$gt:1}} //满足条件才参与分组
    ,initial:{total:0} //每一组的初始值
    ,$reduce:function(doc,initial){
      initial.total += doc.age; //每个文档累加一次 最终会得到该分组下所有年龄的总和
    }
  }
});

分组结果

//retval:返回值类型说明

{
    "retval" : [
        {
            "home" : "北京",
            "total" : 5
        },
        {
            "home" : "佛山",
            "total" : 2
        },
        {
            "home" : "东莞",
            "total" : 3
        }
    ],
    "count" : NumberLong(4),
    "keys" : NumberLong(3),
    "ok" : 1
}

distinct:查找不重复的key值

以下会在students集合下查找所有key为home的值

db.runCommand({distinct:'students',key:'home'});

返回是这样的

{ "values" : [ "北京", "广州", "佛山", "东莞" ], "ok" : 1 }

执行命令时也可以这样执行

db.runCommand({distinct:'students',key:'home'}).values;

这样能直接得到key的值组成的数组

 [ "北京", "广州", "佛山", "东莞" ]

drop

删除集合除了db.xxx.drop(),也可以

db.runCommand({drop:'students'});

其它

查看数据库信息

db.runCommand({buildInfo:1}); 

查看students集合下上一次的执行错误信息

db.runCommand({getLastError:'students'}); 

固定集合

有着固定大小的集合,满了以后会覆盖掉最先插入的。(先入先出)

特性

  • 没有索引
  • 插入和查询速度非常快,不需要重新分配空间
  • 特别适合存储日志

创建固定集合

  • size单位是kb
  • max单位是
  • capped:是否有上限封顶,必须为true
db.createCollection('logs',{size:5,max:5,capped:true})

非固定集合转换为固定集合

db.runCommand({convertToCapped:'logs',size:5})

gridfs:网格文件存储系统

gridfs是mongodb自带的文件系统,使用二进制存储文件。

mongodb可以以BSON格式保存二进制对象,但是BSON对象的体积不能超过4M。所以mongodb提供了mongofiles。它可以把一个大文件透明地分割成小文件(256k),从而保存大体积的数据

GridFS用于存储和恢复那些超过16M(BSON文件限制)的文件(如:图片、音频、视频等)

GridFS用两个集合来存储一个文件:fs.files(元信息)与fs.chunks(实际内容)

每个文件的实际内容被存在chunks(二进制数据)中,和文件有关的meta数据(filename,content_type,还有用户自定义的属性)将会被存在files集合中。

使用

存储

将1.txt放到myfiles数据库中

mongofiles -d myfiles put 1.txt

一个文件会在fs.files中对应一个id

假如我们有两份文件存储在gridfs中

可以发现files中有两个id

fs.chunks中则不是这样了,它会分成很多份

查看文件列表

查看myfiles数据库下的所有文件

mongofiles -d myfiles list

详细查看文件信息

db.fs.files.find();

db.fs.files.find(files_id:objectId(''));;

获取&&下载

mongofiles  -d myfile get 1.txt

删除文件

mongofiles -d myfiles delete 1.txt

eval

执行脚本

db.eval('1+1');
<<<
2
db.eval("return 'hello'");
<<<
hello
db.system.js.insert({_id:'xx',value:'111'});
//类似于声明了一个全局变量
db.eval("return xx");
<<<
111

会存储在当前数据库下的Functions文件夹下(和Collections文件夹同级)

db.system.js.insert({_id:'say',value:function(){return 'hello'}});
//类似于声明了一个全局变量
db.eval("say()");
<<<
hello

上一篇:mongoDB基础

基于Redis实现分布式锁

mumupudding阅读(12)

背景

在很多互联网产品应用中,有些场景需要加锁处理,比如:秒杀,全局递增ID,楼层生成等等。大部分的解决方案是基于DB实现的,Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系。其次Redis提供一些命令SETNX,GETSET,可以方便实现分布式锁机制。

Redis命令介绍

使用Redis实现分布式锁,有两个重要函数需要介绍

SETNX命令(SET if Not eXists)
语法:
SETNX key value
功能:
当且仅当 key 不存在,将 key 的值设为 value ,并返回1;若给定的 key 已经存在,则 SETNX 不做任何动作,并返回0。

GETSET命令
语法:
GETSET key value
功能:
将给定 key 的值设为 value ,并返回 key 的旧值 (old value),当 key 存在但不是字符串类型时,返回一个错误,当key不存在时,返回nil。

GET命令
语法:
GET key
功能:
返回 key 所关联的字符串值,如果 key 不存在那么返回特殊值 nil 。

DEL命令
语法:
DEL key [KEY …]
功能:
删除给定的一个或多个 key ,不存在的 key 会被忽略。

兵贵精,不在多。分布式锁,我们就依靠这四个命令。但在具体实现,还有很多细节,需要仔细斟酌,因为在分布式并发多进程中,任何一点出现差错,都会导致死锁,hold住所有进程。

加锁实现

SETNX 可以直接加锁操作,比如说对某个关键词foo加锁,客户端可以尝试
SETNX foo.lock <current unix time>

如果返回1,表示客户端已经获取锁,可以往下操作,操作完成后,通过
DEL foo.lock

命令来释放锁。
如果返回0,说明foo已经被其他客户端上锁,如果锁是非堵塞的,可以选择返回调用。如果是堵塞调用调用,就需要进入以下个重试循环,直至成功获得锁或者重试超时。理想是美好的,现实是残酷的。仅仅使用SETNX加锁带有竞争条件的,在某些特定的情况会造成死锁错误。

处理死锁

在上面的处理方式中,如果获取锁的客户端端执行时间过长,进程被kill掉,或者因为其他异常崩溃,导致无法释放锁,就会造成死锁。所以,需要对加锁要做时效性检测。因此,我们在加锁时,把当前时间戳作为value存入此锁中,通过当前时间戳和Redis中的时间戳进行对比,如果超过一定差值,认为锁已经时效,防止锁无限期的锁下去,但是,在大并发情况,如果同时检测锁失效,并简单粗暴的删除死锁,再通过SETNX上锁,可能会导致竞争条件的产生,即多个客户端同时获取锁。

C1获取锁,并崩溃。C2和C3调用SETNX上锁返回0后,获得foo.lock的时间戳,通过比对时间戳,发现锁超时。
C2 向foo.lock发送DEL命令。
C2 向foo.lock发送SETNX获取锁。
C3 向foo.lock发送DEL命令,此时C3发送DEL时,其实DEL掉的是C2的锁。
C3 向foo.lock发送SETNX获取锁。

此时C2和C3都获取了锁,产生竞争条件,如果在更高并发的情况,可能会有更多客户端获取锁。所以,DEL锁的操作,不能直接使用在锁超时的情况下,幸好我们有GETSET方法,假设我们现在有另外一个客户端C4,看看如何使用GETSET方式,避免这种情况产生。

C1获取锁,并崩溃。C2和C3调用SETNX上锁返回0后,调用GET命令获得foo.lock的时间戳T1,通过比对时间戳,发现锁超时。
C4 向foo.lock发送GESET命令,
GETSET foo.lock <current unix time>
并得到foo.lock中老的时间戳T2

如果T1=T2,说明C4获得时间戳。
如果T1!=T2,说明C4之前有另外一个客户端C5通过调用GETSET方式获取了时间戳,C4未获得锁。只能sleep下,进入下次循环中。

现在唯一的问题是,C4设置foo.lock的新时间戳,是否会对锁产生影响。其实我们可以看到C4和C5执行的时间差值极小,并且写入foo.lock中的都是有效时间错,所以对锁并没有影响。
为了让这个锁更加强壮,获取锁的客户端,应该在调用关键业务时,再次调用GET方法获取T1,和写入的T0时间戳进行对比,以免锁因其他情况被执行DEL意外解开而不知。以上步骤和情况,很容易从其他参考资料中看到。客户端处理和失败的情况非常复杂,不仅仅是崩溃这么简单,还可能是客户端因为某些操作被阻塞了相当长时间,紧接着 DEL 命令被尝试执行(但这时锁却在另外的客户端手上)。也可能因为处理不当,导致死锁。还有可能因为sleep设置不合理,导致Redis在大并发下被压垮。最为常见的问题还有

GET返回nil时应该走那种逻辑?

第一种走超时逻辑
C1客户端获取锁,并且处理完后,DEL掉锁,在DEL锁之前。C2通过SETNX向foo.lock设置时间戳T0 发现有客户端获取锁,进入GET操作。
C2 向foo.lock发送GET命令,获取返回值T1(nil)。
C2 通过T0>T1+expire对比,进入GETSET流程。
C2 调用GETSET向foo.lock发送T0时间戳,返回foo.lock的原值T2
C2 如果T2=T1相等,获得锁,如果T2!=T1,未获得锁。

第二种情况走循环走setnx逻辑
C1客户端获取锁,并且处理完后,DEL掉锁,在DEL锁之前。C2通过SETNX向foo.lock设置时间戳T0 发现有客户端获取锁,进入GET操作。
C2 向foo.lock发送GET命令,获取返回值T1(nil)。
C2 循环,进入下一次SETNX逻辑

两种逻辑貌似都是OK,但是从逻辑处理上来说,第一种情况存在问题。当GET返回nil表示,锁是被删除的,而不是超时,应该走SETNX逻辑加锁。走第一种情况的问题是,正常的加锁逻辑应该走SETNX,而现在当锁被解除后,走的是GETST,如果判断条件不当,就会引起死锁,很悲催,我在做的时候就碰到了,具体怎么碰到的看下面的问题

GETSET返回nil时应该怎么处理?

C1和C2客户端调用GET接口,C1返回T1,此时C3网络情况更好,快速进入获取锁,并执行DEL删除锁,C2返回T2(nil),C1和C2都进入超时处理逻辑。
C1 向foo.lock发送GETSET命令,获取返回值T11(nil)。
C1 比对C1和C11发现两者不同,处理逻辑认为未获取锁。
C2 向foo.lock发送GETSET命令,获取返回值T22(C1写入的时间戳)。
C2 比对C2和C22发现两者不同,处理逻辑认为未获取锁。

此时C1和C2都认为未获取锁,其实C1是已经获取锁了,但是他的处理逻辑没有考虑GETSET返回nil的情况,只是单纯的用GET和GETSET值就行对比,至于为什么会出现这种情况?一种是多客户端时,每个客户端连接Redis的后,发出的命令并不是连续的,导致从单客户端看到的好像连续的命令,到Redis server后,这两条命令之间可能已经插入大量的其他客户端发出的命令,比如DEL,SETNX等。第二种情况,多客户端之间时间不同步,或者不是严格意义的同步。

时间戳的问题

我们看到foo.lock的value值为时间戳,所以要在多客户端情况下,保证锁有效,一定要同步各服务器的时间,如果各服务器间,时间有差异。时间不一致的客户端,在判断锁超时,就会出现偏差,从而产生竞争条件。
锁的超时与否,严格依赖时间戳,时间戳本身也是有精度限制,假如我们的时间精度为秒,从加锁到执行操作再到解锁,一般操作肯定都能在一秒内完成。这样的话,我们上面的CASE,就很容易出现。所以,最好把时间精度提升到毫秒级。这样的话,可以保证毫秒级别的锁是安全的。

分布式锁的问题

1:必要的超时机制:获取锁的客户端一旦崩溃,一定要有过期机制,否则其他客户端都降无法获取锁,造成死锁问题。
2:分布式锁,多客户端的时间戳不能保证严格意义的一致性,所以在某些特定因素下,有可能存在锁串的情况。要适度的机制,可以承受小概率的事件产生。
3:只对关键处理节点加锁,良好的习惯是,把相关的资源准备好,比如连接数据库后,调用加锁机制获取锁,直接进行操作,然后释放,尽量减少持有锁的时间。
4:在持有锁期间要不要CHECK锁,如果需要严格依赖锁的状态,最好在关键步骤中做锁的CHECK检查机制,但是根据我们的测试发现,在大并发时,每一次CHECK锁操作,都要消耗掉几个毫秒,而我们的整个持锁处理逻辑才不到10毫秒,玩客没有选择做锁的检查。
5:sleep学问,为了减少对Redis的压力,获取锁尝试时,循环之间一定要做sleep操作。但是sleep时间是多少是门学问。需要根据自己的Redis的QPS,加上持锁处理时间等进行合理计算。
6:至于为什么不使用Redis的muti,expire,watch等机制,可以查一参考资料,找下原因。

锁测试数据

未使用sleep

第一种,锁重试时未做sleep。单次请求,加锁,执行,解锁时间 

基于Redis实现分布式锁
可以看到加锁和解锁时间都很快,当我们使用

ab -n1000 -c100 ‘http://sandbox6.wanke.etao.com/test/test_sequence.php?tbpm=t’
AB 并发100累计1000次请求,对这个方法进行压测时。 

基于Redis实现分布式锁
我们会发现,获取锁的时间变成,同时持有锁后,执行时间也变成,而delete锁的时间,将近10ms时间,为什么会这样?
1:持有锁后,我们的执行逻辑中包含了再次调用Redis操作,在大并发情况下,Redis执行明显变慢。
2:锁的删除时间变长,从之前的0.2ms,变成9.8ms,性能下降近50倍。
在这种情况下,我们压测的QPS为49,最终发现QPS和压测总量有关,当我们并发100总共100次请求时,QPS得到110多。当我们使用sleep时

使用Sleep时

单次执行请求时

基于Redis实现分布式锁
我们看到,和不使用sleep机制时,性能相当。当时用相同的压测条件进行压缩时 

基于Redis实现分布式锁
获取锁的时间明显变长,而锁的释放时间明显变短,仅是不采用sleep机制的一半。当然执行时间变成就是因为,我们在执行过程中,重新创建数据库连接,导致时间变长的。同时我们可以对比下Redis的命令执行压力情况 

基于Redis实现分布式锁

上图中细高部分是为未采用sleep机制的时的压测图,矮胖部分为采用sleep机制的压测图,通上图看到压力减少50%左右,当然,sleep这种方式还有个缺点QPS下降明显,在我们的压测条件下,仅为35,并且有部分请求出现超时情况。不过综合各种情况后,我们还是决定采用sleep机制,主要是为了防止在大并发情况下把Redis压垮,很不行,我们之前碰到过,所以肯定会采用sleep机制。

30 分钟快速入门 Docker 教程

mumupudding阅读(10)


原文地址:梁桂钊的博客

博客地址:http://blog.720ui.com

欢迎关注公众号:「服务端思维」。一群同频者,一起成长,一起精进,打破认知的局限性。

一、欢迎来到 Docker 世界

1. Docker 与虚拟化

在没有 Docker 的时代,我们会使用硬件虚拟化(虚拟机)以提供隔离。这里,虚拟机通过在操作系统上建立了一个中间虚拟软件层 Hypervisor ,并利用物理机器的资源虚拟出多个虚拟硬件环境来共享宿主机的资源,其中的应用运行在虚拟机内核上。但是,虚拟机对硬件的利用率存在瓶颈,因为虚拟机很难根据当前业务量动态调整其占用的硬件资源,因此容器化技术得以流行。其中,Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的 Linux 机器上。

image.png

Docker 容器不使用硬件虚拟化,它的守护进程是宿主机上的一个进程,换句话说,应用直接运行在宿主机内核上。因为容器中运行的程序和计算机的操作系统之间没有额外的中间层,没有资源被冗余软件的运行或虚拟硬件的模拟而浪费掉。

Docker 的优势不仅如此,我们来比较一番。

特性 Docker 虚拟机
启动速度 秒级 分钟级
交付/部署 开发、测试、生产环境一致 无成熟体系
性能 近似物理机 性能损耗大
体量 极小(MB) 较大(GB)
迁移/扩展 跨平台,可复制 较为复杂

2. 镜像、容器和仓库

Docker 由镜像(Image)、容器(Container)、仓库(Repository) 三部分组成。

Docker 的镜像可以简单的类比为电脑装系统用的系统盘,包括操作系统,以及必要的软件。例如,一个镜像可以包含一个完整的 centos 操作系统环境,并安装了 Nginx 和 Tomcat 服务器。注意的是,镜像是只读的。这一点也很好理解,就像我们刻录的系统盘其实也是可读的。我们可以使用 docker images 来查看本地镜像列表。

Docker 的容器可以简单理解为提供了系统硬件环境,它是真正跑项目程序、消耗机器资源、提供服务的东西。例如,我们可以暂时把容器看作一个 Linux 的电脑,它可以直接运行。那么,容器是基于镜像启动的,并且每个容器都是相互隔离的。注意的是,容器在启动的时候基于镜像创建一层可写层作为最上层。我们可以使用 docker ps -a 查看本地运行过的容器。

Docker 的仓库用于存放镜像。这一点,和 Git 非常类似。我们可以从中心仓库下载镜像,也可以从自建仓库下载。同时,我们可以把制作好的镜像 commit 到本地,然后 push 到远程仓库。仓库分为公开仓库和私有仓库,最大的公开仓库是官方仓库 Dock Hub,国内的公开仓库也有很多选择,例如阿里云等。

image.png

3. Docker 促使开发流程变更

笔者认为,Docker 对开发流程的影响在于使环境标准化。例如,原来我们存在三个环境:开发(日常)环境、测试环境、生产环境。这里,我们对于每个环境都需要部署相同的软件、脚本和运行程序,如图所示。事实上,对于启动脚本内容都是一致的,但是没有统一维护,经常会出问题。此外,对于运行程序而言,如果所依赖的底层运行环境不一致,也会造成困扰和异常。

image.png

现在,我们通过引入 Docker 之后,我们只需要维护一个 Docker 镜像。换句话说,多套环境,一个镜像,实现系统级别的一次构建到处运行。此时,我们把运行脚本标准化了,把底层软件镜像化了,然后对于相同的将要部署的程序实行标准化部署。因此,Docker 为我们提供了一个标准化的运维模式,并固化运维步骤和流程。

image.png

通过这个流程的改进,我们更容易实现 DevOps 的目标,因为我们的镜像生成后可以跑在任何系统,并快速部署。此外,使用 Docker 的很大动力是基于 Docker 实现弹性调度,以更充分地利用机器资源,节省成本。

哈哈,笔者在使用 Docker 过程中,还发现了一些很棒的收益点,例如我们发布回滚的时候只需要切换 TAG 并重启即可。还比如,我们对环境升级,也只需要升级基础镜像,那么新构建的应用镜像,自动会引用新的版本。(欢迎补充~~~)

二、从搭建 Web 服务器开始说起

1. 环境先行,安装 Docker

现在,我们需要安装以下步骤安装 Docker。

官方下载地址:(Mac):https://download.docker.com/mac/stable/Docker.dmg阿里云下载地址(Mac):http://mirrors.aliyun.com/docker-toolbox/mac/docker-for-mac/阿里云下载地址(Windows): http://mirrors.aliyun.com/docker-toolbox/windows/docker-for-windows/

  • 安装指南这里,双击刚刚下载的 Doker.dmg 安装包进行安装。image.png

安装完成后启动, Mac 顶部导航栏出现了一个图标,通过菜单可以进行 docker 配置和退出等操作。

image.png

官方指南:https://docs.docker.com/install/阿里云指南(Linux):https://yq.aliyun.com/articles/110806?spm=5176.8351553.0.0.468b1991jdT95t

  • 设置加速服务

市面上有很多加速服务的提供商,如:DaoCloud,阿里云等。这里,笔者使用的是阿里云。(注意的是,笔者操作系统是 Mac,其他操作系列参见阿里云操作文档) 

image.png

右键点击桌面顶栏的 docker 图标,选择 Preferences ,在 Daemon 标签(Docker 17.03 之前版本为 Advanced 标签)下的 Registry mirrors 列表中将<br />https://xxx.mirror.aliyuncs.com 加到"registry-mirrors"的数组里,点击 Apply & Restart 按钮,等待 Docker 重启并应用配置的镜像加速器。

image.png

阿里云操作文档:https://cr.console.aliyun.com/cn-hangzhou/instances/mirrors

  • 查看版本

至此,我们已经安装完成了。这里,我们来查看版本。

docker version

查看结果,如下所示。

image.png

2. 实干派,从搭建 Web 服务器开始

我们作为实干派,那么先来搭建一个 Web 服务器吧。然后,笔者带你慢慢理解这个过程中,做了什么事情。首先,我们需要拉取 centos 镜像。

docker run -p 80 --name web -i -t centos /bin/bash

紧接着,我们安装 nginx 服务器,执行以下命令:

rpm -ivh http://nginx.org/packages/centos/7/noarch/RPMS/nginx-release-centos-7-0.el7.ngx.noarch.rpm

安装完 Nginx 源后,就可以正式安装 Nginx 了。

yum install -y nginx

至此,我们再输入 whereis nginx 命令就可以看到安装的路径了。最后,我们还需要将 Nginx 跑起来。

nginx

现在,我们执行 ctrl + P +  Q 切换到后台。然后,通过 docker ps -a 来查看随机分配的端口。

image.png

这里,笔者分配的端口是 32769 ,那么通过浏览器访问 http://127.0.0.1:32769 即可。

image.png

大功告成,哈哈哈~

3. 复盘理解全过程

现在,我们来理解下这个流程。首先,我们输入 docker run -p 80 --name web -i -t centos /bin/bash 命令会运行交互式容器,其中 -i 选项告诉 Docker 容器保持标准输入流对容器开放,即使容器没有终端连接,另一个 -t 选项告诉 Docker 为容器分配一个虚拟终端,以便于我们接下来安装 Nginx 服务器。(笔者备注:Docker 还支持输入 -d 选项告诉 Docker 在后台运行容器的守护进程)

Docker 会为我们创建的每一个容器自动生成一个随机的名称。事实上,这种方式虽然便捷,但是可读性很差,并且对我们后期维护的理解成本会比较大。因此,我们通过 --name web 选项告诉 Docker 创建一个名称是 web 的容器。此外,我们通过 -p 80 告诉 Docker 开放 80 端口,那么, Nginx 才可以对外通过访问和服务。但是,我们的宿主机器会自动做端口映射,比如上面分配的端口是 32769 ,注意的是,如果关闭或者重启,这个端口就变了,那么怎么解决固定端口的问题,笔者会在后面详细剖析和带你实战。

这里,还有一个非常重要的知识点 docker run 。Docker 通过 run 命令来启动一个新容器。Docker 首先在本机中寻找该镜像,如果没有安装,Docker 在 Docker Hub 上查找该镜像并下载安装到本机,最后 Docker 创建一个新的容器并启动该程序。

image.png

但是,当第二次执行  docker run 时,因为 Docker 在本机中已经安装该镜像,所以 Docker 会直接创建一个新的容器并启动该程序。

image.png

注意的是,docker run 每次使用都会创建一个新的容器,因此,我们以后再次启动这个容器时,只需要使用命令 docker start  即可。这里, docker start 的作用在用重新启动已存在的镜像,而docker run 包含将镜像放入容器中 docker create ,然后将容器启动 docker start ,如图所示。

image.png

现在,我们可以在上面的案例的基础上,通过 exit 命令关闭 Docker 容器。当然,如果我们运行的是后台的守护进程,我们也可以通过 docker stop web 来停止。注意的是,docker stop 和 docker kill 略有不同,docker stop 发送 SIGTERM 信号,而 docker kill 发送SIGKILL 信号。然后,我们使用 docker start 重启它。

docker start web

Docker 容器重启后会沿用 docker run 命令指定的参数来运行,但是,此时它还是后台运行的。我们必须通过 docker attach 命令切换到运行交互式容器。

docker attach web

4. 不止如此,还有更多命令

Docker 提供了非常丰富的命令。所谓一图胜千言,我们可以从下面的图片了解到很多信息和它们之前的用途。(可以直接跳过阅读,建议收藏,便于扩展阅读)

image.png

如果希望获取更多信息,可以阅读官方使用文档。

Command Description
docker attach Attach local standard input, output, and error streams to a running container
docker build Build an image from a Dockerfile
docker builder Manage builds
docker checkpoint Manage checkpoints
docker commit Create a new image from a container’s changes
docker config Manage Docker configs
docker container Manage containers
docker cp Copy files/folders between a container and the local filesystem
docker create Create a new container
docker deploy Deploy a new stack or update an existing stack
docker diff Inspect changes to files or directories on a container’s filesystem
docker engine Manage the docker engine
docker events Get real time events from the server
docker exec Run a command in a running container
docker export Export a container’s filesystem as a tar archive
docker history Show the history of an image
docker image Manage images
docker images List images
docker import Import the contents from a tarball to create a filesystem image
docker info Display system-wide information
docker inspect Return low-level information on Docker objects
docker kill Kill one or more running containers
docker load Load an image from a tar archive or STDIN
docker login Log in to a Docker registry
docker logout Log out from a Docker registry
docker logs Fetch the logs of a container
docker manifest Manage Docker image manifests and manifest lists
docker network Manage networks
docker node Manage Swarm nodes
docker pause Pause all processes within one or more containers
docker plugin Manage plugins
docker port List port mappings or a specific mapping for the container
docker ps List containers
docker pull Pull an image or a repository from a registry
docker push Push an image or a repository to a registry
docker rename Rename a container
docker restart Restart one or more containers
docker rm Remove one or more containers
docker rmi Remove one or more images
docker run Run a command in a new container
docker save Save one or more images to a tar archive (streamed to STDOUT by default)
docker search Search the Docker Hub for images
docker secret Manage Docker secrets
docker service Manage services
docker stack Manage Docker stacks
docker start Start one or more stopped containers
docker stats Display a live stream of container(s) resource usage statistics
docker stop Stop one or more running containers
docker swarm Manage Swarm
docker system Manage Docker
docker tag Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE
docker top Display the running processes of a container
docker trust Manage trust on Docker images
docker unpause Unpause all processes within one or more containers
docker update Update configuration of one or more containers
docker version Show the Docker version information
docker volume Manage volumes
docker wait Block until one or more containers stop, then print their exit codes

官方阅读链接:https://docs.docker.com/engine/reference/commandline/docker/

5. 进阶:仓库与软件安装的简化

还记得笔者在文章开头介绍的「镜像、容器和仓库」吗?Docker 的仓库用于存放镜像。我们可以从中心仓库下载镜像,也可以从自建仓库下载。同时,我们可以把制作好的镜像从本地推送到远程仓库。

首先,笔者先引入一个知识点:Docker 的镜像就是它的文件系统,一个镜像可以放在另外一个镜像的上层,那么位于下层的就是它的父镜像。所以,Docker 会存在很多镜像层,每个镜像层都是只读的,并且不会改变。当我们创建一个新的容器时,Docker 会构建出一个镜像栈,并在栈的最顶层添加一个读写层,如图所示。

image.png

现在,我们可以通过 docker images 命令查看本地的镜像。

docker images

查询结果,如图所示。

image.png

这里,对几个名词解释一下含义。

  • REPOSITORY:仓库名称。
  • TAG: 镜像标签,其中 lastest 表示最新版本。注意的是,一个镜像可以有多个标签,那么我们就可以通过标签来管理有用的版本和功能标签。
  • IMAGE ID :镜像唯一ID。
  • CREATED :创建时间。
  • SIZE :镜像大小。

那么,如果第一次我们通过 docker pull centos:latest 拉取镜像,那么当我们执行 docker run -p 80 --name web -i -t centos /bin/bash 时,它就不会再去远程获取了,因为本机中已经安装该镜像,所以 Docker 会直接创建一个新的容器并启动该程序。

事实上,官方已经提供了安装好 Nginx 的镜像,我们可以直接使用。现在,我们通过拉取镜像的方式重新构建一个 Web 服务器。首先,我们通过 docker search 来查找镜像。我们获取到 Nginx 的镜像清单。

docker search nginx

补充一下,我们也可以通过访问 Docker Hub (https://hub.docker.com/)搜索仓库,那么 star 数越多,说明它越靠谱,可以放心使用。

image.png

现在,我们通过 docker pull nginx 拉取最新的 Nginx 的镜像。当然,我们也可以通过 docker pull nginx:latest  来操作。

docker pull nginx

然后,我们创建并运行一个容器。与前面不同的是,我们通过 -d 选项告诉 Docker 在后台运行容器的守护进程。并且,通过 8080:80 告诉 Docker 8080 端口是对外开放的端口,80 端口对外开放的端口映射到容器里的端口号。

docker run -p 8080:80 -d --name nginx nginx

我们再通过 docker ps -a 来查看,发现容器已经后台运行了,并且后台执行了 nginx 命令,并对外开放 8080 端口。

image.png

因此,通过浏览器访问 http://127.0.0.1:8080 即可。

image.png

6. 其他选择,使用替代注册服务器

Docker Hub 不是软件的唯一来源,我们也可以切换到国内的其他替代注册服务器,例如阿里云。我们可以登录 https://cr.console.aliyun.com 搜索,并拉取公开的镜像。

image.png

image.png

现在,我们输入 docker pull 命令进行拉取。

docker pull registry.cn-hangzhou.aliyuncs.com/qp_oraclejava/orackejava:8u172_DCEVM_HOTSWAPAGEN_JCE

这里,笔者继续补充一个知识点:注册服务器的地址。事实上,注册服务器的地址是有一套规范的。完整格式是:[仓库主机/][用户名/]容器短名[:标签]。这里,仓库主机是 registry.cn-hangzhou.aliyuncs.com,用户名是 qp_oraclejava,容器短名是 orackejava,标签名是 8u172_DCEVM_HOTSWAPAGEN_JCE。事实上,我们上面通过 docker pull centos:latest 拉取镜像,相当于 docker pull registry.hub.docker.com/centos:latest 。

三、构建我的镜像

通过上面的学习,笔者相信你已经对 Docker 使用有了一个大致的了解,就好比我们通过 VMware 安装了一个系统,并让它跑了起来,那么我们就可以在这个 Linux 系统(CentOS 或者 Ubuntu ) 上面工作我们想要的任何事情。事实上,我们还会经常把我们安装好的 VMware 系统进行快照备份并实现克隆来满足我们下次快速的复制。这里,Docker 也可以构建定制内容的 Docker 镜像,例如上面我们使用官方提供的安装好 Nginx 的 Docker 镜像。注意的是,我们通过基于已有的基础镜像,在上面添加镜像层的方式构建新镜像而已。

总结一下,Docker 提供自定义镜像的能力,它可以让我们保存对基础镜像的修改,并再次使用。那么,我们就可以把操作系统、运行环境、脚本和程序打包在一起,并在宿主机上对外提供服务。

Docker 构建镜像有两种方式,一种方式是使用 docker commit 命令,另外一种方式使用 docker build 命令和 Dockerfile 文件。其中,不推荐使用 docker commit 命令进行构建,因为它没有使得整个流程标准化,因此,在企业的中更加推荐使用 docker build 命令和 Dockerfile 文件来构建我们的镜像。我们使用Dockerfile 文件可以让构建镜像更具备可重复性,同时保证启动脚本和运行程序的标准化。

1. 构建第一个 Dockerfile 文件

现在,我们继续实战。这里,我们把一开始搭建的 Web 服务器构建一个镜像。首先,我们需要创建一个空的 Dokcerfile 文件。

mkdir dockerfile_testcd dockerfile_test/touch Dockerfilenano Dockerfile

紧接着,我们需要编写一个 Dockerfile 文件,代码清单如下

FROM centos:7MAINTAINER LiangGzone "lianggzone@163.com"RUN rpm -ivh http://nginx.org/packages/centos/7/noarch/RPMS/nginx-release-centos-7-0.el7.ngx.noarch.rpmRUN yum install -y nginxEXPOSE 80

最后,我们通过 docker build 命令进行构建。

docker build -t="lianggzone/nginx_demo:v1" .

现在, 我们来通过 docker images 看下我们的新镜像吧。

image.png

2. 理解 Dockerfile 全过程

哇,我们通过编写一个 Dockerfile 文件顺利构建了一个新的镜像。这个过程简单得让人无法相信。现在,让我们来理解一下这个全过程吧。首先, FROM centos:7 是 Dockerfile 必须要的第一步,它会从一个已经存在的镜像运行一个容器,换句话说,Docker 需要依赖于一个基础镜像进行构建。这里,我们指定 centos 作为基础镜像,它的版本是 7 (CentOS 7)。然后,我们通过 MAINTAINER LiangGzone "lianggzone@163.com" 指定该镜像的作者是 LiangGzone,邮箱是 lianggzone@163.com。这有助于告诉使用者它的作者和联系方式。接着,我们执行两个 RUN 指令进行 Nginx 的下载安装,最后通过  EXPOSE 80 暴露 Dokcer 容器的 80 端口。注意的是,Docker 的执行顺序是从上而下执行的,所以我们要明确整个流程的执行顺序。除此之外,Docker 在执行每个指令之后都会创建一个新的镜像层并且进行提交。

我们使用  docker build 命令进行构建,指定 - t 告诉 Docker 镜像的名称和版本。注意的是,如果没有指定任何标签,Docker 将会自动为镜像设置一个 lastest 标签。还有一点,我们最后还有一个 . 是为了让 Docker 到当前本地目录去寻找 Dockerfile 文件。注意的是,Docker 会在每一步构建都会将结果提交为镜像,然后将之前的镜像层看作缓存,因此我们重新构建类似的镜像层时会直接复用之前的镜像。如果我们需要跳过,可以使用 --no-cache 选项告诉 Docker 不进行缓存。

3. Dockerfile 指令详解

Dockerfile 提供了非常多的指令。笔者这里特别整理了一份清单,建议收藏查看。

image.png

官方地址:https://docs.docker.com/engine/reference/builder/#usage

指令辨别一:RUN、CMD、ENTRYPOINT

RUN 、 CMD 、 ENTRYPOINT  三个指令的用途非常相识,不同在于,RUN 指令是在容器被构建时运行的命令,而CMD 、 ENTRYPOINT 是启动容器时执行 shell 命令,而 RUN 会被 docker run 命令覆盖,但是  ENTRYPOINT 不会被覆盖。事实上,docker run 命令指定的任何参数都会被当作参数再次传递给 ENTRYPOINT  指令。CMD 、 ENTRYPOINT 两个指令之间也可以一起使用。例如,我们 可以使用 ENTRYPOINT 的 exec 形式设置固定的默认命令和参数,然后使用任一形式的 CMD 来设置可能更改的其他默认值。

FROM ubuntuENTRYPOINT ["top", "-b"]CMD ["-c"]

指令辨别二:ADD、COPY

ADD 、 COPY 指令用法一样,唯一不同的是 ADD  支持将归档文件(tar, gzip, bzip2, etc)做提取和解压操作。注意的是,COPY 指令需要复制的目录一定要放在 Dockerfile 文件的同级目录下。

4. 将镜像推送到远程仓库

远程仓库:Docker Hub 

镜像构建完毕之后,我们可以将它上传到 Docker Hub 上面。首先,我们需要通过 docker login 保证我们已经登录了。紧接着,我们使用 docker push 命令进行推送。

docker push lianggzone/nginx_demo:v1

这里,我们了解下它的使用,格式是 docker push [OPTIONS] NAME[:TAG] ,其中,笔者设置 NAME 是 lianggzone/nginx_demo,TAG 是 v1。 (笔者注:推送 Docker Hub 速度很慢,耐心等待) 最后,上传完成后访问:https://hub.docker.com/u/lianggzone/,如图所示。

image.png

远程仓库:阿里云

同时,我们也可以使用国内的仓库,比如阿里云。首先,在终端中输入访问凭证,登录 Registry 实例。如果你不知道是哪个,可以访问 https://cr.console.aliyun.com/cn-hangzhou/instances/credentials

docker login --username=帐号 registry.cn-hangzhou.aliyuncs.com

现在,将镜像推送到阿里云镜像仓库。其中, docker tag [IMAGE_ID] registry.cn-hangzhou.aliyuncs.com/[命名空间]/[镜像名称]:[版本] 和 docker push registry.cn-hangzhou.aliyuncs.com/[命名空间]/[镜像名称]:[版本] 命令的使用如下所示。

docker tag 794c07361565 registry.cn-hangzhou.aliyuncs.com/lianggzone/nginx_demo:v1docker push registry.cn-hangzhou.aliyuncs.com/lianggzone/nginx_demo:v1

最后,上传完成后访问:https://cr.console.aliyun.com/cn-hangzhou/instances/repositories,如图所示。

image.png

5. Dockerfile 的 Github 源码地址

这里,附上我整理的 Dockerfile 的仓库。后面,笔者会陆续更新用到的一些常用文件,欢迎 star 关注。

https://github.com/lianggzone/dockerfile-images

附:参考资料

(完,转载请注明作者及出处。)

写在末尾

【服务端思维】:我们一起聊聊服务端核心技术,探讨一线互联网的项目架构与实战经验。同时,拥有众多技术大牛的「后端圈」大家庭,期待你的加入,一群同频者,一起成长,一起精进,打破认知的局限性。

更多精彩文章,尽在「服务端思维」!

『并发包入坑指北』之阻塞队列

mumupudding阅读(12)

前言

较长一段时间以来我都发现不少开发者对 jdk 中的 J.U.C(java.util.concurrent)也就是 Java 并发包的使用甚少,更别谈对它的理解了;但这却也是我们进阶的必备关卡。

之前或多或少也分享过相关内容,但都不成体系;于是便想整理一套与并发包相关的系列文章。

其中的内容主要包含以下几个部分:

  • 根据定义自己实现一个并发工具。
  • JDK 的标准实现。
  • 实践案例。

基于这三点我相信大家对这部分内容不至于一问三不知。

既然开了一个新坑,就不想做的太差;所以我打算将这个列表下的大部分类都讲到。

所以本次重点讨论 ArrayBlockingQueue

自己实现

在自己实现之前先搞清楚阻塞队列的几个特点:

  • 基本队列特性:先进先出。
  • 写入队列空间不可用时会阻塞。
  • 获取队列数据时当队列为空时将阻塞。

实现队列的方式多种,总的来说就是数组和链表;其实我们只需要搞清楚其中一个即可,不同的特性主要表现为数组和链表的区别。

这里的 ArrayBlockingQueue 看名字很明显是由数组实现。

我们先根据它这三个特性尝试自己实现试试。

初始化队列

我这里自定义了一个类:ArrayQueue,它的构造函数如下:

    public ArrayQueue(int size) {        items = new Object[size];    }

很明显这里的 items 就是存放数据的数组;在初始化时需要根据大小创建数组。

写入队列

写入队列比较简单,只需要依次把数据存放到这个数组中即可,如下图:

但还是有几个需要注意的点:

  • 队列满的时候,写入的线程需要被阻塞。
  • 写入过队列的数量大于队列大小时需要从第一个下标开始写。

先看第一个队列满的时候,写入的线程需要被阻塞,先来考虑下如何才能使一个线程被阻塞,看起来的表象线程卡住啥事也做不了。

有几种方案可以实现这个效果:

  • Thread.sleep(timeout)线程休眠。
  • object.wait() 让线程进入 waiting 状态。

当然还有一些 join、LockSupport.part 等不在本次的讨论范围。

阻塞队列还有一个非常重要的特性是:当队列空间可用时(取出队列),写入线程需要被唤醒让数据可以写入进去。

所以很明显Thread.sleep(timeout)不合适,它在到达超时时间之后便会继续运行;达不到空间可用时才唤醒继续运行这个特点。

其实这样的一个特点很容易让我们想到 Java 的等待通知机制来实现线程间通信;更多线程见通信的方案可以参考这里:深入理解线程通信

所以我这里的做法是,一旦队列满时就将写入线程调用 object.wait() 进入 waiting 状态,直到空间可用时再进行唤醒。

    /**     * 队列满时的阻塞锁     */    private Object full = new Object();    /**     * 队列空时的阻塞锁     */    private Object empty = new Object();

所以这里声明了两个对象用于队列满、空情况下的互相通知作用。

在写入数据成功后需要使用 empty.notify(),这样的目的是当获取队列为空时,一旦写入数据成功就可以把消费队列的线程唤醒。

这里的 wait 和 notify 操作都需要对各自的对象使用 synchronized 方法块,这是因为 wait 和 notify 都需要获取到各自的锁。

消费队列

上文也提到了:当队列为空时,获取队列的线程需要被阻塞,直到队列中有数据时才被唤醒。

代码和写入的非常类似,也很好理解;只是这里的等待、唤醒恰好是相反的,通过下面这张图可以很好理解:

总的来说就是:

  • 写入队列满时会阻塞直到获取线程消费了队列数据后唤醒写入线程
  • 消费队列空时会阻塞直到写入线程写入了队列数据后唤醒消费线程

测试

先来一个基本的测试:单线程的写入和消费。

3123123412345

通过结果来看没什么问题。


当写入的数据超过队列的大小时,就只能消费之后才能接着写入。

2019-04-09 16:24:41.040 [Thread-0] INFO  c.c.concurrent.ArrayQueueTest - [Thread-0]1232019-04-09 16:24:41.040 [main] INFO  c.c.concurrent.ArrayQueueTest - size=32019-04-09 16:24:41.047 [main] INFO  c.c.concurrent.ArrayQueueTest - 12342019-04-09 16:24:41.048 [main] INFO  c.c.concurrent.ArrayQueueTest - 123452019-04-09 16:24:41.048 [main] INFO  c.c.concurrent.ArrayQueueTest - 123456

从运行结果也能看出只有当消费数据后才能接着往队列里写入数据。


而当没有消费时,再往队列里写数据则会导致写入线程被阻塞。

并发测试

三个线程并发写入300条数据,其中一个线程消费一条。

=====0299

最终的队列大小为 299,可见线程也是安全的。

由于不管是写入还是获取方法里的操作都需要获取锁才能操作,所以整个队列是线程安全的。

ArrayBlockingQueue

下面来看看 JDK 标准的 ArrayBlockingQueue 的实现,有了上面的基础会更好理解。

初始化队列

看似要复杂些,但其实逐步拆分后也很好理解:

第一步其实和我们自己写的一样,初始化一个队列大小的数组。

第二步初始化了一个重入锁,这里其实就和我们之前使用的 synchronized 作用一致的;

只是这里在初始化重入锁的时候默认是非公平锁,当然也可以指定为 true 使用公平锁;这样就会按照队列的顺序进行写入和消费。

更多关于 ReentrantLock 的使用和原理请参考这里:ReentrantLock 实现原理

三四两步则是创建了 notEmpty notFull 这两个条件,他的作用于用法和之前使用的 object.wait/notify 类似。

这就是整个初始化的内容,其实和我们自己实现的非常类似。

写入队列

其实会发现阻塞写入的原理都是差不多的,只是这里使用的是 Lock 来显式获取和释放锁。

同时其中的 notFull.await();notEmpty.signal(); 和我们之前使用的 object.wait/notify 的用法和作用也是一样的。

当然它还是实现了超时阻塞的 API

也是比较简单,使用了一个具有超时时间的等待方法。

消费队列

再看消费队列:

也是差不多的,一看就懂。

而其中的超时 API 也是使用了 notEmpty.awaitNanos(nanos) 来实现超时返回的,就不具体说了。

实际案例

说了这么多,来看一个队列的实际案例吧。

背景是这样的:

有一个定时任务会按照一定的间隔时间从数据库中读取一批数据,需要对这些数据做校验同时调用一个远程接口。

简单的做法就是由这个定时任务的线程去完成读取数据、消息校验、调用接口等整个全流程;但这样会有一个问题:

假设调用外部接口出现了异常、网络不稳导致耗时增加就会造成整个任务的效率降低,因为他都是串行会互相影响。

所以我们改进了方案:

其实就是一个典型的生产者消费者模型:

  • 生产线程从数据库中读取消息丢到队列里。
  • 消费线程从队列里获取数据做业务逻辑。

这样两个线程就可以通过这个队列来进行解耦,互相不影响,同时这个队列也能起到缓冲的作用。

但在使用过程中也有一些小细节值得注意。

因为这个外部接口是支持批量执行的,所以在消费线程取出数据后会在内存中做一个累加,一旦达到阈值或者是累计了一个时间段便将这批累计的数据处理掉。

但由于开发者的大意,在消费的时候使用的是 queue.take() 这个阻塞的 API;正常运行没啥问题。

可一旦原始的数据源,也就是 DB 中没数据了,导致队列里的数据也被消费完后这个消费线程便会被阻塞。

这样上一轮积累在内存中的数据便一直没机会使用,直到数据源又有数据了,一旦中间间隔较长时便可能会导致严重的业务异常。

所以我们最好是使用 queue.poll(timeout) 这样带超时时间的 api,除非业务上有明确的要求需要阻塞。

这个习惯同样适用于其他场景,比如调用 http、rpc 接口等都需要设置合理的超时时间。

总结

关于 ArrayBlockingQueue 的相关分享便到此结束,接着会继续更新其他并发容器及并发工具。

对本文有任何相关问题都可以留言讨论。

本文涉及到的所有源码:

https://github.com/crossoverJie/JCSprout/blob/master/src/main/java/com/crossoverjie/concurrent/ArrayQueue.java

你的点赞与分享是对我最大的支持

分布式工作流任务调度系统Easy Scheduler正式开源

mumupudding阅读(12)


分布式工作流任务调度系统Easy Scheduler正式开源

License

1、背景

在多位技术小伙伴的努力下,经过近2年的研发迭代、内部业务剥离及重构,也经历一批种子用户试用一段时间后,EasyScheduler终于迎来了第一个正式开源发布版本 — 1.0.0。相信做过数据处理的伙伴们对开源的调度系统如oozie、azkaban、airflow应该都不陌生,在使用这些调度系统中可能会有这样的体验:比如配置工作流任务不能可视化、任务的运行状态不能实时在线查看、任务运行时不能暂停、不能支持参数传递、不能补数、不能多租户使用、调度系统不高可用等等问题所烦扰过。Easy Scheduler正是在这种背景下应运而生,其目标就是为使调度更加easy,更可以从其中文名“易调度”看出我们的初衷。

2、设计特点

Easy Scheduler是一个分布式工作流任务调度系统,主要解决数据研发ETL错综复杂的依赖关系所带来的各种问题。其主要目标如下:

  • 以DAG图的方式将Task按照任务的依赖关系关联起来,可实时可视化监控任务的运行状态
  • 支持丰富的任务类型:Shell、MR、Spark、SQL(mysql、postgresql、hive、sparksql),Python,Sub_Process、Procedure等
  • 支持工作流定时调度、依赖调度、手动调度、手动暂停/停止/恢复,同时支持失败重试/告警、从指定节点恢复失败、Kill任务等操作
  • 支持工作流优先级、任务优先级及任务的故障转移及任务超时告警/失败
  • 支持工作流全局参数及节点自定义参数设置
  • 支持资源文件的在线上传/下载,管理等,支持在线文件创建、编辑
  • 支持任务日志在线查看及滚动、在线下载日志等
  • 实现集群HA,通过Zookeeper实现Master集群和Worker集群去中心化
  • 支持对Master/Worker cpu load,memory,cpu在线查看
  • 支持工作流运行历史树形/甘特图展示、支持任务状态统计、流程状态统计
  • 支持补数
  • 支持多租户
  • 支持国际化
  • 还有更多等待伙伴们探索

4、与同类调度系统的对比

调度系统对比

5、系统部分截图

6、文档

更多文档请参考:Easy Scheduler中文在线文档

7、感谢

Easy Scheduler使用了很多优秀的开源项目,比如google的guava、guice、grpc,netty,ali的bonecp,quartz,以及apache的众多开源项目等等,我们也非常感谢oozie、azkaban、airflow等优秀调度作品的出现带给我们的启发,正是由于站在这些开源项目的肩膀上,才有Easy Scheduler的诞生的可能。对此我们对使用的所有开源软件表示非常的感谢!我们也希望自己不仅是开源的受益者,也能成为开源的贡献者,于是我们决定把易调度贡献出来,并承诺长期维护。也希望对开源有同样热情和信念的伙伴加入进来,一起为开源献出一份力!

8、后记

Easy Scheduler于2019.03.28号正式开源后,仅仅一周时间,我们就感受到了伙伴们对Easy Scheduler的极大热情,很多伙伴提出使用反馈,还有一些伙伴是直接就找到相应的源代码来提问题或给出更好的建议、甚至直接在Easy Scheduler上撸袖子写代码,这给我们目前的主要开发者予以极大的精神鼓舞,非常感谢伙伴们这么热情和信任我们,我们会和大家一道继续奔走在使调度系统开箱即用这条大道上,为使"数据能力平民化"添砖加瓦,为数据时代贡献自己的激情和汗水!

码云地址: https://gitee.com/easyscheduler/EasyScheduler

开源github地址:https://github.com/analysys/EasyScheduler

在线文档地址:https://analysys.github.io/easyscheduler_docs_cn