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

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


  • 首页

  • 归档

  • 搜索

Laravel 中使用 vaptcha 手势验证码

发表于 2017-12-08

Vaptcha是最近才出现的一种新型的手势验证码,蛮有趣的,记录一下使用方法。
官网地址www.vaptcha.com/

准备工作,获取vid与key

根据文档说明,使用需要在后台创建一个验证单元,如下图所示,其中有选项里面有个验证场景,感觉描述的不是很清楚,感觉大概的意思就是可以给某个场景做统计,比如登录场景。
file
创建成功之后,会的到如下图所示的vid与key,后面再使用的时候会用到。
file

后端开发接口

安装#

按照github的文档来,使用composer安装:

1
复制代码composer require Vaptcha/vaptcha-sdk;

创建路由控制器#

根据文档这里需要两个接口供前端使用,一个是获取vid 和challenge,用于在客户端初始化实例,第二个是宕机模式,虽然不知到时是啥,反正照着写就行了2333。
sdk也提供了对应的两个接口 ,其中getChallenge接口,有个$sceneId参数,就是对应之前的验证场景。路设计如下

1
2
复制代码Route::get('vaptcha/challenge', 'VaptchaController@getChallenge');
Route::get('vaptcha/downtime', 'VaptchaController@getDownTime');

创建控制器:

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
复制代码<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Vaptcha\Vaptcha;

class VaptchaController extends Controller
{

private $vaptcha;

public function __construct(){
$this->vaptcha = new Vaptcha('vid', 'key'); // 这里替换成前面获取到的vid与key
}

public function response($status, $msg, $data){
return response()->json([
'status' => $status,
'msg' => $msg,
'data' => $data
], $status);
}

public function responseSuccess($data){
return $this->response(200, 'success', $data);
}

public function getChallenge(Request $request){
$data = json_decode($this->vaptcha->getChallenge($request->sceneid));
return $this->responseSuccess();
}

public function getDownTime(Request $request) {
$data = json_decode($this->vaptcha->downTime($request->data));
return response()->json($data);
}
}

访问接口,成功获取到数据
file

前端使用vaptcha

直接复制文档中的配置,改一下ok

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
复制代码<style>
#vaptcha_container {
margin-bottom: 10px;
display: table;
background-color: #EEEEEE;
border-radius: 2px
.vaptcha-init-loading {
display: table-cell;
vertical-align: middle;
text-align: center
}
.vaptcha-init-loading>a {
display: inline-block;
width: 18px;
height: 18px;
}
.vaptcha-init-loading>a img {
vertical-align: middle
}
.vaptcha-init-loading .vaptcha-text {
font-family: sans-serif;
font-size: 12px;
color: #CCCCCC;
vertical-align: middle
}
</style>
<!-- 点击式建议宽度不低于200px,高度不低于36px -->
<!-- 嵌入式仅需设置宽度,高度根据宽度自适应,最小宽度为200px -->
<div id="vaptcha_container" style="width:300px;height:36px;">
<!--vaptcha_container是用来引入VAPTCHA的容器,下面代码为预加载动画,仅供参考-->
<div class="vaptcha-init-loading">
<a href="https://www.vaptcha.com/" target="_blank"><img src="https://cdn.vaptcha.com/vaptcha-loading.gif"/></a>
<span class="vaptcha-text">VAPTCHA启动中...</span>
</div>
</div>
<script src="https://cdn.vaptcha.com/v.js"></script>
<!-- 引入jquery -->
<script src="/js/jquery.min.js"></script>
<script type="text/javascript">
//这里使用到验证场景,传过去的参数即为对应的编号,比如之前登录的对应的编号即为01
$.get('/api/vaptcha/challenge?sceneid=01', function(response){
console.log(response);
var options={
vid: response.data.vid, //验证单元id, string, 必填
challenge: response.data.challenge, //验证流水号, string, 必填
container:"#vaptcha_container",//验证码容器, HTMLElement或者selector, 必填
type:"click", //必填,表示点击式验证模式,
effect:'float', //验证图显示方式, string, 可选择float, popup, 默认float
https:false, //协议类型, boolean, 可选true, false
color:"#57ABFF", //按钮颜色, string
outage:"/api/vaptcha/downtime", //服务器端配置的宕机模式接口地址
success:function(token,challenge){//验证成功回调函数, 参数token, challenge 为string, 必填
//todo:执行人机验证成功后的操作
},
fail:function(){//验证失败回调函数
//todo:执行人机验证失败后的操作
}
}
var obj;
window.vaptcha(options,function(vaptcha_obj){
obj = vaptcha_obj;
obj.init();
});
});
</script>

成功显示
file

本文转载自: 掘金

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

左手用R右手Python系列——七周数据分析师学习笔记R语言

发表于 2017-12-08

上一篇我重点写了秦路老师在七周数据分析师系列课程中MySQL模块的实战作业SQL语法,对比了自己的冗余思路与老师的最佳思路。

MySQL入门学习笔记——七周数据分析师实战作业

这一篇,仍然是相同的六个业务问题,我尝试着R语言、Python复盘一遍,这样你可以对比同样的业务逻辑,使用不同工具处理之间的效率、逻辑的差异,以及各自的优缺点。在R语言代码部分,适当位置酌情做了注释,Python部分未做注释,请谨慎参考!

首先大致介绍这两份数据:

1
2
3
4
5
6
7
8
9
10
11
复制代码userinfo  客户信息表 
userId 客户id
gender 性别
brithday 出生日期

orderinfo 订单信息表
orderId 订单序号(虚拟主键)
userId 客户id
isPaid 是否支付
price 商品价格
paidTime 支付时间

以上两个表格是本次分析的主要对象,其中匹配字段是userId。

本次分析的五个问题:

1
2
3
4
5
6
复制代码1、统计不同月份的下单人数;
2、统计用户三月份回购率和复购率
3、统计男女用户消费频次是否有差异
4、统计多次消费的用户,第一次和最后一次消费间隔是多少?
5、统计不同年龄段用户消费金额是否有差异
6、统计消费的二八法则,消费的top20%用户,贡献了多少额度?

R语言版:

1
2
3
4
5
6
7
8
9
复制代码library("magrittr")
library("plyr")
library("dplyr")
library("lubridate")

userinfo <- read.csv("D:/R/File/userinfo.csv",stringsAsFactors = FALSE)
orderinfo <- read.csv("D:/R/File/orderinfo.csv",stringsAsFactors = FALSE)
userinfo$brithday <- as.Date(userinfo$brithday)
orderinfo$paidTime <- as.Date(orderinfo$paidTime)


1、统计不同月份的下单人数;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码orderinfo %>% filter(isPaid == '已支付') %>%                                   
#筛选出已支付订单
transform(date_month = format(as.Date(.$paidTime),'%Y-%m')) %>%
#新建月度变量标签
select(userId,date_month) %>%
#提取用户ID,月度标签
group_by(date_month) %>%
#按照月度标签分组
summarize(num_pep = n_distinct(userId))
#在分组基础上按照用户ID非重复计数
# A tibble: 3 x 2
date_month num_pep
<fctr> <int>
1 2016-03 54799
2 2016-04 43967
3 2016-05 6

2、统计用户三月份回购率和复购率

复购率计算:

1
2
复制代码1- (orderinfo %>% filter(isPaid == '已支付' & month(paidTime) == 3) %>% .[!duplicated(.$userId),] %>% nrow())/
(orderinfo %>% filter(isPaid == '已支付' & month(paidTime) == 3) %>% nrow())

回购率计算:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码#计算四月购买的消费者
four_m <- orderinfo %>%
filter(isPaid == '已支付' & month(paidTime) == 4) %>%
#筛选四月份已支付用户
.[,"userId"] %>%
unique()

#计算三月份购买的消费者
three_m <- orderinfo %>%
filter(isPaid == '已支付' & month(paidTime) == 3) %>%
.[!duplicated(.$userId),]
(three_m %>% filter(userId %in% four_m) %>% nrow())/(three_m %>% nrow())
0.2394022

3、统计男女用户消费频次是否有差异

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码newdate <- left_join(userinfo,orderinfo,by="userId")

newdate %>% filter(isPaid=='已支付' & gender !='') %>%
#筛选已支付且有效的购买记录
select(gender,userId) %>%
group_by(gender,userId) %>% summarize(num_sp=n()) %>%
#按照性别、用户id分组聚合出总体购买频次
select(gender,num_sp) %>% group_by(gender) %>%
summarize(mean_sp=mean(num_sp))
#按照性别聚合出男女平均购买频次# A tibble: 2 x 2
gender mean_sp
<chr> <dbl>1
男 1.8035042
女 1.782891

4、统计多次消费的用户,第一次和最后一次消费间隔时间是多少?

1
2
3
4
5
6
7
8
9
复制代码newdata1 <- orderinfo %>% filter(isPaid=='已支付') %>% 
select(userId,paidTime) %>%
mutate(date=as.Date(paidTime))
myreslut <-data.frame(
userId = newdata1 %>% arrange(userId,date) %>% .$userId %>% unique(),
ltime = newdata1 %>% arrange(userId,date) %>% .[!duplicated(.$userId),] %>% .$date,
ptime = newdata1 %>% arrange(userId,desc(date)) %>% .[!duplicated(.$userId),] %>% .$date,
difftime = difftime(ptime,ltime,units = "days")
) %>% filter(difftime !=0)


5、统计不同年龄段用户消费金额是否有差异

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码date1 <-c('1960-01-01','1969-12-31','1979-12-31','1989-12-31','1999-12-31','2009-12-31','2017-12-07')
newdate$trend <- cut(newdate$brithday,breaks=as.Date(date1),labels=c('60后','70后','80后','90后','00后','10后'),ordered=TRUE)

newdate %>% filter(isPaid == '已支付' & gender != '') %>%
group_by(trend) %>%
summarize(mean_price=mean(price,na.rm=TRUE)) %>%
na.omit
# A tibble: 6 x 2
trend mean_price
<ord> <dbl>
1 60后 616.0806
2 70后 641.1010
3 80后 642.9074
4 90后 600.0559
5 00后 552.6973
6 10后 661.1364

6、统计消费的二八法则,消费的top20%用户,贡献了多少额度?

1
2
3
4
5
复制代码data6 <- newdate %>% filter(isPaid == '已支付' & gender != '') %>% 
group_by(userId) %>%
summarize(sum_sp=sum(price)) %>%
arrange(-sum_sp)
top20_ratio <- sum(data6[1:round((nrow(data6)/5)),]$sum_sp)/sum(data6$sum_sp)

Python版:

1
2
3
4
5
6
复制代码import pandas as pd
import numpy as np
from datetime import datetime

userinfo = pd.read_csv("D:/R/File/userinfo.csv" ,encoding = 'gbk')
orderinfo = pd.read_csv("D:/R/File/orderinfo.csv",encoding = 'gbk')

1、统计不同月份的下单人数;

1
2
3
复制代码userinfo1 = userinfo.dropna()

userinfo1['brithday'] = [datetime.strptime(x,'%Y/%m/%d').strftime('%Y-%m-%d') for x in userinfo1['brithday']]

发现在转化日期时,有几个日期时非法日期,这可能是日期字段中存在着脏数据,直接删除掉即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码errortime = []
for i in userinfo1['brithday']:
try:
datetime.strptime(i,'%Y/%m/%d')
pass
except:
errortime.append(i)

