开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Jupyter 常见可视化框架选择 选择标准 框架罗列 bq

发表于 2017-11-15

对于以Python作为技术栈的数据科学工作者,Jupyter是不得不提的数据报告工具。可能对于R社区而言,鼎鼎大名的ggplot2是常见的可视化框架,而大家对于Python,以及Jupyter为核心的交互式报告的可个视化方案就并没有那么熟悉。本文试图比较几个常用的解决方案,方便大家选择。

选择标准

称述式还是命令式

数据工作者使用的图的类别,常见的就三类:GIS可视化、网络可视化和统计图。因此,大多数场景下,我们并不想接触非常底层的基于点、线、面的命令,所以,选择一个好的封装的框架相当重要。

当然,公认较好的封装是基于《The Grammar of Graphics (Statistics and Computing)》一书,R中的ggplot2基本上就是一个很好的实现。我们基本上可以像用「自然语言」(Natural Language)一样使用这些绘图命令。我们姑且采用计算机科学领域的「陈述式」来表达这种绘图方式。

相反,有时候,以下情形时,我们可能对于这种绘图命令可能并不在意:

  1. 出图相当简单,要求绘制速度,一般大的框架较重(当然只是相对而言);
  2. 想要对细节做非常详尽的微调,一般大框架在微调方面会相对复杂或者退缩成一句句命令;
  3. 是统计作图可视化的创新者,想要尝试做出新的可视化实践。

这些情况下,显然,简单操作式并提供底层绘制命令的框架更让人愉快,与上面类似,我们借用「命令式」描述这类框架。

是否交互

与传统的交付静态图标不同,基于Web端的Jupter的一大好处就是可以绘制交互的图标(最近的RNotebook也有实现),因此,是否选择交互式,也是一个需要权衡的地方。

交互图的优势:

  1. 可以提供更多的数据维度和信息;
  2. 用户端可以做更多诸如放大、选取、转存的操作;
  3. 可以交付BI工程师相应的JavaScript代码用以工程化;
  4. 效果上比较炫酷,考虑到报告接受者的特征可以选择。

非交互图的优势:

  1. 报告文件直接导出成静态文件时相对问题,不会因为转换而损失信息;
  2. 图片可以与报告分离,必要时作为其他工作的成果;
  3. 不需要在运行Notebook时花很多世界载入各类前端框架。

是非内核交互

Jupyter上大多数命令通过以下方式获取数据,而大多数绘图方式事实上只是通过Notebook内的代码在Notebook与内核交互后展示出输出结果。但ipywidgets框架则可以实现Code Cell中的代码与Notebook中的前端控件(比如按钮等)绑定来进行操作内核,提供不同的绘图结果,甚至某些绘图框架的每个元素都可以直接和内核进行交互。

3262887070-59fae2a6967b6_articlex

用这些框架,可以搭建更复杂的Notebook的可视化应用,但缺点是因为基于内核,所以在呈递、展示报告时如果使用离线文件时,这些交互就会无效。

框架罗列

matplotlib

最家喻户晓的绘图框架是matplotlib,它提供了几乎所有python内静态绘图框架的底层命令。如果按照上面对可视化框架的分法,matplotlib属于非交互式的的「命令式」作图框架。

1
2
3
4
5
6
7
8
9
10
复制代码## matplotlib代码示例
from pylab import *
 
X = np.linspace(-np.pi, np.pi, 256,endpoint=True)
C,S = np.cos(X), np.sin(X)
 
plot(X,C)
plot(X,S)
 
show()

3262887070-59fae2a6967b6_articlex

优点是相对较快,底层操作较多。缺点是语言繁琐,内置默认风格不够美观。

matplotlib在jupyter中需要一些配置,可以展现更好的效果,详情参见这篇文章.

ggplot和plotnine

值得一说,对于R迁移过来的人来说,ggplot和plotnine简直是福音,基本克隆了ggplot2所有语法。横向比较的话,plotnine的效果更好。这两个绘图包的底层依旧是matplotlib,因此,在引用时别忘了使用%matplotlib inline语句。值得一说的是plotnine也移植了ggplot2中良好的配置语法和逻辑。

1
2
3
4
5
复制代码## plotnine示例
(ggplot(mtcars, aes('wt', 'mpg', color='factor(gear)'))
+ geom_point()
+ stat_smooth(method='lm')
+ facet_wrap('~gear'))

3262887070-59fae2a6967b6_articlex

Seaborn

seaborn准确上说属于matplotlib的扩展包,在其上做了许多非常有用的封装,基本上可以满足大部分统计作图的需求,以matplotlib+seaborn基本可以满足大部分业务场景,语法也更加「陈述式」。

缺点是封装较高,基本上API不提供的图就完全不可绘制,对于各类图的拼合也不适合;此外配置语句语法又回归「命令式」,相对复杂且不一致。

1
2
3
4
5
复制代码## seaborn示例
import seaborn as sns; sns.set(color_codes=True)
iris = sns.load_dataset("iris")
species = iris.pop("species")
g = sns.clustermap(iris)

3262887070-59fae2a6967b6_articlex

plotly

plotly是跨平台JavaScript交互式绘图包,由于开发者的核心是javascript,所以整个语法类似于写json配置,语法特质也介于「陈述式」和「命令式」之间,无服务版本是免费的。

有点是学习成本不高,可以很快将语句移植到javascript版本;缺点是语言相对繁琐。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
复制代码##plotly示例
import plotly.plotly as py
import plotly.graph_objs as go
 
# Add data
month = ['January', 'February', 'March', 'April', 'May', 'June', 'July',
         'August', 'September', 'October', 'November', 'December']
high_2000 = [32.5, 37.6, 49.9, 53.0, 69.1, 75.4, 76.5, 76.6, 70.7, 60.6, 45.1, 29.3]
low_2000 = [13.8, 22.3, 32.5, 37.2, 49.9, 56.1, 57.7, 58.3, 51.2, 42.8, 31.6, 15.9]
high_2007 = [36.5, 26.6, 43.6, 52.3, 71.5, 81.4, 80.5, 82.2, 76.0, 67.3, 46.1, 35.0]
low_2007 = [23.6, 14.0, 27.0, 36.8, 47.6, 57.7, 58.9, 61.2, 53.3, 48.5, 31.0, 23.6]
high_2014 = [28.8, 28.5, 37.0, 56.8, 69.7, 79.7, 78.5, 77.8, 74.1, 62.6, 45.3, 39.9]
low_2014 = [12.7, 14.3, 18.6, 35.5, 49.9, 58.0, 60.0, 58.6, 51.7, 45.2, 32.2, 29.1]
 
# Create and style traces
trace0 = go.Scatter(
    x = month,
    y = high_2014,
    name = 'High 2014',
    line = dict(
        color = ('rgb(205, 12, 24)'),
        width = 4)
)
trace1 = go.Scatter(
    x = month,
    y = low_2014,
    name = 'Low 2014',
    line = dict(
        color = ('rgb(22, 96, 167)'),
        width = 4,)
)
trace2 = go.Scatter(
    x = month,
    y = high_2007,
    name = 'High 2007',
    line = dict(
        color = ('rgb(205, 12, 24)'),
        width = 4,
        dash = 'dash') # dash options include 'dash', 'dot', and 'dashdot'
)
trace3 = go.Scatter(
    x = month,
    y = low_2007,
    name = 'Low 2007',
    line = dict(
        color = ('rgb(22, 96, 167)'),
        width = 4,
        dash = 'dash')
)
trace4 = go.Scatter(
    x = month,
    y = high_2000,
    name = 'High 2000',
    line = dict(
        color = ('rgb(205, 12, 24)'),
        width = 4,
        dash = 'dot')
)
trace5 = go.Scatter(
    x = month,
    y = low_2000,
    name = 'Low 2000',
    line = dict(
        color = ('rgb(22, 96, 167)'),
        width = 4,
        dash = 'dot')
)
data = [trace0, trace1, trace2, trace3, trace4, trace5]
 
# Edit the layout
layout = dict(title = 'Average High and Low Temperatures in New York',
              xaxis = dict(title = 'Month'),
              yaxis = dict(title = 'Temperature (degrees F)'),
              )
 
fig = dict(data=data, layout=layout)
py.iplot(fig, filename='styled-line')

3262887070-59fae2a6967b6_articlex

注意:此框架在jupyter中使用需要使用init_notebook_mode()加载JavaScript框架。

bokeh

bokeh是pydata维护的比较具有潜力的开源交互可视化框架。

值得一说的是,该框架同时提供底层语句和「陈述式」绘图命令。相对来说语法也比较清楚,但其配置语句依旧有很多可视化框架的问题,就是与「陈述式」命令不符,没有合理的结构。此外,一些常见的交互效果都是以底层命令的方式使用的,因此如果要快速实现Dashboard或者作图时就显得较为不便了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
复制代码## Bokeh示例
import numpy as np
import scipy.special
 
from bokeh.layouts import gridplot
from bokeh.plotting import figure, show, output_file
 
p1 = figure(title="Normal Distribution (μ=0, σ=0.5)",tools="save",
            background_fill_color="#E8DDCB")
 
mu, sigma = 0, 0.5
 
measured = np.random.normal(mu, sigma, 1000)
hist, edges = np.histogram(measured, density=True, bins=50)
 
x = np.linspace(-2, 2, 1000)
pdf = 1/(sigma * np.sqrt(2*np.pi)) * np.exp(-(x-mu)**2 / (2*sigma**2))
cdf = (1+scipy.special.erf((x-mu)/np.sqrt(2*sigma**2)))/2
 
p1.quad(top=hist, bottom=0, left=edges[:-1], right=edges[1:],
        fill_color="#036564", line_color="#033649")
p1.line(x, pdf, line_color="#D95B43", line_width=8, alpha=0.7, legend="PDF")
p1.line(x, cdf, line_color="white", line_width=2, alpha=0.7, legend="CDF")
 
p1.legend.location = "center_right"
p1.legend.background_fill_color = "darkgrey"
p1.xaxis.axis_label = 'x'
p1.yaxis.axis_label = 'Pr(x)'
 
 
 
p2 = figure(title="Log Normal Distribution (μ=0, σ=0.5)", tools="save",
            background_fill_color="#E8DDCB")
 
mu, sigma = 0, 0.5
 
measured = np.random.lognormal(mu, sigma, 1000)
hist, edges = np.histogram(measured, density=True, bins=50)
 
x = np.linspace(0.0001, 8.0, 1000)
pdf = 1/(x* sigma * np.sqrt(2*np.pi)) * np.exp(-(np.log(x)-mu)**2 / (2*sigma**2))
cdf = (1+scipy.special.erf((np.log(x)-mu)/(np.sqrt(2)*sigma)))/2
 
p2.quad(top=hist, bottom=0, left=edges[:-1], right=edges[1:],
        fill_color="#036564", line_color="#033649")
p2.line(x, pdf, line_color="#D95B43", line_width=8, alpha=0.7, legend="PDF")
p2.line(x, cdf, line_color="white", line_width=2, alpha=0.7, legend="CDF")
 
p2.legend.location = "center_right"
p2.legend.background_fill_color = "darkgrey"
p2.xaxis.axis_label = 'x'
p2.yaxis.axis_label = 'Pr(x)'
 
 
 
p3 = figure(title="Gamma Distribution (k=1, θ=2)", tools="save",
            background_fill_color="#E8DDCB")
 
k, theta = 1.0, 2.0
 
measured = np.random.gamma(k, theta, 1000)
hist, edges = np.histogram(measured, density=True, bins=50)
 
x = np.linspace(0.0001, 20.0, 1000)
pdf = x**(k-1) * np.exp(-x/theta) / (theta**k * scipy.special.gamma(k))
cdf = scipy.special.gammainc(k, x/theta) / scipy.special.gamma(k)
 
p3.quad(top=hist, bottom=0, left=edges[:-1], right=edges[1:],
        fill_color="#036564", line_color="#033649")
p3.line(x, pdf, line_color="#D95B43", line_width=8, alpha=0.7, legend="PDF")
p3.line(x, cdf, line_color="white", line_width=2, alpha=0.7, legend="CDF")
 
p3.legend.location = "center_right"
p3.legend.background_fill_color = "darkgrey"
p3.xaxis.axis_label = 'x'
p3.yaxis.axis_label = 'Pr(x)'
 
 
 
p4 = figure(title="Weibull Distribution (λ=1, k=1.25)", tools="save",
            background_fill_color="#E8DDCB")
 
lam, k = 1, 1.25
 
measured = lam*(-np.log(np.random.uniform(0, 1, 1000)))**(1/k)
hist, edges = np.histogram(measured, density=True, bins=50)
 
x = np.linspace(0.0001, 8, 1000)
pdf = (k/lam)*(x/lam)**(k-1) * np.exp(-(x/lam)**k)
cdf = 1 - np.exp(-(x/lam)**k)
 
p4.quad(top=hist, bottom=0, left=edges[:-1], right=edges[1:],
       fill_color="#036564", line_color="#033649")
p4.line(x, pdf, line_color="#D95B43", line_width=8, alpha=0.7, legend="PDF")
p4.line(x, cdf, line_color="white", line_width=2, alpha=0.7, legend="CDF")
 
p4.legend.location = "center_right"
p4.legend.background_fill_color = "darkgrey"
p4.xaxis.axis_label = 'x'
p4.yaxis.axis_label = 'Pr(x)'
 
 
 
output_file('histogram.html', title="histogram.py example")
 
show(gridplot(p1,p2,p3,p4, ncols=2, plot_width=400, plot_height=400, toolbar_location=None))

3262887070-59fae2a6967b6_articlex

bqplot

bqplot是基于ipywidgets和d3.js组合发展的内核交互式的可视化框架。语法上采用了和matplotlib大致一致的语法已经相对封装较高的「陈述式语法」。优点是直接和内核交互,可以使用大量控件来实现更多的图像处理,缺点也是直接的,离线文档则不会显示任何图案、控件也都失效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
复制代码## bqplot示例
import numpy as np
from IPython.display import display
from bqplot import (
    OrdinalScale, LinearScale, Bars, Lines, Axis, Figure
)
 
size = 20
np.random.seed(0)
 
x_data = np.arange(size)
 
x_ord = OrdinalScale()
y_sc = LinearScale()
 
bar = Bars(x=x_data, y=np.random.randn(2, size), scales={'x': x_ord, 'y':
y_sc}, type='stacked')
line = Lines(x=x_data, y=np.random.randn(size), scales={'x': x_ord, 'y': y_sc},
             stroke_width=3, colors=['red'], display_legend=True, labels=['Line chart'])
 
ax_x = Axis(scale=x_ord, grid_lines='solid', label='X')
ax_y = Axis(scale=y_sc, orientation='vertical', tick_format='0.2f',
            grid_lines='solid', label='Y')
 
Figure(marks=[bar, line], axes=[ax_x, ax_y], title='API Example',
       legend_location='bottom-right')

3262887070-59fae2a6967b6_articlex

其他特殊需求的作图

除了统计作图,网络可视化和GIS可视化也是很常用的,在此只做一个简单的罗列:

GIS类:

  • gmap:交互,使用google maps接口
  • ipyleaflet:交互,使用leaflet接口

网络类:

  • networkx:底层为matplotlib
  • plotly

总结

底层实现 交互方式 语法 语言结构 备注 推荐程度
matplotlib – 无 命令式 底层语言 可以实现复杂底层操作 ★★★
gglot matplotlib 无 陈述式 类ggplot2 建议选择plotnine ★★
plotnine matplotlib 无 陈述式 类ggplot2 完全移植ggplot2 ★★★★★
seaborn matplotlib 无 陈述式 高级语言 有很多有用的统计图类的封装;但不适合做图拼装 ★★★★★
plotly plotly.js 前端交互 介于命令式和陈述式之间 类似JavaScript 语法类似于json配置 ★★★★
bokeh – 前端交互 命令、陈述式 同时有底层语言和高级语言 社区具有潜力 ★★★
bqplot d3.js 内核交互 命令、陈述式 有类似matplotlib底层语言,已经封装好的高级语言 内核交互 ★★★★

1 赞 收藏 评论

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

如何实现飞跃:在架构内构建云就绪应用

发表于 2017-11-15

本文要点

  • 云就绪(Cloud-Ready)!=云原生(Cloud-Native),后者比前者要进行更多的重建。
  • 云就绪的工作主要是修改部署过程,也可能要转变思维方式。
  • 云管理平台(CMP)是一个辅助工具。
  • 好处主要是可移植性、合理大小的云部署以及应对未来变化的灵活性,这样在处理这些老旧的应用的时候,你可以节省更多的时间。
  • 大处着眼,小处着手。不必一下子做完所有的事,那就从可控的开始吧。

在企业中,应用组合可能很复杂。 应用程序包含多样的需求和多种架构并不罕见,这些架构涉及的范围非常广,从电子邮件、维基和博客等协作项目到金融交易资产,再到人力资源的员工门户网站以及面向客户的营销网站。为了给这种多样化的需求寻找一种恰当的托管方式,我们有一种现代化的方法,那就是,在数据中心中运行一些应用程序,而其他应用程序在公有云中运行。研究表明,这样的混合云方法受到多达73%的组织的欢迎。

但这具体是什么意思呢?这些截然不同的应用直接在计算机上运行多年,而编写这些应用的开发人员早已离开公司,如何将这些应用变得云就绪(公有云或私有云)?

云就绪与云原生

云原生应用编写之初就是为在公有云上运行,通常意味着基于容器进行部署。这种架构在其初始设计中内置了水平自动扩展能力,并且通常依赖于云端衍生服务,如负载均衡器、对象存储、托管数据库和队列系统,这样它可以专注于具体任务的业务逻辑。通常,持续集成/持续交付工具链与这些应用相关联,因此敏捷软件开发方法可以快速地推出新的迭代。

也就是说,这不是这里讨论的那种应用。

经典的企业应用程序有多个组件,如Web服务器、应用服务器和数据库服务器。许多应用程序最初是在客户端-服务器时代编写的,要直接在硬件上运行它们。这些类型的应用程序尽管年代久远,但仍然可以变成云就绪的。基本上,组件通过TCP连接使用IP地址和端口号进行通信,通常由DNS辅助。这些应用程序的结构丝毫不会阻碍它们在虚拟机或甚至是容器上运行,它们如果可以在两者中任意一个上运行,那么也就可以部署到任何公有云或私有云上。

虽然这样的应用程序无法像云原生应用程序一样充分利用公有云提供的服务,但有时候,经典的企业应用程序可以变成云就绪的,并从中受益,而无需完全重写。这种转变的限制因素通常不是代码,因为如上所述,代码在公有云上运行时运行的上下文不会改变。相反,限制因素往往是部署机制,而检查应用程序生命周期的这个方面就能将经典的多层企业应用程序转变为云就绪的应用程序。

你如今如何部署?

在考虑现有的客户端-服务器时代应用程序的云就绪时,请问自己以下问题:应用程序如今如何部署?

最终要考虑生产环境,再三琢磨部署细节能了解使应用程序云就绪的难易程度。应用程序是五年前手动部署的吗,部署人员之后就退休了?这很可能表明,应用程序要变成云就绪还有很多问题。

应用程序有一套脚本来准备其Linux或Windows环境,然后自动安装自定义设置和依赖吗?离现在较近的应用程序有一组脚本,可以自动执行诸如准备操作系统内核和将IP地址注入到配置文件中的任务。这些过程和(或)脚本是过渡到云部署的关键,为云就绪指明了路线。在准备这些经典应用程序时,最重要的背景知识是,上述过程如何运行。

这两种极端的中间情况是,应用程序没有自动化脚本,但是有一本运行手册,详细说明了为应用程序准备特定环境所需的步骤。尽管没有完全使用脚本的场景那么容易转变成云就绪,也没有手动案例那么难,但这种情况描绘了一个需要根据具体情况仔细审查的灰色区域。

宠物与家畜