['1691-07-07 00:00:00.0', '0072-09-28 00:00:00.0', '1882-05-13 00:00:00.0', '1069-09-26 00:00:00.0']
userinfo1 = userinfo1.loc[[x not in errortime for x in userinfo1['brithday']],]
userinfo1['brithday'] = [datetime.strptime(x,'%Y/%m/%d').strftime('%Y-%m-%d') for x in userinfo1['brithday']]
orderinfo1 = orderinfo.dropna()
orderinfo1['paidTime'] = [datetime.strptime(x,'%Y/%m/%d %H:%M').strftime('%Y-%m-%d') for x in orderinfo1['paidTime']]
orderinfo1['date_month'] = [datetime.strptime(x,'%Y-%m-%d').strftime('%Y-%m') for x in orderinfo1['paidTime']]
orderinfo1.query('isPaid == "已支付"').groupby('date_month')['userId'].count()date_month
2016-03 238474
2016-04 223324
2016-05 7
Name: userId, dtype: int64

2、统计用户三月份回购率和复购率

复购率计算:

1
2
3
复制代码nodup = orderinfo1.loc[(orderinfo1['isPaid']== '已支付') & (orderinfo1['date_month'] == '2016-03'),'userId'].duplicated().sum()
allnum= orderinfo1.loc[(orderinfo1['isPaid']== '已支付') & (orderinfo1['date_month'] == '2016-03'),'userId'].count()1-nodup/allnum
0.22979024967082362

回购率计算:

1
2
3
4
5
6
7
8
复制代码#计算四月购买的消费者
four_m = orderinfo1.loc[(orderinfo1['isPaid']== '已支付') & (orderinfo1['date_month'] == '2016-04'),].drop_duplicates('userId')
#计算三月份购买的消费者
three_m = orderinfo1.loc[(orderinfo1['isPaid']== '已支付') & (orderinfo1['date_month'] == '2016-03'),].drop_duplicates('userId')

len(three_m.loc[[x in four_m['userId'] for x in three_m['userId']],])/len(three_m)
len(set(four_m['userId']) & set(three_m['userId']))/len(three_m)
0.23940217887187723

3、统计男女用户消费频次是否有差异

1
2
3
4
5
6
7
8
复制代码newdate = userinfo1.merge(orderinfo1,on='userId')
newdate.loc[(newdate['isPaid']=='已支付') & (newdate['gender'] !=''),['gender','userId']].groupby(['gender','userId'])['userId'].\
agg({'con':'count'}).reset_index().groupby('gender')['con'].mean()

gender
女 1.806659
男 1.818501
Name: con, dtype: float64

4、统计多次消费的用户,第一次和最后一次消费间隔是多少?

1
2
3
4
5
6
7
8
9
复制代码newdata1 = orderinfo1.loc[orderinfo1.isPaid=='已支付',['userId','paidTime']]

myreslut = pd.DataFrame({
'userId':newdata1['userId'].unique(),
'ltime':newdata1.sort_values(by=['userId','paidTime'],ascending=[True,True]).drop_duplicates('userId')['paidTime'].tolist(),
'ptime':newdata1.sort_values(by=['userId','paidTime'],ascending=[True,False]).drop_duplicates('userId')['paidTime'].tolist()
},columns = ['userId','ltime','ptime'])
myreslut['difftime'] =[(datetime.strptime(str(b),"%Y-%m-%d") - datetime.strptime(str(a),"%Y-%m-%d")).days for a,b in zip(myreslut.ltime,myreslut.ptime)]
myreslut.loc[myreslut.difftime!=0,:]


5、统计不同年龄段用户消费金额是否有差异

1
2
3
复制代码date1 = ['1960-01-01','1969-12-31','1979-12-31','1989-12-31','1999-12-31','2009-12-31','2017-12-07']
newdate['trend'] = pd.cut(pd.to_datetime(newdate['brithday']),bins=pd.to_datetime(date1),labels=['60后','70后','80后','90后','00后','10后'])
newdate.loc[(newdate.isPaid =='已支付' ) & (newdate.gender != ''),].groupby('trend')['price'].agg({'mean_price':np.nanmean})


6、统计消费的二八法则,消费的top20%用户,贡献了多少额度?