开始云就绪之前,另一个要考虑的部署方面涉及到思维方式的改变。对于这些较老的应用程序,根据其发布的年代,物理硬件当时是一种稀缺资源。获取新硬件需要几个月的时间,这影响了应用程序架构,这种架构考量物理服务器的养护,将软件发布视为风险。因此,我们将服务器看做宠物,给它们取名字,并尽我们所能,保持它们一直运行。如果在这些宠物般的服务器上所发布的新软件引入了难以恢复的风险,从零开始重新安装一台物理机器的成本是非常高的。

相比之下,采用虚拟机的方案能够控制额外的生命周期,带来了改进应用程序部署的可能性。在虚拟机世界中,计算资源可以被视为一次性实体,而不是稀缺的资源。例如,与其花费时间来升级VM的操作系统,不如使用新的操作系统创建一个新的VM,将其插入到负载均衡器池中,然后删除旧的VM。将VM看做家畜,带来了诸如水平自动扩展和更快的发布周期等优点,因为计算资源的稀缺性被最小化了。

即便不是全部,但有一些遗留企业应用程序适合这种云就绪的现代化方式。具体地说,不同程度地使用负载均衡器的应用或者允许节点插入的应用都是非常适合采用这种方式进行改善的。无论如何,在云就绪过程中,了解这两种方法之间的差异非常重要。

工具

云管理平台(CMP)专门用于辅助云就绪相关的准备工作。它提供了一种机制,了解特定应用程序部署的人员可以建立一个描述每个应用程序组件以及它们如何彼此交互的蓝图或资料。

CMP通常提供常用组件的实现,如上所示的HAProxy、Apache和MySQL。想要自己实现这些层的组织可以创建自己的组件,也可以在每一层上注入脚本来自定义安装。

例如,在上面的屏幕截图中,可能需要一些自定义的MySQL配置,这超出了CMP提供的基本安装的功能范围,比如我们想在操作系统上安装特定监控或安全软件。这里就能很好地用到部署过程的知识,以确定对目前正在使用的基本组件的补充之处。

CMP的核心优势在于,它将具体的公有云和私有云的细节抽象化,从而简化了经典企业应用程序向云就绪过渡的过程。例如,IT人员不必成为Google Cloud Platform、Amazon Web Services和VMware API的专家,而是要努力将应用程序抽象为CMP表示形式。虽然可以直接使用新的云API编写部署脚本将应用程序变得云就绪,但如果将来选择了不同的云,那么部署脚本也得修改。通过使用CMP的抽象来掩盖这些API细节,这可以一次完成,通常比直接使用API的方法更省力。

好处与如何开始

将经典企业应用程序转变成云就绪,使用CMP最大的好处是,可移植性和易用性。更进一步,可以将水平自动扩展和蓝绿部署注入到新的部署脚本中,从而为一些应用程序提供额外的好处。一些CMP甚至能够针对云部署运行基准测试,以便可以正确选择每个应用程序层的VM大小,甚至可以使用价格和性能标准彼此进行比较。这能够确保,应用程序满足业务需求的情况下,应用程序运行在最高效的云上,同时可以使用与初次云就绪时相同的CMP抽象轻松地迁移云平台。

大多数公司最开始所采用的部署机制都是移植那些低需求、无复杂高可用性需求且最初的部署专家仍然在职的应用程序。这降低了首次尝试云就绪转变的风险。早期成功之后,就可以评估应用程序组合的其余部分,并排定云就绪的优先级。通常,这意味着推后需求程度高、可用性要求高或专家早已不在的应用程序。

关于作者

作为思科全球合作伙伴组织的云技术解决方案架构师,Pete Johnson是一名有着20多年技术行业经验的资深人士,在Twitter上关注@nerdguru可以联系到他。

查看英文原文:www.infoq.com/articles/cl…

感谢张卫滨对本文的审校。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Go示例集合

发表于 2017-11-15

Go 示例集合 记录一下

Go 字典

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
复制代码//Go 字典

package main

import (
"fmt"
)

func main() {

dictionary := make(map[string]int)

dictionary["k1"] = 7
dictionary["k2"] = 10

//输出字典
fmt.Println("map:", dictionary)
//获取一个键的值
name := dictionary["k1"]
fmt.Println("name:", name)

//内置函数len 返回字典元素的个数
fmt.Println("len:", len(dictionary))

//内置函数delete 从字典删除一个键对应的值
delete(dictionary, "k2")
fmt.Println("map:", dictionary)

// 根据键来获取值有一个可选的返回值,这个返回值表示字典中是否
// 存在该键,如果存在为true,返回对应值,否则为false,返回零值
// 有的时候需要根据这个返回值来区分返回结果到底是存在的值还是零值
// 比如字典不存在键x对应的整型值,返回零值就是0,但是恰好字典中有
// 键y对应的值为0,这个时候需要那个可选返回值来判断是否零值。

_, ok := dictionary["k2"]
fmt.Println("ok:", ok)

//可以使用 ":=" 同时定义和初始化一个字典
myMap := map[string]int{"foo": 1, "bar": 2}
fmt.Println("map:", myMap)

}
输出 map: map[k1:7 k2:10]
name: 7
len: 2
map: map[k1:7]
ok: false
map: map[foo:1 bar:2]

Go string操作函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
复制代码//Go 字符串操作函数

package main

import (
"fmt"
s "strings"
)

var p = fmt.Println

func main() {

// 下面是strings包里面提供的一些函数实例。注意这里的函数并不是
// string对象所拥有的方法,这就是说使用这些字符串操作函数的时候
// 你必须将字符串对象作为第一个参数传递进去。
p("Contains: ", s.Contains("test", "es"))
p("Count: ", s.Count("test", "t"))
p("HasPrefix: ", s.HasPrefix("test", "te"))
p("HasSuffix:", s.HasSuffix("test", "st"))
p("Index:", s.Index("test", "e"))
p("Join:", s.Join([]string{"a", "b"}, "-"))
p("Repeat:", s.Repeat("a", 5))
p("Replace:", s.Replace("foo", "o", "0", -1))
p("Replace:", s.Replace("foo", "o", "0", 1))
p("Split:", s.Split("a-b-c-d-e", "-"))
p("ToLower:", s.ToLower("TEST"))
p("ToUpper:", s.ToUpper("test"))
p()

//这两个方法不是string包函数
//获取字符串长度
p("Len: ", len("hello"))
//获取指定索引的字符
p("Char:", "hello"[1])

}

输出:
Contains: true
Count: 2
HasPrefix: true
HasSuffix: true
Index: 1
Join: a-b
Repeat: aaaaa
Replace: f00
Replace: f0o
Split: [a b c d e]
ToLower: test
ToUpper: TEST

Len: 5
Char: 101

Go 字符串格式化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
复制代码//Go 字符串格式化

package main

import (
"fmt"
"os"
)

type point struct {
x, y int
}

func main() {

// Go提供了几种打印格式,用来格式化一般的Go值,例如 下面的%v打印了一个point结构体的对象的值
p := point{1, 2}
fmt.Printf("%v\n", p)
// 如果所格式化的值是一个结构体对象,那么`%+v`的格式化输出将包括结构体的成员名称和值
fmt.Printf("%+v\n", p)
// `%#v`格式化输出将输出一个值的Go语法表示方式。
fmt.Printf("%#v\n", p)
// 使用`%T`来输出一个值的数据类型
fmt.Printf("%T\n", p)
// 格式化布尔型变量
fmt.Printf("%t\n", true)
// 格式化布尔型变量
fmt.Printf("%t\n", true)
// 有很多的方式可以格式化整型,使用`%d`是一种标准的以10进制来输出整型的方式
fmt.Printf("%d\n", 123)
// 这种方式输出整型的二进制表示方式
fmt.Printf("%b\n", 14)
// 这里打印出该整型数值所对应的字符
fmt.Printf("%c\n", 33)
// 使用`%x`输出一个值的16进制表示方式
fmt.Printf("%x\n", 456)
// 浮点型数值也有几种格式化方法。最基本的一种是`%f`
fmt.Printf("%f\n", 78.9)
// `%e`和`%E`使用科学计数法来输出整型
fmt.Printf("%e\n", 123400000.0)
fmt.Printf("%E\n", 123400000.0)
// 使用`%s`输出基本的字符串
fmt.Printf("%s\n", "\"string\"")
// 输出像Go源码中那样带双引号的字符串,需使用`%q`
fmt.Printf("%q\n", "\"string\"")
// `%x`以16进制输出字符串,每个字符串的字节用两个字符输出
fmt.Printf("%x\n", "hex this")
// 使用`%p`输出一个指针的值
fmt.Printf("%p\n", &p)
// 当输出数字的时候,经常需要去控制输出的宽度和精度。
//可以使用一个位于%后面的数字来控制输出的宽度,默认情况下输出是右对齐的,左边加上空格
fmt.Printf("|%6d|%6d|\n", 12, 345)
// 你也可以指定浮点数的输出宽度,同时你还可以指定浮点数的输出精度
fmt.Printf("|%6.2f|%6.2f|\n", 1.2, 3.45)
// To left-justify, use the `-` flag.
fmt.Printf("|%-6.2f|%-6.2f|\n", 1.2, 3.45)
// 你也可以指定输出字符串的宽度来保证它们输出对齐。默认 // 情况下,输出是右对齐的
fmt.Printf("|%6s|%6s|\n", "foo", "b")
// 为了使用左对齐你可以在宽度之前加上`-`号
fmt.Printf("|%-6s|%-6s|\n", "foo", "b")
// 可以用`Sprintf`来将格式化后的字符串赋值给一个变量
s := fmt.Sprintf("a %s", "string")
fmt.Println(s)
// 也可以使用`Fprintf`来将格式化后的值输出到`io.Writers`
fmt.Fprintf(os.Stderr, "an %s\n", "error")
}
輸出
{1 2}
{x:1 y:2}
main.point{x:1, y:2}
main.point
true
true
123
1110
!
1c8
78.900000
1.234000e+08
1.234000E+08
"string"
"\"string\""
6865782074686973
0xc42006e1a0
| 12| 345|
| 1.20| 3.45|
|1.20 |3.45 |
| foo| b|
|foo |b |
a string
an error

Go Base64编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
复制代码// Go Base64编码
package main

import (
b64 "encoding/base64"
"fmt"
)

func main() {

// 这里是我们用来演示编码和解码的字符串
data := "abc123!?$*&()'-=@~"

// Go支持标准的和兼容URL的base64编码。
// 我们这里使用标准的base64编码,这个
// 函数需要一个`[]byte`参数,所以将这个字符串转换为字节数组
sEnc := b64.StdEncoding.EncodeToString([]byte(data))
fmt.Println(sEnc)
// 解码一个base64编码可能返回一个错误,
// 如果你不知道输入是否是正确的base64编码,你需要检测一些解码错误
sDec, _ := b64.StdEncoding.DecodeString(sEnc)
fmt.Println(string(sDec))
fmt.Println()
// 使用兼容URL的base64编码和解码
uEnc := b64.URLEncoding.EncodeToString([]byte(data))
fmt.Println(uEnc)
uDec, _ := b64.URLEncoding.DecodeString(uEnc)
fmt.Println(string(uDec))
}
输出
YWJjMTIzIT8kKiYoKSctPUB+
abc123!?$*&()'-=@~

YWJjMTIzIT8kKiYoKSctPUB-
abc123!?$*&()'-=@~

这两种方法都将原数据编码为base64编码,区别在于标准的编码后面是 + ,而兼容URL的编码方式后面 是-。

Go range函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
复制代码//Go range
package main

import (
"fmt"
)

func main() {

// 这里我们使用range来计算一个切片的所有元素和 // 这种方法对数组也适用
nums := []int{2, 3, 4}
sum := 0
for _, num := range nums {
sum += num
}
fmt.Println("sum:", sum)
// range 用来遍历数组和切片的时候返回索引和元素值
// 如果我们不要关心索引可以使用一个下划线(_)来忽略这个返回值 // 当然我们有的时候也需要这个索引
for i, num := range nums {
if num == 3 {
fmt.Println("index:", i)
}
}
// 使用range来遍历字典的时候,返回键值对。
kvs := map[string]string{"a": "apple", "b": "banana"}
for k, v := range kvs {
fmt.Printf("%s -> %s\n", k, v)
}
// range函数用来遍历字符串时,返回Unicode代码点。
// 第一个返回值是每个字符的起始字节的索引,第二个是字符代码点,
// 因为Go的字符串是由字节组成的,多个字节组成一个rune类型字符。
for i, c := range "go" {
fmt.Println(i, c)

}
}
输出
sum: 9
index: 1
a -> apple
b -> banana
0 103
1 111

Go sha1 散列

SHA1散列经常用来计算二进制或者大文本数据的短标识值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
复制代码//Go sha1 散列
package main

import (
"crypto/sha1"
"fmt"
)

func main() {
s := "sha1 this string"
// 生成一个hash的模式是sha1.New(),sha1.Write(bytes)
// 然后是sha1.Sum([]byte{})
h := sha1.New()
// 写入要hash的字节,如果你的参数是字符串,使用[]byte(s)
// 把它强制转换为字节数组
h.Write([]byte(s))
// 这里计算最终的hash值,Sum的参数是用来追加而外的字节到要
// 计算的hash字节里面,一般来讲,如果上面已经把需要hash的字节都写入了,这里就设为nil就可以了
bs := h.Sum(nil)
// SHA1散列值经常以16进制的方式输出,例如git commit就是
// 这样,所以可以使用`%x`来将散列结果格式化为16进制的字符串
fmt.Println(s)
fmt.Printf("%x\n", bs)
}
输出
sha1 this string
cf23df2207d99a74fbe169e3eba035e633b65d94

长期更新

github地址:github.com/sallenhando…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

监控利器之 Prometheus

发表于 2017-11-15

一直以来,我们会在项目中,使用 APM 去监控应用的状况,分析性能等,这些工具很有效,而且不侵入业务,不需要埋点。

然而,有些需求,是 APM 的监控满足不了的,比如 应用业务指标 。

监控模式

目前,采集指标有两种方式,一种是『推』,另一种就是『拉』:

推的代表有 ElasticSearch,InfluxDB,OpenTSDB 等,需要你从程序中将指标使用 TCP,UDP 等方式推送至相关监控应用,只是使用 TCP 的话,一旦监控应用挂掉或存在瓶颈,容易对应用本身产生影响,而使用 UDP 的话,虽然不用担心监控应用,但是容易丢数据。

拉的代表,主要代表就是 Prometheus,让我们不用担心监控应用本身的状态。而且,可以利用 DNS-SRV 或者 Consul 等服务发现功能就可以自动添加监控。

当然,InfluxDB 加上 collector,或者 ES 加上 metricbeat 也可以变为 『拉』,而 Prometheus 加上 Push Gateway 也可以变为 『推』。

接下来,我们主要介绍下 Prometheus。

Prometheus

『普罗米修斯』,也是希腊之神,取义『先见之明』,应该就是监控的意义所在吧。

它跟 k8s 一样,也是依据 Google 内部的应用原理设计来的,可以看作是 Google 内部监控系统 Borgmon 的一个实现。

架构图如下(来自 Prometheus 官方文档):

Prometheus 可以从配置或者用服务发现,去调用各个应用的 metrics 接口,来采集数据,然后存储在硬盘中,而如果是基础应用比如数据库,负载均衡器等,可以在相关的服务中安装 Exporters 来提供 metrics 接口供 Prometheus 拉取。

采集到的数据有两个去向,一个是报警,另一个是可视化。

下面将一一介绍。

Metrics 格式

1
复制代码<metric name>{<label name>=<label value>, ...}

各个部分需符合相关的正则表达式

  • metric name: [a-zA-Z:][a-zA-Z0-9:]*
  • label name: [a-zA-Z0-9_]*
  • label value: .* (即不限制)

需要注意的是,label value 最好使用枚举值,而不要使用无限制的值,比如用户 ID,Email 等,不然会消耗大量内存,也不符合指标采集的意义。

Metrics 接口的实现

大部分语言都有提供客户端,比如 Node.js 的客户端 prom-client:

1
复制代码npm install prom-client --save

目前,这个客户端提供了完整功能,可以在应用中埋点采集数据,比如

  • 今天注册了多少用户,收入了多少钱,可以使用 Counter;
  • Node 内存以及 CPU 的变化,可以使用 Gause;
  • API 接口响应时间的统计,可以使用 Histogram 或者 Summary,前者可以按照具体数值,而后者可以按照百分比去统计响应时长;

对了,这个包内部提供了采集默认数据的功能,比如 Node 相关的指标:

1
2
3
4
5
复制代码const promClient = require('prom-client');

promClient.collectDefaultMetrics({
timeout: 5000,
});

报警

你可以根据业务需求,来定制相关的规则去报警,然后关键就来了,你是否在传统的短信或者邮件报警中感到厌烦呢?

一方面,当线上问题出现的时候,我们会收到大量的报警消息,而其中很大一部分是重复的;另一方面,收到没用的报警,或者报警级别不高,导致这时候如果有重要的报警,会被我们忽略。

Prometheus 的 AlertManager 提供了解决这些问题的各种高级报警功能。

  • 报警分组 :将报警分组,当报警大量出现的时候,只会发一条消息告诉你数据库挂了的情况出现了 100 次,而不是用 100 条推送轰炸你;
  • 报警抑制 :显然,当数据库出问题的时候,其它的应用可肯定会出问题,这时候你可能不会需要其它的不相干的报警短信,这个功能将真正有用的信息及时通知你;
  • 报警静默 :一些不重要的报警,可以完全忽略,因此也就没有必要通知;

报警通知的方式,目前可以通过 webhook, email 等方式,估计微信或者钉钉也可以,我目前使用的是 slack。

可视化

首选当然是 Grafana,Prometheus 自己放弃了 PromDash 的可视化工具,而专注于监控数据采集与分析。在 Grafana 中配置 Prometheus 也很简单,在配置好数据源之后,可以直接创建图表。

需要注意的是,你会需要用到 Prometheus 专用的 查询语言 去配置数据,其中如果涉及到的图表内容太多,你可能会需要用到 Grafana 的模板:

  • label_values(label):全局中 label 值的集合;
  • label_values(metric, label):某个 metric 的 label 值的集合;
  • metrics(metric):metric 的正则表达集合,返回全部匹配的 metric;
  • query_result(query):返回查询集合;

Ref

  • prometheus.io/docs/operat…
  • prometheus.io/blog/2015/0…
  • docs.grafana.org/features/da…

原链接: github.com/xizhibei/bl…

知识共享许可协议知识共享许可协议

本文采用 署名 - 非商业性使用 - 相同方式共享(BY-NC-SA) 进行许可。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

redis源码分析之事务Transaction(上)

发表于 2017-11-15

这周学习了一下redis事务功能的实现原理,本来是想用一篇文章进行总结的,写完以后发现这块内容比较多,而且多个命令之间又互相依赖,放在一篇文章里一方面篇幅会比较大,另一方面文章组织结构会比较乱,不容易阅读。因此把事务这个模块整理成上下两篇文章进行总结。

原文地址:www.jianshu.com/p/acb97d620…

这篇文章我们重点分析一下redis事务命令中的两个辅助命令:watch跟unwatch。

一、redis事务辅助命令简介

依然从server.c文件的命令表中找到相应的命令以及它们对应的处理函数。

1
2
3
复制代码//watch,unwatch两个命令我们把它们叫做redis事务辅助命令
{"watch",watchCommand,-2,"sF",0,NULL,1,-1,1,0,0},
{"unwatch",unwatchCommand,1,"sF",0,NULL,0,0,0,0,0},
  1. watch,用于客户端关注某个key,当这个key的值被修改时,整个事务就会执行失败(注:该命令需要在事务开启前使用)。
  2. unwatch,用于客户端取消已经watch的key。

用法举例如下:
clientA

1
2
3
4
5
6
7
8
9
10
复制代码127.0.0.1:6379> watch a
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set b b
QUEUED
//在执行前插入clientB的操作如下,事务就会执行失败
127.0.0.1:6379> exec
(nil)
127.0.0.1:6379>

clientB

1
2
3
复制代码127.0.0.1:6379> set a aa
OK
127.0.0.1:6379>

二、redis事务辅助命令源码分析

在看具体执行函数之前首先了解几个数据结构:

1
2
3
4
5
6
7
8
9
10
11
复制代码//每个客户端对象中有一个watched_keys链表来保存已经watch的key
typedef struct client {
list *watched_keys;
}
//上述链表中每个节点的数据结构
typedef struct watchedKey {
//watch的key
robj *key;
//指向的DB,后面细说
redisDb *db;
} watchedKey;

关于事务的几个命令所对应的函数都放在了multi.c文件中。
一起看下watch命令对应处理函数的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
复制代码void watchCommand(client *c) {
int j;
//如果客户端处于事务状态,则返回错误信息
//由此可以看出,watch必须在事务开启前使用
if (c->flags & CLIENT_MULTI) {
addReplyError(c,"WATCH inside MULTI is not allowed");
return;
}
//依次watch客户端的各个参数(这里说明watch命令可以一次watch多个key)
//注:0表示命令本身,所以参数从1开始
for (j = 1; j < c->argc; j++)
watchForKey(c,c->argv[j]);
//返回结果
addReply(c,shared.ok);
}

//具体的watch操作,代码较长,慢慢分析
void watchForKey(client *c, robj *key) {
list *clients = NULL;
listIter li;
listNode *ln;
//上面已经提到了数据结构
watchedKey *wk;

//首先判断key是否已经被客户端watch
//listRewind这个函数在发布订阅那篇文章里也有,就是把客户端的watched_keys赋值给li
listRewind(c->watched_keys,&li);
while((ln = listNext(&li))) {
wk = listNodeValue(ln);
//这里一个wk节点中有db,key两个字段
if (wk->db == c->db && equalStringObjects(key,wk->key))
return;
}
//开始watch指定key
//整个watch操作保存了两套数据结构,一套是在db->watched_keys中的字典结构,如下:
clients = dictFetchValue(c->db->watched_keys,key);
//如果是key第一次出现,则进行初始化
if (!clients) {
clients = listCreate();
dictAdd(c->db->watched_keys,key,clients);
incrRefCount(key);
}
//把当前客户端加到该key的watch链表中
listAddNodeTail(clients,c);
//另一套是在c->watched_keys中的链表结构:如下
wk = zmalloc(sizeof(*wk));
//初始化各个字段
wk->key = key;
wk->db = c->db;
incrRefCount(key);
//加入到链表最后
listAddNodeTail(c->watched_keys,wk);
}

整个watch的数据结构比较复杂,我这里画了一张图方便理解:

watch数据结构

watch数据结构

简单解释一下上面的图,首先redis把每个客户端连接包装成了一个client对象,上图中db,watch_keys就是其中的两个字段(client对象里面还有很多其他字段,包括上篇文章中提到的pub/sub)。

  1. db字段指向给该client对象分配的储存空间,db对象中也含有一个watched_keys字段,是字典类型(也就是哈希表),以想要watch的key做key,存储的链表则是所有watch该key的客户端。
  2. watch_keys字段则是一个链表类型,每个节点类型为watch_key,其中包含两个字段,key表示watch的key,db则指向了当前client对象的db字段,如上图。

看完watch命令的源码以后,再来看一下unwatch命令,如果搞明白了上面提到的两套数据结构,那么看unwatch的源码应该会比较容易,毕竟就是删除数据结构中对应的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
复制代码void unwatchCommand(client *c) {
//取消watch所有key
unwatchAllKeys(c);
//修改客户端状态
c->flags &= (~CLIENT_DIRTY_CAS);
addReply(c,shared.ok);
}

//取消watch的key
void unwatchAllKeys(client *c) {
listIter li;
listNode *ln;
//如果客户端没有watch任何key,则直接返回
if (listLength(c->watched_keys) == 0) return;
//注意这里操作的是链表字段
listRewind(c->watched_keys,&li);
while((ln = listNext(&li))) {
list *clients;
watchedKey *wk;
//遍历取出该客户端watch的key
wk = listNodeValue(ln);
//取出所有watch了该key的客户端,这里则是字典(即哈希表)
clients = dictFetchValue(wk->db->watched_keys, wk->key);
//空指针判断
serverAssertWithInfo(c,NULL,clients != NULL);
//从watch列表中删除该客户端
listDelNode(clients,listSearchKey(clients,c));
//如果key只有一个当前客户端watch,则删除
if (listLength(clients) == 0)
dictDelete(wk->db->watched_keys, wk->key);
//从当前client的watch列表中删除该key
listDelNode(c->watched_keys,ln);
//减少引用数
decrRefCount(wk->key);
//释放内存
zfree(wk);
}
}

最后我们考虑一下watch机制的触发时机,现在我们已经把想要watch的key加入到了watch的数据结构中,可以想到触发watch的时机应该是修改key的内容时,通知到所有watch了该key的客户端。

感兴趣的用户可以任意选一个修改命令跟踪一下源码,例如set命令,我们发现所有对key进行修改的命令最后都会调用touchWatchedKey()函数,而该函数源码就位于multi.c文件中,该函数就是触发watch机制的关键函数,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码//这里入参db就是客户端对象中的db,上文已经提到,不赘述
void touchWatchedKey(redisDb *db, robj *key) {
list *clients;
listIter li;
listNode *ln;
//保存watchkey的字典为空,则返回
if (dictSize(db->watched_keys) == 0) return;
//注意这里操作的是字典(即哈希表)数据结构
clients = dictFetchValue(db->watched_keys, key);
//如果没有客户端watch该key,则返回
if (!clients) return;
//把client赋值给li
listRewind(clients,&li);
//遍历watch了该key的客户端,修改他们的状态
while((ln = listNext(&li))) {
client *c = listNodeValue(ln);
c->flags |= CLIENT_DIRTY_CAS;
}
}

跟我们猜测的一样,就是每当key的内容被修改时,则遍历所有watch了该key的客户端,设置相应的状态为CLIENT_DIRTY_CAS。

三、redis事务辅助命令总结

上面就是redis事务命令中watch,unwatch的实现原理,其中最复杂的应该就是watch对应的那两套数据结构了,跟之前的pub/sub类似,都是使用链表+哈希表的结构存储,另外也是通过修改客户端的状态位FLAG来通知客户端。

代码比较多,而且C++代码看上去会比较费劲,需要慢慢读,反复读。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java 设计模式之桥接模式(七)

发表于 2017-11-15

一、前言

本篇主题为结构型模式中的第二个模式–桥接模式。上篇 Java 设计模式主题为《Java 设计模式之适配器模式(六)》。

二、简单介绍

# 2.1 定义

桥接模式,是结构型的设计模式之一。桥接模式基于类的最小设计原则,通过使用封装,聚合以及继承等行为来让不同的类承担不同的责任。它的主要特点是把抽象(abstraction)与行为实现(implementation)分离开来,从而可以保持各部分的独立性以及应对它们的功能扩展。

# 2.2 应用场景

处理多层继承结构,多维度变化的场景。将各个维度设计成独立的继承结构,使得各个维度可以独立的扩展。

三、实现方式

我们以电脑为例,在网上商城中,我们可以看到电脑的分类菜单,分类结构如下图所示:

图中将电脑结构分成 3 级,第 1 级就是商品名称(电脑),第 2 级是电脑类型分类,第 3 级是品牌分类。现在,我们试着通过代码实现上图的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
复制代码public interface Computer {

void info();

}



class Desktop implements Computer {

@Override

public void info() {

System.out.println("台式电脑!");

}

}



class Laptop implements Computer {

@Override

public void info() {

System.out.println("笔记本电脑!");

}

}



class LenovoDesktop extends Desktop {

@Override

public void info() {

System.out.println("联想台式电脑");

}

}



class AsusDesktop extends Desktop {

@Override

public void info() {

System.out.println("华硕台式电脑");

}

}



class LenovoLaptop extends Laptop {

@Override

public void info() {

System.out.println("联想笔记本电脑");

}

}



class AsusLaptop extends Laptop {

@Override

public void info() {

System.out.println("华硕笔记本电脑");

}

}

上述代码通过继承方式实现了图中的电脑分类结构。但是,使用继承的方式存在几个问题:

  1. 扩展性问题:如果新增平板电脑分类,需要添加 N 个品牌子类。如果新增索尼品牌,其他电脑分类也需要添加相应子类。
  2. 违背单一职责原则:新增类型或新增品牌都会导致第 3 级目录结构的变化。

下面,我们使用桥接模式解决上述问题:

我们从 2 个维度分类,将它们分离独立出来,即将电脑类型和电脑品牌分离出来。

品牌维度:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
复制代码public interface Brand {

void info();

}



class LenovoBrand implements Brand {



@Override

public void info() {

System.out.println("联想");

}



}



class AsusBrand implements Brand {



@Override

public void info() {

System.out.println("华硕");

}



}

类型维度:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
复制代码public abstract class Computer {



protected Brand brand;



public Computer(Brand brand) {

this.brand = brand;

}



public void info() {

this.brand.info();

}

}



class Desktop extends Computer {



public Desktop(Brand brand) {

super(brand);

}



public void info() {

super.info();

System.out.println("台式电脑");

}



}



class Laptop extends Computer {



public Laptop(Brand brand) {

super(brand);

}



public void info() {

super.info();

System.out.println("笔记本电脑");

}



}

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码public class Client {



public static void main(String[] args) {



Computer computer = new Desktop(new LenovoBrand());



computer.info();

}

}

结果打印:

1
2
3
复制代码联想

台式电脑

当我们在其中一个维度上的代码进行修改时并不是影响到另一个维度的代码,这样遵循了单一职责原则,同时代码结构也变得灵活起来。

UML 类图表示如下:

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Linux 内核通用链表学习

发表于 2017-11-15

描述

在linux内核中封装了一个通用的双向链表库,这个通用的链表库有很好的扩展性和封装性,它给我们提供了一个固定的指针域结构体,我们在使用的时候,只需要在我们定义的数据域结构体中包含这个指针域结构体就可以了,具体的实现、链接并不需要我们关心,只要调用提供给我们的相关接口就可以完成了。

传统的链表结构
1
2
3
4
5
6
复制代码struct node{
int key;
int val;
node* prev;
node* next;
}
linux 内核通用链表库结构
1
2
3
4
5
6
7
8
9
10
11
复制代码提供给我们的指针域结构体:
struct list_head {
struct list_head *next, *prev;
};