1
2
3
复制代码data6 = newdate.loc[(newdate.isPaid =='已支付' ) & (newdate.gender != ''),].groupby('userId')['price'].\
agg({'sum_sp':np.nansum}).sort_values('sum_sp',ascending=False)
top20_ratio = np.sum(data6.loc[range((len(data6)//5)-1),'sum_sp'])/np.sum(data6['sum_sp'])

本文转载自: 掘金

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

设计模式学习笔记-单例模式 单例模式的学习与理解

发表于 2017-12-08

单例模式的学习与理解

单例模式算是设计模式中最容易理解,也是最容易手写代码的模式了吧。但是其中的坑却不少,所以也常作为面试题来考。本文主要对几种单例写法的整理,并分析其优缺点。很多都是一些老生常谈的问题,但如果你不知道如何创建一个线程安全的单例,不知道什么是双检锁,那这篇文章可能会帮助到你。

单例模式中,主要的技巧是:

1.将构造函数标为私有类型,然后就不能通过构造方法创建实例了

2.当然还是需要创建实例的,那就声明一个静态类变量

3.然后外部当需要获取实例的时候就通过公有类型的静态类方法获取静态类实例

1.懒汉模式

当被问到要实现一个单例模式时,很多人的第一反应是写出如下的代码,包括教科书上也是这样教我们的。

1
2
3
4
5
6
7
8
9
10
复制代码public class Singleton {
private static Singleton instance;
private Singleton( ){ }
public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}

这段代码简单明了,而且使用了懒加载模式,但是却存在致命的问题。当有多个线程并行调用 getInstance() 的时候,就会创建多个实例。也就是说在多线程下不能正常工作。

2.饿汉模式

这种方法非常简单,因为单例的实例被声明成 static 和 final 变量了,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的。

1
2
3
4
5
6
7
8
复制代码public class SingletonUrge {
//创建类的时候就会执行这句话
private static SingletonUrge instance = new SingletonUrge();
private SingletonUrge(){}
public static SingletonUrge getInstance(){
return instance;
}
}

这种写法如果完美的话,就没必要在啰嗦那么多双检锁的问题了。缺点是它不是一种懒加载模式(lazy initialization),单例会在加载类后一开始就被初始化,即使客户端没有调用 getInstance()方法。饿汉式的创建方式在一些场景中将无法使用:譬如 Singleton 实例的创建是依赖参数或者配置文件的,在 getInstance() 之前必须调用某个方法设置参数给它,那样这种单例写法就无法使用了。

3.加同步锁

为了解决懒汉模式下的问题,最简单的方法是将整个 getInstance() 方法设为同步(synchronized)。

1
2
3
4
5
6
7
8
9
10
复制代码public class SingletonSync {
private static SingletonSync instance;
private SingletonSync(){}
public synchronized static SingletonSync getInstance(){
if (null == instance) {
instance = new SingletonSync();
}
return instance;
}
}

虽然做到了线程安全,并且解决了多实例的问题,但是它并不高效。因为在任何时候只能有一个线程调用 getInstance() 方法。但是同步操作只需要在第一次调用时才被需要,即第一次创建单例实例对象时。这就引出了双重检验锁。

4.双重验证机制

双重检验锁模式(double checked locking pattern),是一种使用同步块加锁的方法。程序员称其为双重检查锁,因为会有两次检查 null == instance,一次是在同步块外,一次是在同步块内。为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的if,如果在同步块内不进行二次检验的话就会生成多个实例了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码public class SingletonDauth {
private volatile static SingletonDauth instance;
private SingletonDauth(){}
public static SingletonDauth getInstance(){
if (null == instance) {
synchronized (SingletonDauth.class){
if (null == instance){
instance = new SingletonDauth();
}
}
}
return instance;
}
}

这段代码看起来很完美,很可惜,它是有问题。主要在于instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。

  1. 给 instance 分配内存
  2. 调用 Singleton 的构造函数来初始化成员变量
  3. 将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)

但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非null了(但却没有初始化),所以线程二会直接返回instance,然后使用,然后顺理成章地报错。

我们只需要将 instance 变量声明成 volatile就可以了。

5.静态内部类

我比较倾向于使用静态内部类的方法,这种方法也是《Effective Java》上所推荐的。

1
2
3
4
5
6
7
8
9
复制代码public class SingletonInnerclass {
private static class SingletonHolder{
public static SingletonInnerclass instance = new SingletonInnerclass();
}
private SingletonInnerclass(){}
public static SingletonInnerclass getInstance(){
return SingletonHolder.instance;
}
}

这种写法仍然使用JVM本身机制保证了线程安全问题;由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本。

6.枚举型

通过enum关键字来实现枚举,在枚举中需要注意的有:

1.枚举中的属性必须放在最前面,一般使用大写字母表示

2.枚举中可以和java类一样定义方法

3.枚举中的构造方法必须是私有的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码public class Resource{
//这里就是需要存放的单例资源
public Resource(){
}
}
public enum Singleton{
INSTANCE;
private Resource instance = null;
Singleton(){
instance = new Resource();
}
public Resource getInstance() {
return instance;
}
}

默认枚举实例的创建是线程安全的.(创建枚举类的单例在JVM层面也是能保证线程安全的),这个优秀的思想直接源于Joshua Bloch的《Effective Java》(《Java高效编程指南》)。 所以不需要担心线程安全的问题

这里有几个原因关于为什么在Java中宁愿使用一个枚举量来实现单例模式:

1、 自由序列化;

2、 保证只有一个实例(即使使用反射机制也无法多次实例化一个枚举量);

3、 线程安全

Junit测试

下面是构建了一个maven项目,然后将设计模式的程序进行分析学习。之后将会把程序源码放到我的github上。

  • Java代码测试如下
1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码import  org.junit.Test;
public class AppTest
{
@Test
public void test(){
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println("Singleton Test"+singleton1.hashCode()+"and"+singleton2.hashCode());
SingletonInnerclass singleton3 = SingletonInnerclass.getInstance();
SingletonInnerclass singleton4 = SingletonInnerclass.getInstance();
System.out.print("SingletonInnerclass Test"+singleton3.hashCode()+"and"+singleton4.hashCode());
}
}
  • maven配置如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.nezha.dp</groupId>
<artifactId>dp</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>dp</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- https://mvnrepository.com/artifact/junit/junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
</dependency>
</dependencies>
</project>

参考文献

[1] blog.csdn.net/goodlixueyo…

[2] Freeman E, Freeman E, Sierra K, et al. Head First 设计模式[J]. 2007.

[3] wuchong.me/blog/2014/0…

本文转载自: 掘金

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

设计模式学习笔记-观察者模式 观察者模式

发表于 2017-12-08

观察者模式

个人博客:nezha.github.io
我的公众号:nezha_blog

本文的源代码放在我的GitHub上:nezha/DesignPatterns

观察者模式是对象的行为模式,又叫发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式。

观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态上发生变化时,会通知所有观察者对象,使它们能够自动更新自己。

Java Messages Service(JMS)消息服务使用观察者模式与命令模式来实现不同的程序之间的数据的发布和订阅。

观察者模式的结构

Command + option + shift

image
观察者模式所涉及的角色有:

  • 抽象主题(Subject)角色:抽象主题角色把所有对观察者对象的引用保存在一个聚集(比如ArrayList对象)里,每个主题都可以有任何数量的观察者。抽象主题提供一个接口,可以增加和删除观察者对象,抽象主题角色又叫做抽象被观察者(Observable)角色。
  • 具体主题(ConcreteSubject)角色:将有关状态存入具体观察者对象;在具体主题的内部状态改变时,给所有登记过的观察者发出通知。具体主题角色又叫做具体被观察者(Concrete Observable)角色。
  • 抽象观察者(Observer)角色:为所有的具体观察者定义一个接口,在得到主题的通知时更新自己,这个接口叫做更新接口。
  • 具体观察者(ConcreteObserver)角色:存储与主题的状态自恰的状态。具体观察者角色实现抽象观察者角色所要求的更新接口,以便使本身的状态与主题的状态 像协调。如果需要,具体观察者角色可以保持一个指向具体主题对象的引用。

观察者模式例子

1.抽象主题(Subject)

1
2
3
4
5
6
7
csharp复制代码public interface Subject {
//methods to register and unregister observers
public void register(Observer obj);
public void unregister(Observer obj);
//method to notify observers of change
public void notifyObservers();
}

2.具体主题(ConcreteSubject)

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
typescript复制代码public class ConcreteSubject implements Subject {

private List<Observer> observers;
private String message;
private boolean changed;
private final Object MUTEX= new Object();

public ConcreteSubject(){
this.observers=new ArrayList<Observer>();
}
@Override
public void register(Observer obj) {
if(obj == null) throw new NullPointerException("Null Observer");
if(!observers.contains(obj)) observers.add(obj);
}

@Override
public void unregister(Observer obj) {
observers.remove(obj);
}

@Override
public void notifyObservers() {
List<Observer> observersLocal = null;
//synchronization is used to make sure any observer registered after message is received is not notified
synchronized (MUTEX) {
if (!changed)
return;
observersLocal = new ArrayList<>(this.observers);
this.changed=false;
}
for (Observer obj : observersLocal) {
obj.update("" + new Date() + ">>>"+this.message);
}

}
//method to post message to the topic
public void postMessage(String msg){
System.out.println("Message Posted to Subject:"+msg);
this.message=msg;
this.changed=true;
notifyObservers();
}
}

3.抽象观察者(Observer)

1
2
3
4
pgsql复制代码public interface Observer {
//method to update the observer, used by subject
public void update(String content);
}

4.具体观察者(ConcreteObserver)

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

private String name;
private Subject topic;
private String content;

public ConcreteObserver(Subject topic, String nm){
this.name=nm;
this.topic=topic;
topic.register(this);
}
@Override
public void update(String content) {
this.content = content;
System.out.println(name + this.getClass().getName() +"--- update the info:"+this.content);
}
}

5.测试实验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
haxe复制代码public class ObserverPatternTest {
public static void main(String[] args) {
//create subject
ConcreteSubject subject = new ConcreteSubject();

//create observers
Observer obj1 = new ConcreteObserver(subject,"Obj1");
Observer obj2 = new ConcreteObserver(subject,"Obj2");
Observer obj3 = new ConcreteObserver(subject,"Obj3");
//now send message to subject
subject.postMessage("New Message");
//这里是解绑某一个对象,主动权在subject手中
subject.unregister(obj2);

subject.postMessage("将二号对象移除");
}
}

参考文献

ifeve.com/observer-de…

本文转载自: 掘金

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

浅谈Redis分布式锁实现

发表于 2017-12-08

在分布式系统当中, Redis锁是一个很常用的工具. 举个很常见的例子就是: 某个接口需要去查询数据库的数据, 但是请求量却又很大, 所以我们一般会加一层缓存, 并且设定过期时间. 但是这里存在一个问题就是当并发量很大的情况下, 在缓存过期的瞬间, 会有大量的请求穿透去数据库请求数据, 造成缓存雪崩效应. 这时候如果有锁的机制, 那么就可以控制单个请求去更新缓存.

其实对于Redis锁的看法, 网上已经有很多了, 只是大部分都是基于Java来实现的, 这里给出一个PHP实现的版本. 这里考虑的只是单机部署Redis的情况, 相对会简单好理解, 而且也更加的实用. 如果有分布式Redis部署的情况, 可以参考下Redlock算法的实现.

基本要求

实现一个分布式锁定, 我们至少要考虑它能满足一下的这些需求:

  • 互斥, 就是要在任何的时刻, 同一个锁只能够有一个客户端用户锁定.
  • 不会死锁, 就算持有锁的客户端在持有期间崩溃了, 但是也不会影响后续的客户端加锁
  • 谁加锁谁解锁, 很好理解, 加锁和解锁的必须是同一个客户端

加锁

我们这里使用的是Predis这个这个PHP的客户端, 其他客户端也是同理. 先来看看代码:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码`class RedisTool {        const LOCK_SUCCESS = 'OK';        const IF_NOT_EXIST = 'NX';        const MILLISECONDS_EXPIRE_TIME = 'PX';        const RELEASE_SUCCESS = 1;    /**`
    * 尝试获取锁
    * @param $redis       redis客户端
    * @param $key         锁
    * @param $requestId   请求id
    * @param $expireTime  过期时间
    * @return bool        是否获取成功
    */
   public static function tryGetLock($redis, $key, $requestId, $expireTime) {
       $result = $redis->set(            $key,            $requestId,            self::MILLISECONDS_EXPIRE_TIME,            $expireTime,            self::IF_NOT_EXIST        );                return self::LOCK_SUCCESS === (string)$result;
   }
}

定义一些Redis的操作符作为常量, 加锁的代码其实很简单, 一行代码即可. 简单解释下这个set方法的五个参数:

  • 第一个key是锁的名字, 这个由具体业务逻辑控制, 保证唯一即可
  • 第二个是请求ID, 可能不好理解. 这样做的目的主要是为了保证加解锁的唯一性. 这样我们就可以知道该锁是哪个客户端加的.
  • 第三个参数是一个标识符, 标识时间戳以毫秒为最小单位
  • 具体的过期时间
  • 这个参数是NX, 表示当key不存在时我们才进行set操作

PS. 请求的唯一性ID生成方式很多, 可以参考下这个chronos. 该库涉及到Thrift的RPC调用, 可能上手会比较麻烦, 下回给出一个简单的PHP实现.

简单解释下上面的那段代码, 设置NX保证了只能有一个客户端获取到锁, 满足互斥性; 加入了过期时间, 保证在客户端崩溃后不会造成死锁; 请求ID的作用是用来标识客户端, 这样客户端在解锁的时候可以进行校验是否同一个客户端.

解锁

当锁拥有的客户端完成了对共享资源的操作后, 释放锁需要用到Lua脚本, 也很简单:

1
2
3
复制代码`if redis.call("get",KEYS[1]) == ARGV[1] then`
   return redis.call("del",KEYS[1])else
   return 0end

PHP代码:

1
2
3
复制代码`class RedisTool {        const RELEASE_SUCCESS = 1;       public static function releaseLock($redis, $key, $requestId) {        $lua = "if redis.call('get', KEYS[1]) == ARGV[1] then                 return redis.call('del', KEYS[1])             else                 return 0             end";        $result = $redis->eval($lua, 1, $key, $requestId);        return self::RELEASE_SUCCESS === $result;`
   }
}

没想到一个简单的解锁操作也要用到Lua脚本, 待会会说说常见的几种错误解锁的方式. 其实为什么要用Lua脚本来实现, 主要是为了保证原子性. Redis的eval可以保证原子性, 主要还是源于Redis的特性, 可以看看官网的介绍

常见错误

  1. 错误加锁
1
2
3
4
5
复制代码`public static function wrong1($redis, $key, $requestId, $expireTime) {`
   $result = $redis->setnx($key, $requestId);        if ($result == 1) {                // 这里程序挂了或者expire操作失败,则无法设置过期时间,将发生死锁
       $redis->expire($key, $expireTime);
   }
}

这是比较常见的一种错误实现, 先通过setnx加锁, 然后在通过expire设置过期时间. 这样乍一看和上面的不都一样吗? 其实不然, 这是两条Redis命令, 不具有原子性, 如果在setnx之后程序挂了, 会使得锁没有设置过期时间, 这样就会发生死锁定.

  1. 错误加锁
1
2
3
4
5
6
7
8
9
10
11
复制代码`public static function wrong2($redis, $key, expireTime) {`
   $expires = floor(microtime(true) * 1000) + $expireTime;            // 如果当前锁不存在,返回加锁成功
   if ($redis->setnx($key, $expires) == 1) {                return true;
   }        //如果锁存在,获取锁的过期时间
   $currentValue = floor($redis->get($key));        if ($currentValue != null &&        $currentValue < floor(microtime(true) * 1000)) {                // 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间
       $oldValue = floor($redis->getSet($key, $expires));                if ($oldValue != null && $oldValue === $currentValue) {                        // 考虑并发的情况,只有设置值和当前值相同,它才有权利加锁
           return true;
       }
   }        // 其他情况,一律返回加锁失败
   return false;
}

这个例子实现原理是使用setnx来加锁, 如果锁已经存在的话则获取锁的过期时间并且与当前的时间比较, 过期则设置新的时间, 并且返回加锁成功. 虽然这样也可以加锁, 但是会存在几个问题:

  • 因为时间是客户端生成的, 这样就必须要保证在分布式环境下客户端的时间必须要同步
  • 当锁过期后, 多个客户端同时执行getSet方法, 虽然可以保证互斥性, 只适合这个锁的过期时间在高并发或者多线程的情况下有一定的可能被其他客户端给覆盖
  • 锁没有客户端的标识, 这样任何一个客户端都能够解锁
  1. 错误解锁
1
2
3
复制代码`public static function wrongRelease1($redis, $key) {`
   $redis->del([$key]);
}

这是最典型的错误了, 这样的做法没判断锁的拥有者, 会使得任何一个客户端都可以解锁, 甚至会把别人的锁给解除了.

  1. 错误解锁
1
2
3
4
复制代码`public static function wrongRelease2($redis, $key, $requestId) {            // 判断加锁与解锁是不是同一个客户端`
   if ($requestId === $redis->get($key)) {                    // 若在此时,这把锁突然不是这个客户端的,则会误解锁
       $redis->del([$key]);
   }}

上面的解锁也是没有保证原子性, 注释说的很明白了, 有这样的场景来复现: 客户端A加锁成功后一段时间再来解锁, 在执行删除del操作的时候锁过期了, 而且这时候又有其他客户端B来加锁(这时候加锁是肯定成功的, 因为客户端A的锁过期了), 这是客户端A再执行删除del操作, 会把客户端B的锁给清了.

总结

这样就基本上实现了一个简单的基于Redis的分布式锁. 其实分布式锁的实现远比想象的复杂, 特别是在多机部署Redis的情况下. 当然实现的方式也不仅仅包括Redis, 还可以用Zookeeper来实现. 随着对分布式系统的深入理解, 可以再来慢慢地思考这个问题.

本文转载自: 掘金

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

我们是否需要通用的C++软件包管理器?

发表于 2017-12-08

C++在软件包管理器上并不存在短板。当前有大量的工具可用,例如buckaroo、cget、conan、conda、cpm、
cppan、hunter等等,不胜枚举。