我们只需要包含它就可以:
struct node{
int val;
int key;
struct list_head* head;
}

可以看到通过这个 list_head 结构就把我们的数据层跟驱动层分开了,而内核提供的各种操作方法接口也只关心 list_head 这个结构,也就是具体链接的时候也只链接这个list_head 结构,并不关心你数据层定义了什么类型.

一些接口宏定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码//初始化头指针
#define LIST_HEAD_INIT(name) { &(name), &(name) }

#define LIST_HEAD(name) \
struct list_head name = LIST_HEAD_INIT(name)

//遍历链表
#define __list_for_each(pos, head) \
for (pos = (head)->next; pos != (head); pos = pos->next)

//获取节点首地址(不是list_head地址,是数据层节点首地址)
#define list_entry(ptr, type, member) \
container_of(ptr, type, member)

//container_of在Linux内核中是一个常用的宏,用于从包含在某个
//结构中的指针获得结构本身的指针,通俗地讲就是通过结构体变
//量中某个成员的首地址进而获得整个结构体变量的首地址
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})

#define offsetof(s,m) (size_t)&(((s *)0)->m)

使用方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
复制代码typedef struct node{
int val;
int key;
struct list_head* list;
}node;

//初始化头指针
LIST_HEAD(head);

//创建节点
node* a = malloc(sizeof(node));
node* b = malloc(sizeof(node));

//插入链表 方式一
list_add(&a->list,&head);
list_add(&b->list,&head);

//插入链表 方式二
list_add_tail(&a->list,&head);
list_add_tail(&b->list,&head);

//遍历链表
struct list_head* p;
struct node* n;
__list_for_each(p,head){
//返回list_head地址,然后再通过list_head地址反推
//节点结构体首地址.
n = list_entry(pos,struct node,list);
}

list_add 接口,先入后出原则,有点类似于栈

list_add-先入后出模式
list_add-先入后出模式

list_add_tail 接口,先入先出原则,有点类似于fifo

list_add-先入先出模式
list_add-先入先出模式

我们的链表节点,实际在内存中的展示形态

节点描述
节点描述

可以看到最终的形态是,通过指向每个结构体里面的 list_head 类型指针,然后把它们串联起来的

list_entry 接口,通过结构体变量某个成员的地址,反推结构体首地址,就像 __list_for_each 接口只返回 list_head 地址,所以我们要通过这个成员地址在去获取它本身的结构体首地址,底层实现方法 container_of 宏

反推结构体首地址
反推结构体首地址

结束

linux 内核提供的这个通用链表库里面还有很多其他的接口,这里没有详细的一一举例,有兴趣的可以自己去看看,在源码包 include/linux/list.h 文件里面,不过通过阅读一些源代码确实对我们也有很大的提高,可以看看高手是如何去设计并实现,还可以学到一些技巧以及对代码细节的掌握~~.

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

underscore 系列之如何写自己的 underscor

发表于 2017-11-15

underscore系列第一篇,讲解 underscore 的代码组织方式

前言

在 《JavaScript 专题系列》 中,我们写了很多的功能函数,比如防抖、节流、去重、类型判断、扁平数组、深浅拷贝、查找数组元素、通用遍历、柯里化、函数组合、函数记忆、乱序等,可以我们该如何组织这些函数,形成自己的一个工具函数库呢?这个时候,我们就要借鉴 underscore 是怎么做的了。

自己实现

如果是我们自己去组织这些函数,我们该怎么做呢?我想我会这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码(function(){
var root = this;

var _ = {};

root._ = _;

// 在这里添加自己的方法
_.reverse = function(string){
return string.split('').reverse().join('');
}

})()

_.reverse('hello');
=> 'olleh'

我们将所有的方法添加到一个名为 _ 的对象上,然后将该对象挂载到全局对象上。

之所以不直接 window._ = _ 是因为我们写的是一个工具函数库,不仅要求可以运行在浏览器端,还可以运行在诸如 Node 等环境中。

root

然而 underscore 可不会写得如此简单,我们从 var root = this 开始说起。

之所以写这一句,是因为我们要通过 this 获得全局对象,然后将 _ 对象,挂载上去。

然而在严格模式下,this 返回 undefined,而不是指向 Window,幸运的是 underscore 并没有采用严格模式,可是即便如此,也不能避免,因为在 ES6 中模块脚本自动采用严格模式,不管有没有声明 use strict。

如果 this 返回 undefined,代码就会报错,所以我们的思路是对环境进行检测,然后挂载到正确的对象上。我们修改一下代码:

1
2
复制代码var root = (typeof window == 'object' && window.window == window && window) ||
(typeof global == 'object' && global.global == global && global);

在这段代码中,我们判断了浏览器和 Node 环境,可是只有这两个环境吗?那我们来看看 Web Worker。

Web Worker

Web Worker 属于 HTML5 中的内容,引用《JavaScript权威指南》中的话就是:

在 Web Worker 标准中,定义了解决客户端 JavaScript 无法多线程的问题。其中定义的 “worker” 是指执行代码的并行过程。不过,Web Worker 处在一个自包含的执行环境中,无法访问 Window 对象和 Document 对象,和主线程之间的通信业只能通过异步消息传递机制来实现。

为了演示 Web Worker 的效果,我写了一个 demo,查看代码。

在 Web Worker 中,是无法访问 Window 对象的,所以 typeof window 和 typeof global 的结果都是 undefined,所以最终 root 的值为 false,将一个基本类型的值像对象一样添加属性和方法,自然是会报错的。

那么我们该怎么办呢?

虽然在 Web Worker 中不能访问到 Window 对象,但是我们却能通过 self 访问到 Worker 环境中的全局对象。我们只是要找全局变量挂载而已,所以完全可以挂到 self 中嘛。

而且在浏览器中,除了 window 属性,我们也可以通过 self 属性直接访问到 Winow 对象。

1
2
复制代码console.log(window.window === window); // true
console.log(window.self === window); // true

考虑到使用 self 还可以额外支持 Web Worker,我们直接将代码改成 self:

1
2
复制代码var root = (typeof self == 'object' && self.self == self && self) ||
(typeof global == 'object' && global.global == global && global);

node vm

到了这里,依然没完,让你想不到的是,在 node 的 vm 模块中,也就是沙盒模块,runInContext 方法中,是不存在 window,也不存在 global 变量的,查看代码。

但是我们却可以通过 this 访问到全局对象,所以就有人发起了一个 PR,代码改成了:

1
2
3
复制代码var root = (typeof self == 'object' && self.self == self && self) ||
(typeof global == 'object' && global.global == global && global) ||
this;

微信小程序

到了这里,还是没完,轮到微信小程序登场了。

因为在微信小程序中,window 和 global 都是 undefined,加上又强制使用严格模式,this 为 undefined,挂载就会发生错误,所以就有人又发了一个 PR,代码变成了:

1
2
3
4
复制代码var root = (typeof self == 'object' && self.self == self && self) ||
(typeof global == 'object' && global.global == global && global) ||
this ||
{};

这就是现在 v1.8.3 的样子。

虽然作者可以直接讲解最终的代码,但是作者更希望带着大家看看这看似普通的代码是如何一步步演变成这样的,也希望告诉大家,代码的健壮性,并非一蹴而就,而是汇集了很多人的经验,考虑到了很多我们意想不到的地方,这也是开源项目的好处吧。

函数对象

现在我们讲第二句 var _ = {};。

如果仅仅设置 为一个空对象,我们调用方法的时候,只能使用 `.reverse(‘hello’)` 的方式,实际上,underscore 也支持类似面向对象的方式调用,即:

1
复制代码_('hello').reverse(); // 'olleh'

再举个例子比较下两种调用方式:

1
2
3
4
5
6
7
8
9
复制代码// 函数式风格
_.each([1, 2, 3], function(item){
console.log(item)
});

// 面向对象风格
_([1, 2, 3]).each(function(item){
console.log(item)
});

可是该如何实现呢?

既然以 _([1, 2, 3]) 的形式可以执行,就表明 _ 不是一个字面量对象,而是一个函数!

幸运的是,在 JavaScript 中,函数也是一种对象,我们举个例子:

1
2
3
4
5
6
复制代码var _ = function() {};
_.value = 1;
_.log = function() { return this.value + 1 };

console.log(_.value); // 1
console.log(_.log()); // 2

我们完全可以将自定义的函数定义在 _ 函数上!

目前的写法为:

1
2
3
4
5
6
7
8
复制代码var root = (typeof self == 'object' && self.self == self && self) ||
(typeof global == 'object' && global.global == global && global) ||
this ||
{};

var _ = function() {}

root._ = _;

如何做到 _([1, 2, 3]).each(...)呢?即 函数返回一个对象,这个对象,如何调用挂在 函数上的方法呢?

我们看看 underscore 是如何实现的:

1
2
3
4
5
6
复制代码var _ = function(obj) {
if (!(this instanceof _)) return new _(obj);
this._wrapped = obj;
};

_([1, 2, 3]);

我们分析下 _([1, 2, 3]) 的执行过程:

  1. 执行 this instanceof _,this 指向 window ,window instanceof _ 为 false,!操作符取反,所以执行 new _(obj)。
  2. new _(obj) 中,this 指向实例对象,this instanceof _ 为 true,取反后,代码接着执行
  3. 执行 this._wrapped = obj, 函数执行结束
  4. 总结,_([1, 2, 3]) 返回一个对象,为 {_wrapped: [1, 2, 3]},该对象的原型指向 _.prototype

示意图如下:

_()示意图

_()示意图

然后问题来了,我们是将方法挂载到 _ 函数对象上,并没有挂到函数的原型上呐,所以返回了的实例,其实是无法调用 _ 函数对象上的方法的!

我们写个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码(function(){
var root = (typeof self == 'object' && self.self == self && self) ||
(typeof global == 'object' && global.global == global && global) ||
this ||
{};

var _ = function(obj) {
if (!(this instanceof _)) return new _(obj);
this._wrapped = obj;
}

root._ = _;

_.log = function(){
console.log(1)
}

})()