可是,如此之多的软件包管理器,反而让开源软件作者难以抉择。或许大家会迫切需要能有一种通用的软件包管理器,但事实上,由于C++尚不具有通用的构建系统,因此看上去也不会存在通用的软件包管理。此外,各种软件包管理器所关注的方面各不相同,例如与CMake的直接集成、可重复构建、共享二进制服务器、解决可满足性依赖等。

我建议,不要把注意力都放在如何整合所有的软件包管理器,并形成一种通用的管理器,而是应该尝试如何创建一种标准化方法,理解依赖关系并实现软件包的安装。C++软件的开发者只需要符合一种简单的格式,就可以支持多种软件包管理器。这种做法也使开发者可以将注意力集中在不同软件包管理器的独有特性上,而不是将放在如何转换为新格式可使用的软件库上。我们的标准化工作分为两个部分,分别是软件包规范和工具链规范。

任何一个标准化工作想要取得成功,就应构建于当前的实践之上。当前的软件包并没有通用的依赖关系定义方法,因此我们需要在软件包管理器间做一些协同。但是,即便是在不同的构建工具之间,还是存在通用的软件库(或软件包)构建和安装方法,并且很多软件包管理器是构建在同样的工作流上的。C++需要的是一种通用格式,将软件包需求提供给不同的软件包管理工具。

本文中,我将探讨具体的标准化工作。

使用需求(Usage Requirement)

当前,软件包管理器的主要工作是安装依赖关系,并决定所安装的版本。提供使用需求并不是软件包管理器的工作。一方面,软件包管理器并不知道使用需求。虽然它可以从所用的依赖关系中做出推测,但推测的内容并不完整。另一方面,构建脚本的确知道使用需求,因此在安装时应使用构建脚本,这样可保持构建脚本和软件包管理器间的不耦合关系。

现在,构建脚本要实现将使用需求告知软件包管理器(这可以通过在构建中的一些查询步骤实现),但这并非构建脚本的当前工作方式。当前,构建脚本生成软件包配置文件,该配置文件进而被下游的构建脚本使用。配置文件有两种格式,即CMake和pkgconfig。鉴于使用需求超出了软件包管理器的范畴,在此我们将不探讨它的具体内容。但毫无疑问,一种明显的解决方案就是采用依赖于构建的pkgconfig文件。

软件包规范

软件包规范是描述软件包内容细节的文件。其中将包括如下域:

  • 软件包名。
  • 描述。
  • 版本。
  • 可能的构建模式,即指定构建软件包所使用的构建系统。相比于依赖软件包管理器去推理构建系统,指定构建模式的方式更好,因为前者时常是模棱两可的。
  • 运行软件包的需求列表,其中包括版本限制。还可以指定需求只是用于构建,或只是用于测试。
  • 将需求中的软件包名映射到一个URL(或者可能是一个指向软件包的URL,软件包中包括这些映射)。对于不具有软件包索引的软件包管理器,或是需要可重复构建的软件包管理器,这非常有用。
  • 可能与该软件包具有冲突的软件包列表。
  • 该软件包可替换的软件包列表。

在理想情况下,该规范将存储在软件库本身,可能存储在最顶层。并且为实现快速的访问,软件包管理器还应索引这些文件。

此外,并非所有软件库都可提供这些软件包文件。因此我们需要有一种方式,非侵入式(non-intrusive)地提供软件包文件。在这种情况下,规范中还需指定如下域:

  • 下载软件包的URL。
  • 可能会使用的构建脚本。在没有提供构建脚本时,或是原始的构建脚本并不充分时,可以使用在此指定的构建脚本。

特别需要指出的是,一些软件包可在所有软件包管理器上使用,非侵入格式可为这些软件包提供一种标准的定义方式。当前,每个软件包管理器都具有自己的格式,它们会重新实现自身的软件包“食谱”。

我们可使用类似于pkgconfig的可用格式定义这些信息。这时,其中可以进一步包括pkgconfig文件中使用的变量定义和替换。为支持对可选依赖非常有用的条件定义,该格式需要做进一步的扩展。下面给出的例子中展示了这样的软件包文件格式:

1
2
3
4
5
6
7
复制代码__Fri Dec 08 2017 09:52:29 GMT+0800 (CST)____Fri Dec 08 2017 09:52:29 GMT+0800 (CST)__Name: foo
Description: A foo library
Version: 1.0
Requires:
zlib > 1.5
Dependencies:
zlib = http://zlib.net/zlib-1.2.11.tar.gz__Fri Dec 08 2017 09:52:29 GMT+0800 (CST)____Fri Dec 08 2017 09:52:29 GMT+0800 (CST)__

现在的问题是,如何解决软件包文件和构建系统之间的复制问题,因为构建中需要再次请求依赖关系。我希望在将来,构建系统可以读取同一软件包文件,以了解要搜索的依赖关系。如果该问题可以被标准化,那么这种集成是非常有可能的。

工具链规范

在软件包管理器知道了去哪里找到依赖关系后,下一步就是构建和安装软件包。尽管我们可以尽量创建标准的构建脚本,但是有一些构建过于复杂,以至于无法处理每个给出的构建需求。我们知道,CMake正力图成为一种高层的构建脚本,可生成其它的构建脚本。但即便如此,依然是不够的。软件作者此时会转而使用其它的构建工具。

我们并非力图去做标准化,并给出大而全的构建脚本,而是关注如何实现一种调用构建系统的标准方式,这更为简单。使用configure、build和install是调用构建的通用方式,这非常易于标准化。这样,软件包管理器的关键部分,是如何告知构建系统所使用的构建“环境”或工具链。当前,每个构建系统都有不同的格式,例如CMake使用的是工具链文件,Meson使用了交叉文件和环境变量,boost构建使用了一个user-config.jam文件,makefile和autotools使用的是一系列环境变量。

因此,我们需要提出一种描述工具链的标准化格式,其中应该包括:

  • 所使用的编译器。
  • 编译器标志。
  • 链接器标志。
  • 系统。
  • 交叉编译。
  • 构建类型(debug或release)。
  • 软件库类型(共享库还是静态库)。
  • 头文件目录。
  • 预处理定义。
  • 编译中使用的选项。
  • 链接共享、静态或可执行中使用的选项。
  • 寻找依赖关系的路径列表。
  • 交叉编译中使用的根路径(即sysroots)。

这里可以使用很简单的格式,例如“变量=赋值”的形式。进一步,每个变量应都可在软件包文件中访问,这样可基于工具链确定可选依赖关系。

在整个工具链和构建系统中,标准化的工具链有助于实现一致的构建和安装。也有助于确保构建系统足以处理软件包管理器所需的构建场景。

下面给出一个用于mingw工具链的文件:

1
2
3
4
5
6
7
8
9
复制代码__Fri Dec 08 2017 09:52:29 GMT+0800 (CST)____Fri Dec 08 2017 09:52:29 GMT+0800 (CST)__system = windows
cross_compile = true
c_compiler = x86_64-w64-mingw32-gcc
cxx_compiler = x86_64-w64-mingw32-g++
rc_compiler = x86_64-w64-mingw32-windres
root_path = /usr/x86_64-w64-mingw32
emulator = wine
install_prefix = ~/packages
prefix_path = ~/packages__Fri Dec 08 2017 09:52:29 GMT+0800 (CST)____Fri Dec 08 2017 09:52:29 GMT+0800 (CST)__

一旦构建系统支持工具链文件,这时编写包装器就是一件相对简化的事情,并由包装器实现标准化格式转换为原生的构建工具。当然,并非每个选项都能被所有的构建工具理解。因此,如果能向用户给出警告,说明某个选项不被某个构建工具所支持,这将十分有帮助的。

进一步考虑

作为cget软件包管理器的作者,我也提出了自己的软件包依赖关系描述格式。但是标准化的C++软件包规范,有助于实现在不同的构建和软件包工具间的协同和互操作。此外,如果力图去构建一种通用构建工具和软件包管理器(例如build2),这无疑将是一场艰苦的战斗,并人们也不会很快地采用该工具。因此,我们聚焦于如何尽可能地实现现有实践的标准化,这样用户也不必重写他们的构建文件。

查看英文原文: Does C++ need a universal package manager?

感谢雨多田光对本文的审校。

本文转载自: 掘金

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

SSH密钥登录流程分析

发表于 2017-12-07

本文首发于 https://jaychen.cc

作者 jaychen

写一篇短文,介绍 ssh 密钥登录远程服务器流程和注意事项。

登录流程

密钥登录比密码登录安全,主要是因为他使用了非对称加密,登录过程中需要用到密钥对。整个登录流程如下:

  1. 远程服务器持有公钥,当有用户进行登录,服务器就会随机生成一串字符串,然后发送给正在进行登录的用户。
  2. 用户收到远程服务器发来的字符串,使用与远程服务器公钥配对的私钥对字符串进行加密,再发送给远程服务器。
  3. 服务器使用公钥对用户发来的加密字符串进行解密,得到的解密字符串如果与第一步中发送给客户端的随机字符串一样,那么判断为登录成功。

整个登录的流程就是这么简单,但是在实际使用 ssh 登录中还会碰到一些小细节,这里演示一遍 ssh 远程登录来展示下这些细节问题。

生成密钥对

使用 ssh-keygen 就可以直接生成登录需要的密钥对。ssh-keygen 是 Linux 下的命令,不添加任何参数就可以生成密钥对。

1
2
3
4
5
6
复制代码➜  ~ ssh-keygen
Generating public/private rsa key pair.

Enter file in which to save the key (/home/jaychen/.ssh/id_rsa): #1
Enter passphrase (empty for no passphrase): #2
Enter same passphrase again: #3

执行 ssh-keygen 会出现如上的提示,在 #1 处这里提示用户输入生成的私钥的名称,如果不填,默认私钥保存在 /home/jaychen/.ssh/id_rsa 文件中。这里要注意两点:

  • 生成的密钥,会放在执行 ssh-keygen 命令的用户的家目录下的 .ssh 文件夹中。即 $HOME/.ssh/ 目录下。
  • 生成的公钥的文件名,通常是私钥的文件名后面加 .pub 的后缀。

#2 处,提示输入密码,注意这里的密码是用来保证私钥的安全的。如果填写了密码,那么在使用密钥进行登录的时候,会让你输入密码,这样子保证了如果私钥丢失了不至于被恶意使用。话是这么说,但是平时使用这里我都是直接略过。

#3 是重复 #2 输入的密码,这里就不废话了。

生成密钥之后,就可以在 /home/jaychen/.ssh/ 下看到两个文件了(我这里会放在 /home/jaychen 下是因为我使用 jaychen 用户来执行 ssh-keygen 命令)

1
2
3
4
5
6
复制代码➜  .ssh ls
total 16K
drwx------ 2 jaychen jaychen 4.0K 12月 7 17:57 .
drwx------ 9 jaychen jaychen 4.0K 12月 7 18:14 ..
-rw------- 1 jaychen jaychen 1.7K 12月 7 17:57 id_rsa.github
-rw-r--r-- 1 jaychen jaychen 390 12月 7 17:57 id_rsa.github.pub

生成的私钥还要注意一点:私钥的权限应该为 rw-------,如果私钥的权限过大,那么私钥任何人都可以读写就会变得不安全。ssh 登录就会失败。

首次 ssh 登录

登录远程服务器的命令是

1
复制代码ssh 登录用户@服务器ip

这里开始要注意两个用户的概念:

  • 本地执行这条命令的用户,即当前登录用户,我这里演示的用户名称是 jaychen。
  • 要登录到远程服务器的用户。

在开始登录之前,我们要首先要把生成公钥上传到服务器。

公钥的内容要保存到要登录的用户的家目录下的 .ssh/authorized_keys 文件中。假设你之后要使用 root 用户登录远程服务器,那么公钥的内容应该是保存在 /root/.ssh/authorized_keys中。注意 authorized_keys 文件是可以保存多个公钥信息的,每个公钥以换行分开。

上传完毕之后,执行

1
复制代码ssh root@远程服务器 ip

这个时候,如上面说的,远程服务器会发送一段随机字符串回来,这个时候需要使用私钥对字符串进行加密。而这个私钥会去执行该命令的用户的家目录下的 .ssh 目录读取私钥文件,默认私钥文件为 id_rsa 文件。即 $HOME/.ssh/id_rsa 文件。假设在生成密钥的时候对私钥进行了加密,那么这个时候就需要输入密码。

上面的流程用户登录的时候是不会感知的,ssh 在背后完成了所有的校验操作,如果密钥匹配的话,那么用户就可以直接登录到远程服务器,但是如果是首次登录的话,会出现类似下面的提示:

1
2
3
4
复制代码➜  .ssh ssh root@192.168.1.1
The authenticity of host '192.168.1.1 (192.168.1.1)' can't be established.
ECDSA key fingerprint is SHA256:61U/SJ4n/QdR7oKT2gaHNuGxhx98saqMfzJnzA1XFZg.
Are you sure you want to continue connecting (yes/no)?

这句话的意思是,远程服务器的真实身份无法校验,只知道公钥指纹(公钥的 MD5 值)为 61U/SJ4n/QdR7oKT2gaHNuGxhx98saqMfzJnzA1XFZg,是否真的要建立连接。出现上面的提示是因为避免存在中间人攻击。

中间人攻击

中间人攻击的前提是,你第一次登录一台远程服务器,你除了用户名、用户名对应的公钥私钥以及服务器 ip 之外,对远程服务器丝毫不了解的情况下。假设你 ssh 远程登录 192.168.1.1 的远程主机,在连接过程中被第三者拦截,第三者假冒自己为 192.168.1.1 的主机,那么你就会直接连接到其他人的服务器上。这就是中间人攻击。

为了避免中间人攻击,ssh 在首次登录的时候会返回公钥指纹,用户需要自己手动去比对你要登录的远程服务器的公钥的公钥指纹和 ssh 返回的公钥指纹是否一样。

经过比较公钥指纹,确认该服务器就是你要登录的服务器,输入 yes 之后就可以成功登录。整个登录流程结束。

known_hosts 文件

第一次登录之后,在本机的 $HOME/.ssh/ 目录下就会生成一个 known_hosts 的文件,内容类似下面

1
2
3
复制代码➜  .ssh cat known_hosts

192.168.1.1 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOPKYWolOYTDevvBR6GV0rFcI0z/DHZizN5l/ajApsgx+UcOOh51liuyBRRCIyF+BR56Le0lP0Pn6nzvLjbqMqg=

这个文件记录了远程主机 ip 和远程主机对应的公钥指纹,那么在下次登录的时候,远程主机发送过来的公钥指纹,直接和 known_hosts 文件中对应 ip 的公钥指纹比较即可。

config 配置

很多时候,我们开发可能需要连接多台远程服务器,并且需要配置 git 服务器的私钥。那么这么多的服务器不能共用一套私钥,不同的服务器应该使用不同的私钥。但是我们从上面的连接流程可以看到,ssh 默认是去读取 $HOME/.ssh/id_rsa 文件作为私钥登录的。如果想要不同的服务器使用不同的私钥进行登录,那么需要在 .ssh 目录下编写 config 文件来进行配置。

config 的配置很简单,只要指明哪个用户登录哪台远程服务器需要使用哪个私钥即可。下面给出一个配置示例。

1
2
3
4
5
6
复制代码Host github.com
User jaychen
IdentityFile ~/.ssh/id_rsa.github
Host 192.168.1.1
User ubuntu
IdentityFile ~/.ssh/id_rsa.xxx

上面 config 文件字段含义如下:

  • Host 指明了远程主机的 ip,除了使用 ip 地址,也可以直接使用网址。
  • User 指的是登录远程主机的用户。
  • IdentityFile 指明使用哪个私钥文件。

编写好 config 文件之后,需要把 config 文件的权限改为 rw-r--r--。如果权限过大,ssh 会禁止登录。

本文转载自: 掘金

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

使用 Pandora 平台玩转直播实时质量监控

发表于 2017-12-07

结合七牛云生态,赋能应用大数据的核心能力,让用户可将资源精力聚焦于直播业务价值提升,而无需担忧复杂的大数据技术和运维监控难题。

背景

去年( 2016 年)被称为直播元年,各类移动直播平台如雨后春笋冒出,不断满足人们对强交互、高实时性的新媒体载体的要求。直播过程中所涉及的环节众多,诸如推流、网络传输、节点调度、流处理和播放等,要全面地建立起一套能够对各个环节性能进行监控的系统绝非易事。目前七牛直播云已经建立起一套完善的内部数据监控平台,实现了一个智能调度、按需伸缩、高容错的实时流网络,我们称之为 LiveNet。LiveNet 完美解决了直播场景的三高之痛:技术门槛高、成本高、卡顿延时率高。

然而,在实际的客户对接及服务过程中,各式各样的问题仍然不可避免,如直播卡顿、马赛克、花屏、黑屏、杂音、音画不同步等等。这些问题中,有些是传输链路原因,有些是用户的使用姿势引起,有些是参数配置错误所致,也有些是直播 SDK 本身的问题。很多情况下,如果没有足够的数据线索进行支撑,应对线上用户反馈的这类问题,直播平台开发者经常两眼一抹黑,定位问题基本只能靠猜。通常情况下,在直播客户没有建立自己的直播质量监控系统时,七牛云的专业技术服务团队是客户排障的第一选择。针对绝大部分常见的问题,七牛云技术支持能够快速提供排查建议,如帮助查询某路直播流的实时状态,判断主播推流的稳定性。

但是,由于七牛云直播技术支持并没有直接接触到各个直播客户的终端用户,在问题排查过程中难免存在信息不对称的情况,增大沟通的时间成本。特别是当直播客户线上问题集中爆发时,如果直播平台的开发者没有一个系统的途径能够进行自查,势必使得该直播平台的用户体验陡然下降。那么,建立一个直播质量监控系统需要有哪些投入呢?以下我们为您详细解析。

直播质量监控系统

通常来说,如果要建立一套自己的直播质量监控体系,一般要完成以下几个步骤:

1
2
3
4
5
复制代码1. 在 App 端埋点,收集由直播 SDK 回调的音视频帧率、码率等与直播质量相关的数据,并进行上报;
2. 建立一个收点的网关,如果数据量太大,还需要 Kafka 等队列做数据缓存;
3. 搭建 HDFS 、 Elasticsearch 等存储服务,将接收的 QoS 数据转存到这些存储系统;
4. 搭建一套实时/离线数据流分析服务;
5. 数据可视化展示、告警系统。

实现以上功能,不仅需要有一个资深大数据背景的技术团队和客户端团队的支撑以及漫长的开发周期,系统上线后仍需持续投入精力持续维护迭代,以应对诸如逐步上升的数据量;若是对平台的横向扩展能力没考虑周全,众多开源组件崩盘的风险很可能会让之前的投入白费。

那么,有没有一种不需要自己造轮子的途径,去实现绝大部分质量监控功能?如今,我们给出了肯定的回答!借助于七牛大数据平台 Pandora,以及七牛直播云 SDK 所集成的 QoS 质量上报模块,七牛直播云用户能够快速打造一套属于自己的实时直播质量监控系统,并实现各种维度的自定义分析能力。

七牛大数据平台 Pandora

七牛大数据平台 Pandora 是一套面向海量数据,能够让基础技术人员轻松管理大数据传输、计算、存储和分析的大数据 PaaS 平台,提供简单、高效、开放的一站式大数据服务,核心服务及功能包括大数据工作流引擎、时序数据库、日志检索服务、Spark 服务、报表工作室。同时提供了海量离线数据分析等众多大数据分析工具支持,并结合七牛云生态,赋能应用大数据的核心能力,让用户可将资源精力聚焦于业务价值提升而无需担忧复杂的大数据技术和部署运维难题。

直播 QoS

直播质量 (QoS)实时上报模块几乎是每个高品质直播 SDK 的必要组成部分,它对于提升直播 SDK 性能以及直播网络的节点调度策略、链路质量具有重要作用。七牛云直播 SDK 的 QoS 模块使我们可以对终端用户连接的节点进行实时监控,了解其推流失败次数、卡顿次数以及卡顿时长。通过推流性能的实时监控和服务端实时调度系统的结合,可以实现对推流用户线路和节点的调整。

如何启用七牛直播质量监控服务

  • 直播云 SDK 集成

首先,请确保您的直播 App 集成了最新版本的七牛推流/播放 SDK。是的,在移动端,您要做的就是这么简单!

  • 创建 Grafana App

  • 载入我们为您提供的 Grafana 配置
    完成以上步骤后,便已万事俱备,只待直播质量日志的持续上报。

至此,您已经可以:

1
2
复制代码1. 在 Grafana 中观察到精细到每个流的质量变化曲线;
2. 通过 Pandora 日志服务 LogDB 所提供的强大日志检索功能,快速回溯追踪每个流或某个用户设备的数据。

Grafana 数据可视化展示

目前,我们为您预先内置了五个直播质量统计场景,分别是推流状态、服务器状态、播放地域统计、播放终端跟踪和地区运营商状态。

  • 推流各质量指标曲线,实时查看每个推流的音视频帧率、码率等指标的变化

  • 地区运营商平均推流质量曲线,可快速了解某地区、某运营商的总体推流状况

  • 播放终端质量曲线,查看总体播放质量变化,或精细到单设备的各项播放指标监控

  • 直播流在某区域运营商的播流质量曲线,让每个流在每个城市、运营商的播放质量都能得到监控

  • 推流加速节点的平均质量曲线,用于帮助判断是否由于推流节点的负载变化导致直播质量的变化

需要指出的是,这些内置场景只是 QoS 数据的一小部分应用,您可以根据需求拓展或增加 Grafana 的 Dashboard,关注您想要的质量维度。

LogDB 直播质量日志检索

事实上,直播质量日志在被可视化呈现之前,会被先导入到 Pandora LogDB 日志检索服务中。您可登录七牛官方网站,在日志检索模块中进行直播质量日志搜索。并且 LogDB 无缝兼容 Elasticsearch 协议,您也可使用我们为您提供的 Kibana 应用玩转日志检索。通过质量日志搜索,您可以进行各种直播问题的查询,如根据某个设备 id 进行单用户的日志回溯,进行用户级别的直播排障。

直播排障

那么,这些可视化图表及日志搜索该如何派上实际用场呢?下面我们就以一个常见的直播排障场景来说明这些数据在定位问题中能够给到直播厂商的帮助,使直播平台的技术人员能够自助快速排障。