_().log(); // _(...).log is not a function

确实有这个问题,所以我们还需要一个方法将 _上的方法复制到 _.prototype 上,这个方法就是 _.mixin。

_.functions

为了将 _ 上的方法复制到原型上,首先我们要获得 _ 上的方法,所以我们先写个 _.functions 方法。

1
2
3
4
5
6
7
复制代码_.functions = function(obj) {
var names = [];
for (var key in obj) {
if (_.isFunction(obj[key])) names.push(key);
}
return names.sort();
};

isFunction 函数可以参考 《JavaScript专题之类型判断(下)》

_.mixin

现在我们可以写 mixin 方法了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码var ArrayProto = Array.prototype;
var push = ArrayProto.push;

_.mixin = function(obj) {
_.each(_.functions(obj), function(name) {
var func = _[name] = obj[name];
_.prototype[name] = function() {
var args = [this._wrapped];
push.apply(args, arguments);
return func.apply(_, args);
};
});
return _;
};

_.mixin(_);

each 方法可以参考 《JavaScript专题jQuery通用遍历方法each的实现》

值得注意的是:因为 _[name] = obj[name] 的缘故,我们可以给 underscore 拓展自定义的方法:

1
2
3
4
5
6
7
复制代码_.mixin({
addOne: function(num) {
return num + 1;
}
});

_(2).addOne(); // 3

至此,我们算是实现了同时支持面向对象风格和函数风格。

导出

终于到了讲最后一步 root._ = _,我们直接看源码:

1
2
3
4
5
6
7
8
复制代码if (typeof exports != 'undefined' && !exports.nodeType) {
if (typeof module != 'undefined' && !module.nodeType && module.exports) {
exports = module.exports = _;
}
exports._ = _;
} else {
root._ = _;
}

为了支持模块化,我们需要将 _ 在合适的环境中作为模块导出,但是 nodejs 模块的 API 曾经发生过改变,比如在早期版本中:

1
2
3
4
5
6
7
8
复制代码// add.js
exports.addOne = function(num) {
return num + 1
}

// index.js
var add = require('./add');
add.addOne(2);

在新版本中:

1
2
3
4
5
6
7
8
复制代码// add.js
module.exports = function(1){
return num + 1
}

// index.js
var addOne = require('./add.js')
addOne(2)

所以我们根据 exports 和 module 是否存在来选择不同的导出方式,那为什么在新版本中,我们还要使用 exports = module.exports = _ 呢?

这是因为在 nodejs 中,exports 是 module.exports 的一个引用,当你使用了 module.exports = function(){},实际上覆盖了 module.exports,但是 exports 并未发生改变,为了避免后面再修改 exports 而导致不能正确输出,就写成这样,将两者保持统一。

写个 demo 吧:

第一个 demo:

1
2
3
4
5
6
7
8
复制代码// exports 是 module.exports 的一个引用
module.exports.num = '1'

console.log(exports.num) // 1

exports.num = '2'

console.log(module.exports.num) // 2

第二个 demo:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码// addOne.js
module.exports = function(num){
return num + 1
}

exports.num = '3'

// result.js 中引入 addOne.js
var addOne = require('./addOne.js');

console.log(addOne(1)) // 2
console.log(addOne.num) // undefined

第三个 demo:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码// addOne.js
exports = module.exports = function(num){
return num + 1
}

exports.num = '3'

// result.js 中引入 addOne.js
var addOne = require('./addOne.js');

console.log(addOne(1)) // 2
console.log(addOne.num) // 3

最后为什么要进行一个 exports.nodeType 判断呢?这是因为如果你在 HTML 页面中加入一个 id 为 exports 的元素,比如

1
复制代码<div id="exports"></div>

就会生成一个 window.exports 全局变量,你可以直接在浏览器命令行中打印该变量。

此时在浏览器中,typeof exports != 'undefined' 的判断就会生效,然后 exports._ = _,然而在浏览器中,我们需要将 _ 挂载到全局变量上呐,所以在这里,我们还需要进行一个是否是 DOM 节点的判断。

源码

最终的代码如下,有了这个基本结构,你可以自由添加你需要使用到的函数了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
复制代码(function() {

var root = (typeof self == 'object' && self.self == self && self) ||
(typeof global == 'object' && global.global == global && global) ||
this || {};

var ArrayProto = Array.prototype;

var push = ArrayProto.push;

var _ = function(obj) {
if (obj instanceof _) return obj;
if (!(this instanceof _)) return new _(obj);
this._wrapped = obj;
};

if (typeof exports != 'undefined' && !exports.nodeType) {
if (typeof module != 'undefined' && !module.nodeType && module.exports) {
exports = module.exports = _;
}
exports._ = _;
} else {
root._ = _;
}

_.VERSION = '0.1';

var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;

var isArrayLike = function(collection) {
var length = collection.length;
return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
};

_.each = function(obj, callback) {
var length, i = 0;

if (isArrayLike(obj)) {
length = obj.length;
for (; i < length; i++) {
if (callback.call(obj[i], obj[i], i) === false) {
break;
}
}
} else {
for (i in obj) {
if (callback.call(obj[i], obj[i], i) === false) {
break;
}
}
}

return obj;
}

_.isFunction = function(obj) {
return typeof obj == 'function' || false;
};

_.functions = function(obj) {
var names = [];
for (var key in obj) {
if (_.isFunction(obj[key])) names.push(key);
}
return names.sort();
};

/**
* 在 _.mixin(_) 前添加自己定义的方法
*/
_.reverse = function(string){
return string.split('').reverse().join('');
}

_.mixin = function(obj) {
_.each(_.functions(obj), function(name) {
var func = _[name] = obj[name];
_.prototype[name] = function() {
var args = [this._wrapped];

push.apply(args, arguments);

return func.apply(_, args);
};
});
return _;
};

_.mixin(_);

})()

相关链接

  1. 《JavaScript专题之类型判断(下)》
  2. 《JavaScript专题jQuery通用遍历方法each的实现》

underscore 系列

underscore 系列目录地址:github.com/mqyqingfeng…。

underscore 系列预计写八篇左右,重点介绍 underscore 中的代码架构、链式调用、内部函数、模板引擎等内容,旨在帮助大家阅读源码,以及写出自己的 undercore。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SpringMvc4x高级配置(三) 服务器端推送技术之S

发表于 2017-11-15

一. 点睛

服务器端推送技术在我们日常开发中较为常用,可能早期很多人的解决方案是使用Ajax向服务器轮询消息,使浏览器尽可能第一时间获得服务端的消息,因为这种方式的轮询频率不好控制,所以大大增加了服务端的压力。

下面要介绍的服务端推送方案都是基于:当客户端向服务端发送请求,服务端会抓住这个请求不放,等有数据更新的时候才返回给客户端,当客户端接收到消息后,再向服务端发送请求,周而复始。这种方式的好处是减少了服务器的请求数量,大大减少了服务器的压力。

除了服务器端推送技术之外,还有一个另外的双向通信的技术——WebSocket,后面会在介绍Spring Boot的时候进行演示。

下面将提供基于SSE(Server Send Event 服务端发送事件)的服务器端推送和基于Servlet3.0+的异步方法特性的演示,其中第一种方式需要新式浏览器的支持,第二种方式是跨浏览器的。

二. 示例

  1. 演示控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码package org.light4j.springMvc4.web;

import java.util.Random;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class SseController {

@RequestMapping(value="/push",produces="text/event-stream") //①
public @ResponseBody String push(){
Random r = new Random();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "data:Testing 1,2,3" + r.nextInt() +"\n\n";
}

}

代码解释:

① 注意,这里使用输出的媒体类型为text/event-stream,这是服务器端SSE的支持,本例演示每5秒向浏览器推送随机消息。

  1. 演示页面

在src/main/resources下面新建sse.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
复制代码<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>SSE Demo</title>

</head>
<body>
<div id="msgFrompPush"></div>
<script type="text/javascript" src="<c:url value="assets/js/jquery.js" />"></script>
<script type="text/javascript">

if (!!window.EventSource) { //①
var source = new EventSource('push');
s='';
source.addEventListener('message', function(e) {//②
s+=e.data+"<br/>";
$("#msgFrompPush").html(s);

});

source.addEventListener('open', function(e) {
console.log("连接打开.");
}, false);

source.addEventListener('error', function(e) {
if (e.readyState == EventSource.CLOSED) {
console.log("连接关闭");
} else {
console.log(e.readyState);
}
}, false);
} else {
console.log("你的浏览器不支持SSE");
}
</script>
</body>
</html>

代码解释:

① EventSource对象只有新式的浏览器才有(Chrome,Firefox等)。EventSource的SSE的客户端。
② 添加SSE客户端监听,在此获得服务器端推送的消息。

  1. 配置

在文件MyMvcConfig的方法addViewControllers中添加viewController映射访问演示页面sse.jsp的映射,代码如下:

1
复制代码 registry.addViewController("/sse").setViewName("/sse");

添加完成之后的代码如下所示:

1
2
3
4
5
6
7
复制代码@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/index").setViewName("/index");
registry.addViewController("/toUpload").setViewName("/upload");
registry.addViewController("/converter").setViewName("/converter");
registry.addViewController("/sse").setViewName("/sse");
}
  1. 运行。

访问http://localhost/springMvc4.x-serverSendEvent/sse,可以看到效果如下图所示:

三. 源代码示例:

github地址:点击查看
码云地址:点击查看

打赏 欢迎关注人生设计师的微信公众账号
公众号ID:longjiazuoA

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

微服务架构的基础框架选择:Spring Cloud还是Dub

发表于 2017-11-14

最近一段时间不论互联网还是传统行业,凡是涉及信息技术范畴的圈子几乎都在讨论微服务架构。近期也看到各大技术社区开始组织一些沙龙和论坛来分享Spring Cloud的相关实施经验,这对于最近正在整理Spring Cloud相关套件内容与实例应用的我而言,还是有不少激励的。

目前,Spring Cloud在国内的知名度并不高,在前阵子的求职过程中,与一些互联网公司的架构师、技术VP或者CTO在交流时,有些甚至还不知道该项目的存在。可能这也与国内阿里巴巴开源服务治理框架Dubbo有一定的关系,除了Dubbo本身较为完善的中文文档之外,不少科技公司的架构师均出自阿里系,所以就目前情况看,短期国内还是Dubbo的天下。