假设某个主播向直播平台反馈直播卡顿,那么,直播平台的技术人员首先获取到该主播的流 id,然后在 Grafana 的流状态 Dashboard 中,过滤出这个流,发现其推流曲线如下:

从时序图中,可以看到有几个时间点推流的音视频帧率、码率都降为 0,存在一定的波动。那么,这个波动是由什么原因引起的呢?是主播的网络不稳定?主播将直播 App 退至后台进行了其他操作?还是推流的节点负载过重?

为了回答上面的猜测,直播技术客服利用日志检索服务进行更进一步的问题追踪,那便是在 LogDB 中搜索推流 id,回溯主播的推流行为。

通过搜索该主播设备上报的直播质量日志,可以发现,在音视频发送帧率降为 0 的几个时间点,其音视频的编码帧率也为 0,并且视频发送缓冲区的丢帧率为 0,这说明了并非由于主播网络问题导致发送丢帧,而是直播 App 退至后台导致音视频的采集被挂起。此时基本可以判定直播不畅的锅该由主播自己背,平台可以帮助矫正这个主播的使用姿势。

更进一步地,如果全部情况都表明主播的网络质量良好,操作正常,那么可以观察该直播流所连接节点的情况。若该节点其他的流也存在类似的波动,那么证明节点的负载过重,可为这些直播流进行推流节点的调度以优化推流质量。

以上,通过简单的几步查询,直播平台便能自助针对单个用户的问题进行定位排查,极大缩短了线上问题的解决周期,提升平台用户体验。

告警设置

除此之外,我们还为您创建的 Grafana 提供了完善多样的报警功能。使您能够对重点关注的直播质量指标进行监测,主动发现并解决问题。
我们同样以一个场景来说明告警的使用姿势。比如,您可能很关注播放终端的平均接收码率,因为它直接关系着用户观看的视觉体验。假设您期望播放码率的平均值应该在 800Kbps 以上,那么就可针对这个指标设置一个告警监控。如下图:

一旦监测的平均播放码率小于期望值,那么您将收到如下一个报警消息。此时,直播平台运维可快速做出响应,查看问题出现的原因。

啊哈,原来如此!

那么,开通了基于 Pandora 大数据平台的直播质量监控之后,您的客户端质量数据又是如何被实时处理并呈现在您的面前呢?以下便为您揭晓个中奥秘。

过滤用户数据

直播 QoS 数据被客户端上报后会进入收点服务的消息队列(Kafka)中,利用一个内部数据采集服务,我们从消息队列中根据推流域名实时过滤并拉取不同客户的 QoS 数据,转发到对应账号的 Pandora 大数据工作流中。

事实上,Pandora 本身提供了一个强大的通用数据采集工具,它适用于各类日志数据收集场景,比如:[10分钟内快速构建能够承载海量数据的 nginx 日志分析与报警平台]

Pandora workflow 数据加工

在 logkit 将您直播 QoS 数据上报至 Pandora 时,会自动建立如下一个大数据工作流,目前是将 QoS 数据直接导出至下游的 LogDB 日志检索服务。Pandora 还提供了其他多种导出选择,如导出到七牛对象存储,可将全量日志进行持久化;也可将日志转发到您自己的 HTTP 服务,使直播质量数据在您自己的服务框架中派上用场。

分析维度延展

利用大数据工作流引擎,您可以利用上报到数据源中的 QoS 数据进行更多维度的自定义分析,只需在实时工作流中新建 Transform 计算任务,便能对上报的数据进行进一步的聚合处理,导出更多维度的数据。
例如,我们甚至可以利用 QoS 数据进行活跃用户数这类简单的运营统计分析,只需新建如下一个计算任务,加之简单的一段 SQL 代码,便能计算出各个省份每五分钟内安卓活跃用户的数量。计算结果可选择导出到 LogDB、时序数据库、七牛对象存储或是您本地架设的 HTTP 服务,即将 Pandora 的直播质量分析结果回流到您的平台进行落地。

离线分析

除了利用实时工作流进行分析外,您还可创建离线的 XSpark,分析更多更久的海量数据,详见 「XSpark 使用入门」

后记

如果您已经是七牛直播云的用户,那么只需联系七牛 Pandora 团队申请开通,审核通过之后,我们将为您提供一个开箱即用的直播质量数据监控应用。

如果您使用的是非七牛的直播 SDK,那么,别着急,我们正在准备一个直播数据埋点 SDK,方便您进行相应的自定义直播质量数据上报。为了保障您的数据隐私安全,该 SDK 将全部开源,做到真正开放透明。同样地,欢迎联系我们,与我们交流您所期望的直播质量监控需求。

本文转载自: 掘金

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

Linux运维-搭建高可用Redis缓存 前言 搭建Redi

发表于 2017-12-07

前言

Redis是一个高性能的key-value数据库,现时越来越多企业与应用使用Redis作为缓存服务器。楼主是一枚JAVA后端程序员,也算是半个运维工程师了。在Linux服务器上搭建Redis,怎么可以不会呢?下面楼主就带着大家从0开始,依次搭建:Redis单机服务器 -> Redis主从复制 -> Redis-Sentinel高可用。逐步搭建出高可用的Redis缓存服务器。主要参考文章:简书:搭建一个redis高可用系统

楼主基于此文一步步参考着搭建,所以内容较为相似,感谢原文作者,特此申明。

本文同步发布于简书 :http://www.jianshu.com/p/d7bc873b8797

搭建Redis

1. 下载并解压

首先从Redis官网下载Redis并解压,楼主使用的版本是4.0.2。依次执行如下命令:

1
2
3
复制代码cd /usr/local/
wget http://download.redis.io/releases/redis-4.0.2.tar.gz
tar -zxvf redis-4.0.2.tar.gz

如果没有安装gcc依赖包,则安装对应依赖包

1
复制代码yum install -y gcc-c++ tcl

2. 编译并安装

下载并解压完毕后,则对源码包进行编译安装,楼主的Redis安装路径为/usr/local/redis,同学们可以自行修改语句:make install PREFIX=你想要安装的路径

1
2
复制代码cd /usr/local/redis-4.0.2/
make install PREFIX=/usr/local/redis

复制Redis相关命令到/usr/sbin目录下,这样就可以直接执行这些命令,不用写全路径

1
2
复制代码cd /usr/local/redis/bin/
sudo cp redis-cli redis-server redis-sentinel /usr/sbin/

3. 建立Redis配置文件

安装完成之后将 Redis 配置文件拷贝到系统配置目录/etc/下,redis.conf 是 Redis 的配置文件,redis.conf 在 Redis 源码目录,port默认 6379。

1
复制代码cp /usr/local/redis-4.0.2/redis.conf  /etc/

Redis配置文件主要参数解析参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码daemonize  no               #redis进程是否以守护进程的方式运行,yes为是,no为否(不以守护进程的方式运行会占用一个终端)
pidfile /var/run/redis.pid #指定redis进程的PID文件存放位置
port 6379 #redis进程的端口号
bind 127.0.0.1 #绑定的主机地址
timeout 300 #客户端闲置多长时间后关闭连接,默认此参数为0即关闭此功能
loglevel verbose #redis日志级别,可用的级别有debug.verbose.notice.warning
logfile stdout #log文件输出位置,如果进程以守护进程的方式运行,此处又将输出文件设置为stdout的话,就会将日志信息输出到/dev/null里面去了
databases 16 #设置数据库的数量,默认为0可以使用select <dbid>命令在连接上指定数据库id
save <seconds> <changes> #指定在多少时间内刷新次数达到多少的时候会将数据同步到数据文件;
rdbcompression yes #指定存储至本地数据库时是否压缩文件,默认为yes即启用存储;
dbfilename dump.db #指定本地数据库文件名
dir ./ #指定本地数据问就按存放位置;
slaveof <masterip> <masterport> #指定当本机为slave服务时,设置master服务的IP地址及端口,在redis启动的时候他会自动跟master进行数据同步
masterauth <master-password> #当master设置了密码保护时,slave服务连接master的密码;
requirepass footbared #设置redis连接密码,如果配置了连接密码,客户端在连接redis是需要通过AUTH<password>命令提供密码,默认关闭
maxclients 128 #设置同一时间最大客户连接数,默认无限制;redis可以同时连接的客户端数为redis程序可以打开的最大文件描述符,如果设置 maxclients 0,表示不作限制。当客户端连接数到达限制时,Redis会关闭新的连接并向客户端返回max number of clients reached错误信息
maxmemory<bytes> #指定Redis最大内存限制,Redis在启动时会把数据加载到内存中,达到最大内存后,Redis会先尝试清除已到期或即将到期的Key,当此方法处理 后,仍然到达最大内存设置,将无法再进行写入操作,但仍然可以进行读取操作。Redis新的vm机制,会把Key存放内存,Value会存放在swap区
appendonly no #指定是否在每次更新操作后进行日志记录,Redis在默认情况下是异步的把数据写入磁盘,如果不开启,可能会在断电时导致一段时间内的数据丢失。因为 redis本身同步数据文件是按上面save条件来同步的,所以有的数据会在一段时间内只存在于内存中。默认为no
appendfilename appendonly.aof #指定跟新日志文件名默认为appendonly.aof
appendfsync everysec #指定更新日志的条件,有三个可选参数no:表示等操作系统进行数据缓存同步到磁盘(快),always:表示每次更新操作后手动调用fsync()将数据写到磁盘(慢,安全), everysec:表示每秒同步一次(折衷,默认值);
3.1 设置后端启动:

由于Redis默认是前端启动,必须保持在当前的窗口中,如果使用ctrl + c退出,那么Redis也就退出,不建议使用。

1
复制代码vi /etc/redis.conf

修改Redis配置文件把旧值daemonize no 改为 新值daemonize yes

3.2 设置访问:

Redis默认只允许本机访问,可是有时候我们也需要 Redis 被远程访问。

1
复制代码vi /etc/redis.conf

找到 bind 那行配置,默认是: # bind 127.0.0.1

去掉#注释并改为: bind 0.0.0.0 此设置会变成允许所有远程访问。如果想指定限制访问,可设置对应的IP。

3.3 配置Redis日志记录:

找到logfile那行配置,默认是:logfile "",改为logfile /var/log/redis_6379.log

3.4 设置 Redis 请求密码:
1
复制代码vi /etc/redis.conf

找到默认是被注释的这一行:# requirepass foobared

去掉注释,把 foobared 改为你想要设置的密码,比如我打算设置为:123456,所以我改为:requirepass "123456"

修改之后重启下服务

有了密码之后,进入客户端,就得这样访问:redis-cli -h 127.0.0.1 -p 6379 -a 123456

4. Redis常用操作

4.1 启动
1
复制代码/usr/local/redis/bin/redis-server /etc/redis.conf
4.2 关闭
1
复制代码/usr/local/redis/bin/redis-cli -h 127.0.0.1 -p 6379 shutdown
4.3 查看是否启动
1
复制代码ps -ef | grep redis
4.4 进入客户端
1
复制代码redis-cli
4.5 关闭客户端
1
复制代码redis-cli shutdown
4.6 设置开机自动启动配置
1
复制代码echo "/usr/local/redis/bin/redis-server /etc/redis.conf" >> /etc/rc.local
4.7 开放防火墙端口
1
2
3
复制代码添加规则:iptables -I INPUT -p tcp -m tcp --dport 6379 -j ACCEPT
保存规则:service iptables save
重启 iptables:service iptables restart

5. 将Redis注册为系统服务

在/etc/init.d目录下添加Redis服务的启动,暂停和重启脚本:

1
复制代码vi /etc/init.d/redis

脚本内容如下:

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
复制代码#!/bin/sh  
#
# redis - this script starts and stops the redis-server daemon
#
# chkconfig: - 85 15
# description: Redis is a persistent key-value database
# processname: redis-server
# config: /usr/local/redis/bin/redis-server
# config: /etc/redis.conf
# Source function library.
. /etc/rc.d/init.d/functions
# Source networking configuration.
. /etc/sysconfig/network
# Check that networking is up.
[ "$NETWORKING" = "no" ] && exit 0
redis="/usr/local/redis/bin/redis-server"
prog=$(basename $redis)
REDIS_CONF_FILE="/etc/redis.conf"
[ -f /etc/sysconfig/redis ] && . /etc/sysconfig/redis
lockfile=/var/lock/subsys/redis
start() {
[ -x $redis ] || exit 5
[ -f $REDIS_CONF_FILE ] || exit 6
echo -n $"Starting $prog: "
daemon $redis $REDIS_CONF_FILE
retval=$?
echo
[ $retval -eq 0 ] && touch $lockfile
return $retval
}
stop() {
echo -n $"Stopping $prog: "
killproc $prog -QUIT
retval=$?
echo
[ $retval -eq 0 ] && rm -f $lockfile
return $retval
}
restart() {
stop
start
}
reload() {
echo -n $"Reloading $prog: "
killproc $redis -HUP
RETVAL=$?
echo
}
force_reload() {
restart
}
rh_status() {
status $prog
}
rh_status_q() {
rh_status >/dev/null 2>&1
}
case "$1" in
start)
rh_status_q && exit 0
$1
;;
stop)
rh_status_q || exit 0
$1
;;
restart|configtest)
$1
;;
reload)
rh_status_q || exit 7
$1
;;
force-reload)
force_reload
;;
status)
rh_status
;;
condrestart|try-restart)
rh_status_q || exit 0
;;
*)
echo $"Usage: $0 {start|stop|status|restart|condrestart|try-restart| reload|orce-reload}"
exit 2
esac

赋予脚本权限

1
复制代码chmod 755 /etc/init.d/redis

启动、停止和重启:

1
2
3
复制代码service redis start
service redis stop
service redis restart

至此,Redis单机服务器已搭建完毕,下面我们看看主从架构如何搭建。

搭建Redis主从架构

1. redis-server说明

1
2
3
复制代码172.16.2.185:6379 主

172.16.2.181:6379 从

2. Redis主从架构配置

  • 编辑从机的 Redis 配置文件,找到 210 行(大概),默认这一行应该是注释的: # slaveof <masterip> <masterport>
  • 我们需要去掉该注释,并且填写我们自己的主机的 IP 和 端口,比如:slaveof 172.16.2.185 6379,如果主机设置了密码,还需要找到masterauth <master-password>这一行,去掉注释,改为masterauth 主机密码。
  • 配置完成后重启从机Redis 服务
  • 重启完之后,进入主机的 redis-cli 状态下redis-cli -h 127.0.0.1 -p 6379 -a 123456,输入:INFO replication
    可以查询到当前主机的 Redis处于什么角色,有哪些从机已经连上主机。

主机信息172.16.2.185

1
2
3
4
5
6
7
8
9
10
11
12
复制代码# Replication
role:master
connected_slaves:1
slave0:ip=172.16.2.181,port=6379,state=online,offset=28,lag=1
master_replid:625ae9f362643da5337835beaeabfdca426198c7
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:28
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:28

从机信息172.16.2.181

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码# Replication
role:slave
master_host:172.16.2.185
master_port:6379
master_link_status:up
master_last_io_seconds_ago:3
master_sync_in_progress:0
slave_repl_offset:210
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:625ae9f362643da5337835beaeabfdca426198c7
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:210
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:210
  • 此时已经完成了主从配置,我们可以测试下:
    我们进入主机的 redis-cli 状态,然后 set 某个值,比如:set myblog YouMeek.com
  • 我们切换进入从机的 redis-cli 的状态下,获取刚刚设置的值看是否存在:get myblog,此时,我们可以发现是可以获取到值的。

3. Redis主从架构总结

  • 需要注意的是:从库不具备写入数据能力,不然会报错。 从库只有只读能力。
  • 主从架构的优点:除了减少主库连接的压力,还有可以关掉主库的持久化功能,把持久化的功能交给从库进行处理。
  • 第一个从库配置的信息是连上主库,后面的第二个从库配置的连接信息是连上第一个从库, 假如还有第三个从库的话,我们可以把第三个从库的配置信息连上第二个从库上,以此类推。

Redis Sentinel高可用架构搭建

1. 自动故障转移

  • 虽然使用主从架构配置Redis做了备份,看上去很完美。但由于Redis目前只支持主从复制备份(不支持主主复制),当主Redis挂了,从Redis只能提供读服务,无法提供写服务。所以,还得想办法,当主Redis挂了,让从Redis升级成为主Redis。
  • 这就需要自动故障转移,Redis Sentinel带有这个功能,当一个主Redis不能提供服务时,Redis Sentinel可以将一个从Redis升级为主Redis,并对其他从Redis进行配置,让它们使用新的主Redis进行复制备份。

Redis Sentinel架构图- 图片来自于CSDN 在Redis Sentinel环境下,jedis该如何配置

注意:搭建Redis Sentinel推荐至少3台服务器,但由于楼主偷懒,下面用例只用了2台服务器。

Redis Sentinel的主要功能如下:

  1. 监控:哨兵不断的检查master和slave是否正常的运行。
  2. 通知:当监控的某台Redis实例发生问题时,可以通过API通知系统管理员和其他的应用程序。
  3. 自动故障转移:如果一个master不正常运行了,哨兵可以启动一个故障转移进程,将一个slave升级成为master,其他的slave被重新配置使用新的master,并且应用程序使用Redis服务端通知的新地址。
  4. 配置提供者:哨兵作为Redis客户端发现的权威来源:客户端连接到哨兵请求当前可靠的master的地址。如果发生故障,哨兵将报告新地址。

默认情况下,每个Sentinel节点会以每秒一次的频率对Redis节点和其它的Sentinel节点发送PING命令,并通过节点的回复来判断节点是否在线。

如果在down-after-millisecondes毫秒内,没有收到有效的回复,则会判定该节点为主观下线。

如果该节点为master,则该Sentinel节点会通过sentinel is-master-down-by-addr命令向其它sentinel节点询问对该节点的判断,如果超过<quorum>个数的节点判定master不可达,则该sentinel节点会将master判断为客观下线。

这个时候,各个Sentinel会进行协商,选举出一个领头Sentinel,由该领头Sentinel对master节点进行故障转移操作。

故障转移包含如下三个操作:

  1. 在所有的slave服务器中,挑选出一个slave,并将其转换为master。
  2. 让其它slave服务器,改为复制新的master。
  3. 将旧master设置为新master的slave,这样,当旧的master重新上线时,它会成为新master的slave。

2. 搭建Redis Sentinel高可用架构

这里使用两台服务器,每台服务器上开启一个redis-server和redis-sentinel服务。

redis-server说明

1
2
3
复制代码172.16.2.185:6379 主

172.16.2.181:6379 从

redis-sentinel说明

1
2
3
复制代码172.16.2.185:26379

172.16.2.181:26379
2.1 建立Redis配置文件

如果要做自动故障转移,则建议所有的redis.conf都设置masterauth,因为自动故障只会重写主从关系,即slaveof,不会自动写入masterauth。如果Redis原本没有设置密码,则可以忽略。

Redis程序上面已经安装过了,我们只需增加redis-sentinel的相关配置即可,将 redis-sentinel的配置文件拷贝到系统配置目录/etc/下,sentinel.conf 是 redis-sentinel的配置文件,sentinel.conf 在 Redis 源码目录。

1
复制代码cp /usr/local/redis-4.0.2/sentinel.conf  /etc/

修改sentinel.conf配置文件内容如下:

1
复制代码vi /etc/sentinel.conf
1
2
3
4
5
6
7
复制代码protected-mode no
sentinel monitor mymaster 172.16.2.185 6379 2
# redis在搭建时设置了密码,所以要进行密码配置
sentinel auth-pass mymaster “123456“
#5秒内mymaster没有响应,就认为SDOWN
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 15000

在配置最后加上

1
2
3
复制代码logfile /var/log/sentinel.log
pidfile /var/run/sentinel.pid
daemonize yes

配置文件说明:

1.port :当前Sentinel服务运行的端口

2.dir : Sentinel服务运行时使用的临时文件夹

3.sentinel monitor master001 192.168.110.10163792:Sentinel去监视一个名为master001的主redis实例,这个主实例的IP地址为本机地址192.168.110.101,端口号为6379,而将这个主实例判断为失效至少需要2个 Sentinel进程的同意,只要同意Sentinel的数量不达标,自动failover就不会执行

4.sentinel down-after-milliseconds master001 30000:指定了Sentinel认为Redis实例已经失效所需的毫秒数。当实例超过该时间没有返回PING,或者直接返回错误,那么Sentinel将这个实例标记为主观下线。只有一个 Sentinel进程将实例标记为主观下线并不一定会引起实例的自动故障迁移:只有在足够数量的Sentinel都将一个实例标记为主观下线之后,实例才会被标记为客观下线,这时自动故障迁移才会执行

5.sentinel parallel-syncs master001 1:指定了在执行故障转移时,最多可以有多少个从Redis实例在同步新的主实例,在从Redis实例较多的情况下这个数字越小,同步的时间越长,完成故障转移所需的时间就越长

6.sentinel failover-timeout master001 180000:如果在该时间(ms)内未能完成failover操作,则认为该failover失败

7.sentinel notification-script :指定sentinel检测到该监控的redis实例指向的实例异常时,调用的报警脚本。该配置项可选,但是很常用

2.2 开放防火墙端口
1
2
3
复制代码添加规则:iptables -I INPUT -p tcp -m tcp --dport 26379 -j ACCEPT
保存规则:service iptables save
重启 iptables:service iptables restart
2.3 启动redis-sentinel
1
复制代码redis-sentinel  /etc/sentinel.conf

在任意一台机子均可查看到相关服务信息

1
2
3
复制代码redis-cli -h 127.0.0.1 -p 26379

INFO sentinel
1
2
3
4
5
6
7
复制代码# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=mymaster,status=ok,address=172.16.2.185:6379,slaves=1,sentinels=2

3. 自动故障转移测试

3.1 停止主Redis
1
复制代码redis-cli -h 172.16.2.185 -p 6379 -a 123456 shutdown
3.2 查看redis-sentinel的监控状态
1
2
3
4
5
6
7
复制代码# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=mymaster,status=ok,address=172.16.2.181:6379,slaves=1,sentinels=2

发现从库提升为主库。

3.3 注意事项
  • 如果停掉master后,Sentinel显示足够数量的sdown后,没有出现odown或try-failover,则检查密码等配置是否正确
  • 如果停掉master后,试图切换的时候,发现日志出现 failover-abort-not-elected,则分2种情况分别解决:
  1. 如果Redis实例没有配置
1
2
复制代码protected-mode yes
bind 172.16.2.185