那么第一次实施微服务架构时,我们应该选择哪个基础框架更好呢?

以下内容均为作者个人观点,知识面有限,如有不对,纯属正常,不喜勿喷。

Round 1:背景


Dubbo,是阿里巴巴服务化治理的核心框架,并被广泛应用于阿里巴巴集团的各成员站点。阿里巴巴近几年对开源社区的贡献不论在国内还是国外都是引人注目的,比如:JStorm捐赠给Apache并加入Apache基金会等,为中国互联网人争足了面子,使得阿里巴巴在国人眼里已经从电商升级为一家科技公司了。

Spring Cloud,从命名我们就可以知道,它是Spring Source的产物,Spring社区的强大背书可以说是Java企业界最有影响力的组织了,除了Spring Source之外,还有Pivotal和Netfix是其强大的后盾与技术输出。其中Netflix开源的整套微服务架构套件是Spring Cloud的核心。

小结:如果拿Dubbo与Netflix套件做对比,前者在国内影响力较大,后者在国外影响力较大,我认为在背景上可以打个平手;但是若要与Spring Cloud做对比,由于Spring Source的加入,在背书上,Spring Cloud略胜一筹。不过,英雄不问出处,在背景这一点上,不能作为选择框架的主要因素,当您一筹莫展的时候,可以作为参考依据。

Round 2:社区活跃度


我们选择一个开源框架,社区的活跃度是我们极为关注的一个要点。社区越活跃,解决问题的速度越快,框架也会越来越完善,不然当我们碰到问题,就不得不自己解决。而对于团队来说,也就意味着我们不得不自己去维护框架的源码,这对于团队来说也将会是一个很大的负担。

下面看看这两个项目在github上的更新时间,下面截图自2016年7月30日:

  • Dubbo :github.com/dubbo

  • 最后更新时间为:2016年5月6日*

  • Spring Cloud :github.com/spring-clou…

  • 最后更新时间为:12分钟前*
    可以看到Dubbo的更新已经是几个月前,并且更新频率很低。而Spring Cloud的更新是12分钟前,仍处于高速迭代的阶段。

小结:在社区活跃度上,Spring Cloud毋庸置疑的优于Dubbo,这对于没有大量精力与财力维护这部分开源内容的团队来说,Spring Cloud会是更优的选择。

Round 3:架构完整度


或许很多人会说Spring Cloud和Dubbo的对比有点不公平,Dubbo只是实现了服务治理,而Spring Cloud下面有17个子项目(可能还会新增)分别覆盖了微服务架构下的方方面面,服务治理只是其中的一个方面,一定程度来说,Dubbo只是Spring Cloud Netflix中的一个子集。但是在选择框架上,方案完整度恰恰是一个需要重点关注的内容。

根据Martin Fowler对微服务架构的描述中,虽然该架构相较于单体架构有模块化解耦、可独立部署、技术多样性等诸多优点,但是由于分布式环境下解耦,也带出了不少测试与运维复杂度。

根据微服务架构在各方面的要素,看看Spring Cloud和Dubbo都提供了哪些支持。

Dubbo Spring Cloud
服务注册中心 Zookeeper Spring Cloud Netflix Eureka
服务调用方式 RPC REST API
服务网关 无 Spring Cloud Netflix Zuul
断路器 不完善 Spring Cloud Netflix Hystrix
分布式配置 无 Spring Cloud Config
服务跟踪 无 Spring Cloud Sleuth
消息总线 无 Spring Cloud Bus
数据流 无 Spring Cloud Stream
批量任务 无 Spring Cloud Task
… … …

以上列举了一些核心部件,大致可以理解为什么之前说Dubbo只是类似Netflix的一个子集了吧。当然这里需要申明一点,Dubbo对于上表中总结为“无”的组件不代表不能实现,而只是Dubbo框架自身不提供,需要另外整合以实现对应的功能,比如:

  • 分布式配置:可以使用淘宝的diamond、百度的disconf来实现分布式配置管理。但是Spring Cloud中的Config组件除了提供配置管理之外,由于其存储可以使用git,因此它天然的实现了配置内容的版本管理,可以完美的与应用版本管理整合起来。
  • 服务跟踪:可以使用京东开源的Hydra
  • 批量任务:可以使用当当开源的Elastic-Job
  • ……

虽然,Dubbo自身只是实现了服务治理的基础,其他为保证集群安全、可维护、可测试等特性方面都没有很好的实现,但是几乎大部分关键组件都能找到第三方开源来实现,这些组件主要来自于国内各家大型互联网企业的开源产品。

RPC vs REST

另外,由于Dubbo是基础框架,其实现的内容对于我们实施微服务架构是否合理,也需要我们根据自身需求去考虑是否要修改,比如Dubbo的服务调用是通过RPC实现的,但是如果仔细拜读过Martin Fowler的microservices一文,其定义的服务间通信是HTTP协议的REST API。那么这两种有何区别呢?

先来说说,使用Dubbo的RPC来实现服务间调用的一些痛点:

  • 服务提供方与调用方接口依赖方式太强:我们为每个微服务定义了各自的service抽象接口,并通过持续集成发布到私有仓库中,调用方应用对微服务提供的抽象接口存在强依赖关系,因此不论开发、测试、集成环境都需要严格的管理版本依赖,才不会出现服务方与调用方的不一致导致应用无法编译成功等一系列问题,以及这也会直接影响本地开发的环境要求,往往一个依赖很多服务的上层应用,每天都要更新很多代码并install之后才能进行后续的开发。若没有严格的版本管理制度或开发一些自动化工具,这样的依赖关系会成为开发团队的一大噩梦。而REST接口相比RPC更为轻量化,服务提供方和调用方的依赖只是依靠一纸契约,不存在代码级别的强依赖,当然REST接口也有痛点,因为接口定义过轻,很容易导致定义文档与实际实现不一致导致服务集成时的问题,但是该问题很好解决,只需要通过每个服务整合swagger,让每个服务的代码与文档一体化,就能解决。所以在分布式环境下,REST方式的服务依赖要比RPC方式的依赖更为灵活。
  • 服务对平台敏感,难以简单复用:通常我们在提供对外服务时,都会以REST的方式提供出去,这样可以实现跨平台的特点,任何一个语言的调用方都可以根据接口定义来实现。那么在Dubbo中我们要提供REST接口时,不得不实现一层代理,用来将RPC接口转换成REST接口进行对外发布。若我们每个服务本身就以REST接口方式存在,当要对外提供服务时,主要在API网关中配置映射关系和权限控制就可实现服务的复用了。

相信这些痛点也是为什么当当网在dubbox(基于Dubbo的开源扩展)中增加了对REST支持的原因之一。

小结:Dubbo实现了服务治理的基础,但是要完成一个完备的微服务架构,还需要在各环节去扩展和完善以保证集群的健康,以减轻开发、测试以及运维各个环节上增加出来的压力,这样才能让各环节人员真正的专注于业务逻辑。而Spring Cloud依然发扬了Spring Source整合一切的作风,以标准化的姿态将一些微服务架构的成熟产品与框架揉为一体,并继承了Spring Boot简单配置、快速开发、轻松部署的特点,让原本复杂的架构工作变得相对容易上手一些(如果您读过我之前关于Spring
Cloud的一些核心组件使用的文章,应该能体会这些让人兴奋而激动的特性,传送门)。所以,如果选择Dubbo请务必在各个环节做好整套解决方案的准备,不然很可能随着服务数量的增长,整个团队都将疲于应付各种架构上不足引起的困难。而如果选择Spring Cloud,相对来说每个环节都已经有了对应的组件支持,可能有些也不一定能满足你所有的需求,但是其活跃的社区与高速的迭代进度也会是你可以依靠的强大后盾。

Round 4:文档质量


Dubbo的文档可以说在国内开源框架中算是一流的,非常全,并且讲解的也非常深入,由于版本已经稳定不再更新,所以也不太会出现不一致的情况,另外提供了中文与英文两种版本,对于国内开发者来说,阅读起来更加容易上手,这也是dubbo在国内更火一些的原因吧。

Spring Cloud由于整合了大量组件,文档在体量上自然要比dubbo多很多,文档内容上还算简洁清楚,但是更多的是偏向整合,更深入的使用方法还是需要查看其整合组件的详细文档。另外由于Spring Cloud基于Spring Boot,很多例子相较于传统Spring应用要简单很多(因为自动化配置,很多内容都成了约定的默认配置),这对于刚接触的开发者可能会有些不适应,比较建议了解和学习Spring Boot之后再使用Spring Cloud,不然可能会出现很多一知半解的情况。

小结:虽然Spring Cloud的文档量大,但是如果使用Dubbo去整合其他第三方组件,实际也是要去阅读大量第三方组件文档的,所以在文档量上,我觉得区别不大。对于文档质量,由于Spring Cloud的迭代很快,难免会出现不一致的情况,所以在质量上我认为Dubbo更好一些。而对于文档语言上,Dubbo自然对国内开发团队来说更有优势。

总结


通过上面再几个环节上的分析,相信大家对Dubbo和Spring Cloud有了一个初步的了解。就我个人对这两个框架的使用经验和理解,打个不恰当的比喻:使用Dubbo构建的微服务架构就像组装电脑,各环节我们的选择自由度很高,但是最终结果很有可能因为一条内存质量不行就点不亮了,总是让人不怎么放心,但是如果你是一名高手,那这些都不是问题;而Spring Cloud就像品牌机,在Spring Source的整合下,做了大量的兼容性测试,保证了机器拥有更高的稳定性,但是如果要在使用非原装组件外的东西,就需要对其基础有足够的了解。

从目前Spring Cloud的被关注度和活跃度上来看,很有可能将来会成为微服务架构的标准框架。所以,Spring Cloud的系列文章,我会继续写下去。也欢迎各位朋友一起交流,共同进步。

【一些文章与示例的汇总】:git.oschina.net/didispace/S…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…390391392…399

开发者博客

3990 日志
1304 标签
RSS
© 2024 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%