则在Sentinel 配置文件加上protected-mode no即可

  1. 如果Redis实例有配置
1
2
复制代码protected-mode yes
bind 172.16.2.185

则在Sentinel配置文件加上

1
2
复制代码protected-mode yes
bind 172.16.2.185

至此,redis的高可用方案已经搭建完成。

VIP对外提供虚拟IP实现高可用

1. 现有情况概述

客户端程序(如JAVA程序)连接Redis时需要ip和port,但redis-server进行故障转移时,主Redis是变化的,所以ip地址也是变化的。客户端程序如何感知当前主Redis的ip地址和端口呢?redis-sentinel提供了接口,请求任何一个Sentinel,发送SENTINEL get-master-addr-by-name <master name>就能得到当前主Redis的ip和port。

客户端每次连接Redis前,先向sentinel发送请求,获得主Redis的ip和port,然后用返回的ip和port连接Redis。

这种方法的缺点是显而易见的,每次操作Redis至少需要发送两次连接请求,第一次请求Sentinel,第二次请求Redis。

更好的办法是使用VIP,当然这对配置的环境有一定的要求,比如Redis搭建在阿里云服务器上,可能不支持VIP。

VIP方案是,Redis系统对外始终是同一ip地址,当Redis进行故障转移时,需要做的是将VIP从之前的Redis服务器漂移到现在新的主Redis服务器上。

比如:当前Redis系统中主Redis的ip地址是172.16.2.185,那么VIP(172.16.2.250)指向172.16.2.185,客户端程序用VIP(172.16.2.250)地址连接Redis,实际上连接的就是当前主Redis,这样就避免了向Sentinel发送请求。

当主Redis宕机,进行故障转移时,172.16.2.181这台服务器上的Redis提升为主,这时VIP(172.16.2.250)指向172.16.2.181,这样客户端程序不需要修改任何代码,连接的是172.16.2.181这台主Redis。

2.漂移VIP实现Redis故障转移

那么现在的问题是,如何在进行Redis故障转移时,将VIP漂移到新的主Redis服务器上。

这里可以使用Redis Sentinel的一个参数client-reconfig-script,这个参数配置执行脚本,Sentinel在做failover的时候会执行这个脚本,并且传递6个参数<master-name>、 <role>、 <state>、 <from-ip>、 <from-port>、 <to-ip> 、<to-port>,其中<to-ip>是新主Redis的IP地址,可以在这个脚本里做VIP漂移操作。

1
复制代码sentinel client-reconfig-script mymaster /opt/notify_mymaster.sh

修改两个服务器的redis-sentinel配置文件/etc/sentinel.conf,增加上面一行。然后在/opt/目录下创建notify_mymaster.sh脚本文件,这个脚本做VIP漂移操作,内容如下:

1
复制代码vi /opt/notify_mymaster.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码#!/bin/bash
echo "File Name: $0"
echo "Quoted Values: $@"
echo "Quoted Values: $*"
echo "Total Number of Parameters : $#"

MASTER_IP=${6} #第六个参数是新主redis的ip地址
LOCAL_IP='172.16.2.185' #当前服务器IP,主机172.16.2.185,从机172.16.2.181
VIP='172.16.2.250'
NETMASK='24'
INTERFACE='eth1'
if [ ${MASTER_IP} = ${LOCAL_IP} ]; then
sudo /sbin/ip addr add ${VIP}/${NETMASK} dev ${INTERFACE} #将VIP绑定到该服务器上
sudo /sbin/arping -q -c 3 -A ${VIP} -I ${INTERFACE}
exit 0
else
sudo /sbin/ip addr del ${VIP}/${NETMASK} dev ${INTERFACE} #将VIP从该服务器上删除
exit 0
fi
exit 1 #如果返回1,sentinel会一直执行这个脚本

赋予脚本权限

1
复制代码chmod 755 /opt/notify_mymaster.sh

现在当前主Redis是172.16.2.185,需要手动绑定VIP到该服务器上。

1
2
复制代码/sbin/ip  addr add 172.16.2.250/24 dev eth1
/sbin/arping -q -c 3 -A 172.16.2.250 -I eth1

由于VIP只能绑定只有一台机子,所以建议将改为bind 0.0.0.0添加至redis.conf 中

1
复制代码vi /etc/redis.conf

设置bind 0.0.0.0

由于VIP只能绑定只有一台机子,所以建议将改为bind 0.0.0.0添加至sentinel.conf中

1
复制代码vi /etc/sentinel.conf

设置bind 0.0.0.0

重启Redis

1
复制代码service redis restart`

重启Sentinel

1
复制代码redis-sentinel /etc/sentinel.conf

随后我们在另一台机器172.16.2.181上,通过VIP访问主机

1
复制代码redis-cli -h 172.16.2.250 -p 6379 -a 123456 INFO replication

可正常通讯,信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码# Replication
role:master
connected_slaves:1
slave0:ip=172.16.2.181,port=6379,state=online,offset=0,lag=0
master_replid:325b0bccab611d329d9c2cd2c35a1fe3c01ae196
master_replid2:c1f7a7d17d2c35575a34b00eb10c8abf32df2243
master_repl_offset:22246293
second_repl_offset:22241024
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:22237293
repl_backlog_histlen:9001

访问主机的Sentinel

1
复制代码redis-cli -h 172.16.2.250 -p 26379 INFO sentinel

可正常通讯,信息如下:

1
2
3
4
5
6
7
复制代码# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=mymaster,status=ok,address=172.16.2.185:6379,slaves=1,sentinels=3

下面关闭主机的Redis服务,看看VIP是否漂移到另一台服务器上。

1
复制代码redis-cli -h 172.16.2.185 -p 6379 -a 123456 shutdown

查看是否已进行切换

1
复制代码redis-cli -h 172.16.2.250 -p 26379 INFO sentinel
1
2
3
4
5
6
7
复制代码# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=mymaster,status=ok,address=172.16.2.181:6379,slaves=1,sentinels=3

通过查询Sentinel发现从机172.16.2.181提升为主。

通过访问VIP的方式连接Redis

1
复制代码redis-cli -h 172.16.2.250 -p 6379 -a 123456 INFO replication
1
2
3
4
5
6
7
8
9
10
11
复制代码# Replication
role:master
connected_slaves:0
master_replid:cab30a4083f35652053ffcd099d70b9aaf7a80f3
master_replid2:3da856dd33cce4bedd54926df6797b410f1ab9e8
master_repl_offset:74657
second_repl_offset:36065
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:74657

从上面信息可知,VIP已经飘移成功。可喜可贺,大吉大利,晚上吃鸡。

总结

至此,高可用Redis缓存服务已搭建完毕,迟点会再出一篇文章教大家如何通过JAVA连接Redis进行相关操作。至于Redis Cluster集群方案,等有空再搭建然后再和大家一同分享。

参考文章

搭建一个redis高可用系统

Redis 安装和配置

Redis 复制、Sentinel的搭建和原理说明

Redis 快速入门(官网翻译)

Redis Sentinel机制与用法(一)

Redis哨兵-实现Redis高可用

读懂Redis并配置主从集群及高可用部署

在Redis Sentinel环境下,jedis该如何配置

redis sentinel 主从切换(failover)解决方案,详细配置

Redis-3.2.1主从故障测试实例

本文转载自: 掘金

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

Java笔记-Java反射(二)

发表于 2017-12-07

上一篇文章介绍了反射的基本概念以及获取类相关信息的反射API,这一章节主要记录如何对类的成员进行操作的相关反射API。

操作类成员的类

反射API中提供了如下接口,用于对类的成员进行操作。

1
复制代码 java.lang.reflect.Member

该接口主要有以下三个实现类,用于对类成员中的字段,方法和构造器进行操作。

Tips: 在Java SE 7的手册中指出,构造器不是类的成员,这和Member的实现类想表达的意思不同。

操作字段

字段拥有类型以及值,使用以下类能够获取类中字段的类型信息,获取字段的值以及对字段进行赋值操作。

1
复制代码 java.lang.reflect.Field

操作方法

方法有返回值,参数,并且可能会抛出异常,使用以下类可以获取方法参数以及返回值的类型信息,也可以调用指定对象的方法。

1
复制代码 java.lang.reflect.Method

操作构造器

使用如下类可以操作类的构造器,提供与操作method类似的方法,但有以下两点例外,构造器没有有返回值,并且对构造器的调用可以创建指定类的实例。

1
复制代码 java.lang.reflect.Constructor

实际操作

获取字段类型

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

public static String abc = "123";
private static List<String> a;

public static void main(String[] args) throws NoSuchFieldException {
Class c = Main.class;
Field field = c.getField("abc");
Field field1 = c.getDeclaredField("a");

System.out.println(field.getType());
System.out.println(field.getGenericType());

System.out.println();

System.out.println(field1.getType());
System.out.println(field1.getGenericType());
}
}

如上代码所示,获取对应字段的Field类,具体使用区别在上一张文末介绍了。
getType直接输出这个字段的类类型。
getGenericType直接输出这个字段的类型,如果是泛型字段的话,输出带有泛型实际参数的类型,如果不是泛型则会在内部调用getType。结果如下所示。

1
2
3
4
5
复制代码class java.lang.String
class java.lang.String

interface java.util.List
java.util.List<java.lang.String>

获取字段修饰符

类中字段有许多的修饰符,比如 public,private,transient等,java提供了API获取类的修饰符,不过获取出来的是一个int型数字,好在java提供了Modifier类对获得的整型进行判断,如下代码所示,有兴趣的可以对Modifier源码进行浏览。

1
2
3
4
5
6
7
8
9
复制代码public class Main {
public static int a = 1;
public static void main(String[] args) throws NoSuchFieldException {
Class c = Main.class;
Field field = c.getField("a");
System.out.println(Modifier.isPublic(field.getModifiers()));
System.out.println(Modifier.isStatic(field.getModifiers()));
}
}

读写字段值

反射可以对字段进行读写,如下代码所示,可以用过setX和getX方法对字段进行读写,不过要注意读写前后的类型是否匹配,不然会报异常。

1
2
3
4
5
6
7
8
9
10
11
复制代码    private static int a = 1;

public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
Class c = Main.class;
Field field = c.getDeclaredField("a");
int b = field.getInt(Main.class);
System.out.println(b);
field.setInt(Main.class, 2);
System.out.println(a);
field.setFloat(Main.class, (float) 1.1); // 报异常
}

操作方法和构造器

之后操作方法的类是Method,操作构造器的类是Constructor,通过这些API提供的get方法,可以获得方法和构造器的相关信息,因此在笔记里也不再赘述。

构造器创建实例

构造器和方法的反射类不同点在于,Constructor可以创建实例,代码如下所示。

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

public Main() {
}

public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException {
Class c = Main.class;
Constructor[] ctors = c.getConstructors();
Constructor ctor = null;
for (int i = 0; i < ctors.length; i++) {
ctor = ctors[i];
if (ctor.getGenericParameterTypes().length == 0) // 需要找到默认构造函数创建实例
break;
}

System.out.println(ctor.newInstance().getClass().getCanonicalName());
}
}

结尾

以上就是一些看反射API的一些记录,这个工具本身使用上还是很简单的,但意义还是比较大的,是很多框架存在的基础,下一篇以struts为例子,写一个小demo,展示反射在其中的运用。

本文转载自: 掘金

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

1…913914915…956

开发者博客

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