0%

“唯之与阿,相去几何?
善之与恶,相去何若?
人之所畏,不可不畏。
荒兮,其未央哉!
众人熙熙,如享太牢,如春登台。
我独泊兮,其未兆,如婴儿之未孩;傫傫兮,若无所归!
众人皆有余,而我独若遗。
我愚人之心也哉!”1

如何应对Redis在缓存应用中的穿透、击穿以及雪崩问题?

Redis在做缓存时,通常在代码逻辑上会做如下处理:

  1. 查询缓存
  2. 缓存有,则返回;缓存没有则查询数据库;
  3. 查询数据库,返回;并放入缓存

问题

问题1: 如果缓存没有,数据库也没有,此为缓存穿透。

譬如获取用户信息,当请求携带一个并不存在的id时(譬如:被攻击),就会将请求集中落在访问数据库上。
解决方法:
在缓存和数据库都无法获取对应信息时,将返回的空对象放入缓存,并设置一个合理的过期时间(譬如30秒等)。

问题2: 高并发访问,导致还是有部分的查询接触到了数据库层面,此为缓存击穿。

解决方法:
2步从数据库获取信息代码块上包上同步锁 synchronized即可。

当某个时刻由于某种原因,缓存集体时效了,导致缓存雪崩。

解决方法:
1.粗暴,设置永不过期;弊端在于需要编写定时任务跑批。
2.对不同类别不同场景的对象设置离散的过期时间。

如下代码供参考:

1
public String getNameByUserId(@RequestParam(value = "userId") Integer userId){
2
    String name = redisServiceClient.get("marvel:user:"+userId);
3
    //双重检测
4
    if(name==null){
5
        name = redisServiceClient.get("marvel:user:"+userId);
6
        //同步锁 //防止击穿
7
        synchronized (this){
8
            TUser user = userRepository.getOne(userId);
9
            if(user==null){
10
                redisServiceClient.set("marvel:user:"+userId,null,30);//防止穿透
11
            }
12
            redisServiceClient.set("marvel:user:"+userId,name,60*60*24);//24小时过期
13
        }
14
    }
15
    return name;
16
}

1 :老子《道德经》第二十章,老子故里,中国鹿邑。

“持而盈之,不如其已。
揣而锐之,不可常保。
金玉满堂,莫之能守;富贵而骄,自遗其咎。
功遂身退,天之道也。”1

Jenkins在SpringCloud微服务项目Devops中发挥的作用

Jenkins

本文不做具体安装部署的介绍,我Github上有一个完整的基于docker-compose的安装套件(同时还包括Mysql,Nginx,RabbitMQ,FastDFS,Redis等),可以去参考

Jenkins是以job(常用的有:自由风格模式 多任务模式)的方式提供打包服务,每一个job,都提供了一个方便“插一脚”的前置处理后置处理,均可以调用shell指令的方式进行接入整合。

  • 前置处理:便于做一些备份工作;
  • 后置处理:打包之后,调用类似“跳板机”上的shell脚本进行集群(联调环境,测试环境,生产环境)的服务部署。

Devops持续集成部署架构

架构图:
jenkins-in-devops.png

部署流程解析

1.通过jenkins上job构建服务,并通过后置处理脚本触发跳板机对应脚本 以部署生产环境为例,触发prod_one_deploy.sh

1
export JAVA_HOME=/usr/java/jdk1.8.0_121
2
export PATH=$JAVA_HOME/bin:$PATH
3
#export JAVA_OPTS='-Djava.rmi.server.hostname=106.14.33.163 -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9994 -Dcom.sun.management.jmxremote.rmi.port=9994 -Dcom.sun.management.jmxremote.authenticate=true -Dcom.sun.management.jmxremote.ssl=false'
4
5
marvel_jar_version="1.0.0-RELEASE"
6
marvel_module_dir="/marvel/product/deploy"
7
marvel_module_name=$1
8
9
#marvel_jenkins2jar_dir="/marvel/product/deploy"
10
#marvel_module_dir="/marvel/product/jenkins2jar"
11
marvel_module_name=$1
12
13
#mkdir $marvel_module_dir/$marvel_module_name &>/dev/null
14
#mkdir $marvel_module_dir/$marvel_module_name/target &>/dev/null
15
16
#cp $marvel_jenkins2jar_dir/$marvel_module_name/target/$marvel_module_name-$marvel_jar_version.jar $marvel_module_dir/$marvel_module_name/target/$marvel_module_name-$marvel_jar_version.jar
17
18
deploy_interval_seconds=`cat $marvel_module_dir/conf/deploy_interval_seconds.txt` ## 设定服务启动间隔时间,单位秒
19
20
for ip in `cat $marvel_module_dir/conf/$marvel_module_name.txt`
21
do
22
	echo "开始部署节点[$ip]上的服务[$marvel_module_name]...."
23
	echo "上传服务JAR到节点[$ip]/marvel/deploy/$marvel_module_name/ ..."
24
	ssh root@$ip "mkdir -p /marvel/deploy/$marvel_module_name > /dev/null"
25
	scp $marvel_module_dir/$marvel_module_name/target/$marvel_module_name-$marvel_jar_version.jar root@$ip:/marvel/deploy/$marvel_module_name/$marvel_module_name-$marvel_jar_version.jar
26
	echo "调用节点服务部署脚本..."
27
	ssh root@$ip "/marvel/deploy/one_deploy.sh $marvel_module_name"
28
	echo "已调用节点服务脚本,sleep $deploy_interval_seconds seconds..."
29
	sleep $deploy_interval_seconds
30
done

注意:需要设置跳板机和部署集群节点见的免密登录

2.prod_one_deploy.sh将服务jar包scp到部署节点,并通过ssh方式调用部署脚本one_deploy.sh

1
export JAVA_HOME=/usr/java/jdk1.8.0_121
2
export PATH=$JAVA_HOME/bin:$PATH
3
#export JAVA_OPTS='-Djava.rmi.server.hostname=106.14.33.163 -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9994 -Dcom.sun.management.jmxremote.rmi.port=9994 -Dcom.sun.management.jmxremote.authenticate=true -Dcom.sun.management.jmxremote.ssl=false'
4
#marvelOPTS="-Xms512m -Xmx2056m -XX:MaxNewSize=1024m -XX:MaxPermSize=1024m"
5
marvelOPTS=""
6
marvel_jar_version="1.0.0-RELEASE"
7
marvel_jenkins2jar_dir="/marvel/product/deploy"
8
#marvel_module_dir="/marvel/product/deploy"
9
marvel_module_dir="/marvel/product/jenkins2jar"
10
marvel_module_name=$1
11
12
mkdir $marvel_module_dir/$marvel_module_name &>/dev/null
13
mkdir $marvel_module_dir/$marvel_module_name/target &>/dev/null
14
cp $marvel_jenkins2jar_dir/$marvel_module_name/target/$marvel_module_name-$marvel_jar_version.jar $marvel_module_dir/$marvel_module_name/target/$marvel_module_name-$marvel_jar_version.jar
15
16
echo "赋予服务[$marvel_module_name]JAR可执行权限"
17
chmod 777 $marvel_module_dir/$marvel_module_name/target/$marvel_module_name-$marvel_jar_version.jar
18
echo "获取当前该服务进程ID"
19
process_id=$(ps -ef | grep $marvel_module_name-$marvel_jar_version | grep -v "grep" | awk '{print $2}')
20
21
if [ -z "$process_id" ]; then
22
	echo "服务[$marvel_module_name]当前尚未启动!"
23
else
24
	echo "服务[$marvel_module_name][id=$process_id]运行中,已杀之"
25
	kill -9 $process_id
26
fi
27
echo "服务[$marvel_module_name]启动中..."
28
29
if [ $marvel_module_name = "marvel-service-payment" ];then
30
	nohup java $marvelOPTS -jar $marvel_module_dir/$marvel_module_name/target/$marvel_module_name-$marvel_jar_version.jar --spring.profiles.active=test &>/dev/null &
31
else
32
	nohup java $marvelOPTS -jar $marvel_module_dir/$marvel_module_name/target/$marvel_module_name-$marvel_jar_version.jar --spring.profiles.active=test &>/dev/null &
33
fi
34
#nohup java -jar $marvel_module_dir/$marvel_module_name/target/$marvel_module_name-$marvel_jar_version.jar --spring.profiles.active=test 1>$marvel_module_dir/log/$marvel_module_name.log 2>&1 &

部署到k8s集群

图中另一条部署分支,是部署服务到k8s的,思路是一样的。

  • 将jenkins构建的构建的服务包,通过脚本k8s_deploy_manager.sh进行镜像打包,并上传到本地私服docker镜像中。
    1
    export JAVA_HOME=/usr/java/jdk1.8.0_121
    2
    export PATH=$JAVA_HOME/bin:$PATH
    3
    version=`head -n +1 /marvel/product/deploy/conf/marvel-k8s-prod-version.txt`
    4
    marvel_jar_version="1.0.0-RELEASE"
    5
    marvel_module_dir="/marvel/product/deploy"
    6
    marvel_module_name=$1
    7
    deploy_interval_seconds=`cat $marvel_module_dir/conf/deploy_interval_seconds.txt`
    8
    cp $marvel_module_dir/$1/target/$1-$marvel_jar_version.jar $marvel_module_dir/$1/docker/
    9
    docker build -t marvel-eureka:$version $marvel_module_dir/$1/docker
    10
    docker tag marvel-eureka:$version 106.14.33.xxx:5000/marvel-eureka:$version
    11
    docker push 106.14.33.xxx:5000/marvel-eureka:$version
  • 跳板机通过脚本k8s_one_deploy.sh部署服务到k8s集群,可以通过模版生成的方式将对应服务的k8s配置文件(如:xxx-service.yml,xxx_development.yml)统一管理,供部署脚本调用。

最后

通过jenkins的中间连接,将代码仓库(如github,gitee)中的代码(master或者分支)构建成jar包或者docker镜像,然后通过编写部署脚本将服务构建到普通集群或者k8s集群。

1:老子《道德经》第九章,老子故里,中国鹿邑。

SpringCloud微服务架构在实战项目中的总结

SpringCloud作为分布式微服务产品的研发生态来说是优雅且完备的,尤其是SpringBoot在团队研发中学习成本低,且能高效工作,因此相对于dubbo来说,SpringCloud是更多技术团队的首选。

在刚刚完成的一个智能还款的项目中,就应用了这样的架构来实施业务平台搭建。这里我们抛开业务本身不谈,只从技术层面对架构整体说明一下。

整体架构

整体架构图如下:

图中,我们以从上到下的方式首先对架构进行一次梳理:

  • C端,我们涉及了几乎所有的端,权重较高的则放在了h5端。
  • 接收到C端请求,以VIP(虚拟ip)的方式切入Nginx负载
  • 请求到达网关ZUUL,进而路由到自有服务marvel-facade
  • marvel-facade是通往我们自有服务池的唯一入口,所有请求feign接口路由到具体服务。
  • 自有服务和服务之间也是通过feign接口调用。
  • 引入lcn服务进行分布式事物管理。
  • 引入FastDFS进行分布式文件存储。
  • 引入FELK进行分布式日志监控。其中Ffilebeat
  • SpringCloud核心生态中我们着重使用了Bus Config Eureka zippin

细节总结

不要将所有的服务直接外露

引入marvel-facade层就是将所有的请求汇聚于此,然后根据具体业务逻辑通过feign调用。
优点如下:

  • 对外暴露接口固定,于具体业务服务无关。(尤其重要
  • swagger接口有大局观。

配置中心文件可自动刷新

引入Spring Cloud Bus,并将marvel-config服务承担起刷新配置的职责,由此可实现远端git配置文件仓库发生改变,所有相关服务均可进行对应的刷新。

配置关键信息加密

配置文件统一放在配置中心,配置中心文件明文存在不安全,容易泄露比如数据库用户名、密码等,如何实现git仓库配置文件为密文时,通过配置中心在Config-Server端进行解密。建议使用JCE加密

网关ZUUL很重要

除了做网关需要做的工作意外,对于用户认证、鉴权以及api接口请求的安全(包括反篡改)都要在这里完成。

分布式事物

项目微服务化了之后,分布式事物的问题便一定要处理好。
TX-LCN 和 阿里GTS 可任选一种。

jvm优化

主要集中在不管是用docker和java -jar的方式,要注意配置启动参数的调优。

nginx优化

ningx.conf 需要注意配置相关的线程数、连接数以及请求文件的大小限制等。

核心服务业务逻辑编写,要注重业务逻辑建模(非常重要

对于业务核心逻辑(产品的主功能),通常研发人员为这样做:在controler中定义接收请求,处理请求,或者直接甩给service进行处理,而service层无脑的进行复杂的函数调用,这种函数编程的方式充斥着整个逻辑模块,使后续逻辑的重构变的及其困难。

其实这样做会更加合理,也更加使业务逻辑层次化,便于code review和拥抱重构:

  • 深入理解你要编写的业务逻辑,试着加入面向对象的思维,虽然面向对象似乎是一个被说烂的概念,但它真的非常重要,你也真的似乎还未完全理解和吸收它。
  • controlerservice尽量当成入口来对待。处理请求的逻辑甩给业务建模对象来处理。
  • 业务建模对象中抽象出业务属性和业务方法,配合使用对应的设计模式思想,引入自定义注解 反射动态加载的机制抽丝剥茧,便终能成。

这似乎不是一蹴而就的,也许你可以试着使用之前的函数编程的方式编写业务逻辑,当发现可以重构的时候,便大刀阔斧的往这个方向上靠近。

最后

一个完整的微服务项目,首先要对使用的生态技术有个整体的把控,其不是简单的堆砌,可扩展、高并发以及安全和监控都要从一开始就要做到游刃有余才行。

“宠辱若惊,贵大患若身。
何谓宠辱若惊?
宠为下。
得之若惊,失之若惊,是谓宠辱若惊。
何谓贵大患若身?
吾所以有大患者,为吾有身。
及吾无身,吾有何患?
故贵以身为天下,若可以寄于天下,爱以身为天下者,若可托天下。”1

SpringCloud服务优雅启停的打开方式

The marvel’s service UP and DOWN in elegant model

问题:

1.服务已经发布,在eureka上已经看到注册成功了,但是服务调用方在调用服务时,依然调不到。
2.发布服务时,不能粗暴的kill或者重新部署,由于服务提供方并不支持幂等请求,所以粗暴的方式无可避免导致请求的异常,因此平台要支持服务优雅的上下线。

解决:

对于问题1:

EurekaServer有两个缓存机制:

eurekaserver-cache

  • ReadWriteMap: 当服务注册或者维持心跳时,更新。
  • ReadOnlyMap: 当有服务调用者查询服务列表时,访问。
解决方案:

EurekaServer: marvel-eureka-dev[test].yml

1
#eureka server刷新readCacheMap的时间,注意,client读取的是readCacheMap,这个时间决定了多久会把readWriteCacheMap的缓存更新到readCacheMap上
2
#默认30s
3
eureka.server.responseCacheUpdateIntervalMs=3000
4
#eureka server缓存readWriteCacheMap失效时间,这个只有在这个时间过去后缓存才会失效,失效前不会更新,过期后从registry重新读取注册服务信息,registry是一个ConcurrentHashMap。
5
#由于启用了evict其实就用不太上改这个配置了
6
#默认180s
7
eureka.server.responseCacheAutoExpirationInSeconds=180
8
9
#启用主动失效,并且每次主动失效检测间隔为3s
10
eureka.server.eviction-interval-timer-in-ms=3000

Client:marvel-eureka-client-dev[test].yml

1
#服务过期时间配置,超过这个时间没有接收到心跳EurekaServer就会将这个实例剔除
2
#注意,EurekaServer一定要设置eureka.server.eviction-interval-timer-in-ms否则这个配置无效,这个配置一般为服务刷新时间配置的三倍
3
#默认90s
4
eureka.instance.lease-expiration-duration-in-seconds=15
5
#服务刷新时间配置,每隔这个时间会主动心跳一次
6
#默认30s
7
eureka.instance.lease-renewal-interval-in-seconds=5
8
9
10
#eureka client刷新本地缓存时间
11
#默认30s
12
eureka.client.registryFetchIntervalSeconds=5
13
#eureka客户端ribbon刷新时间
14
#默认30s
15
ribbon.ServerListRefreshInterval=5000

对于问题2:

需要整合actuator,在marvel-client-eureka-dev[test].yml已经整合:

1
management:
2
  endpoint:
3
    health:
4
      show-details: always
5
  endpoints:
6
    web:
7
      exposure:
8
        include: '*'

各个项目在启动的时候,可以看到控制台有如下输出:

1
2019-06-24 14:18:36.810 [main] INFO  o.s.b.a.e.w.s.WebMvcEndpointHandlerMapping - 547 : Mapped "{[/actuator/archaius],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)
2
2019-06-24 14:18:36.811 [main] INFO  o.s.b.a.e.w.s.WebMvcEndpointHandlerMapping - 547 : Mapped "{[/actuator/auditevents],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)
3
2019-06-24 14:18:36.811 [main] INFO  o.s.b.a.e.w.s.WebMvcEndpointHandlerMapping - 547 : Mapped "{[/actuator/beans],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)
4
2019-06-24 14:18:36.812 [main] INFO  o.s.b.a.e.w.s.WebMvcEndpointHandlerMapping - 547 : Mapped "{[/actuator/health],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)
5
2019-06-24 14:18:36.812 [main] INFO  o.s.b.a.e.w.s.WebMvcEndpointHandlerMapping - 547 : Mapped "{[/actuator/conditions],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)
6
2019-06-24 14:18:36.812 [main] INFO  o.s.b.a.e.w.s.WebMvcEndpointHandlerMapping - 547 : Mapped "{[/actuator/configprops],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)
7
2019-06-24 14:18:36.812 [main] INFO  o.s.b.a.e.w.s.WebMvcEndpointHandlerMapping - 547 : Mapped "{[/actuator/env],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)
8
2019-06-24 14:18:36.813 [main] INFO  o.s.b.a.e.w.s.WebMvcEndpointHandlerMapping - 547 : Mapped "{[/actuator/env/{toMatch}],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)
9
2019-06-24 14:18:36.813 [main] INFO  o.s.b.a.e.w.s.WebMvcEndpointHandlerMapping - 547 : Mapped "{[/actuator/env],methods=[POST],consumes=[application/vnd.spring-boot.actuator.v2+json || application/json],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)
10
2019-06-24 14:18:36.814 [main] INFO  o.s.b.a.e.w.s.WebMvcEndpointHandlerMapping - 547 : Mapped "{[/actuator/env],methods=[DELETE],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)
11
2019-06-24 14:18:36.814 [main] INFO  o.s.b.a.e.w.s.WebMvcEndpointHandlerMapping - 547 : Mapped "{[/actuator/info],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)
12
2019-06-24 14:18:36.814 [main] INFO  o.s.b.a.e.w.s.WebMvcEndpointHandlerMapping - 547 : Mapped "{[/actuator/logfile],methods=[GET],produces=[application/octet-stream]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)
13
2019-06-24 14:18:36.814 [main] INFO  o.s.b.a.e.w.s.WebMvcEndpointHandlerMapping - 547 : Mapped "{[/actuator/loggers],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)
14
2019-06-24 14:18:36.815 [main] INFO  o.s.b.a.e.w.s.WebMvcEndpointHandlerMapping - 547 : Mapped "{[/actuator/loggers/{name}],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)
15
2019-06-24 14:18:36.815 [main] INFO  o.s.b.a.e.w.s.WebMvcEndpointHandlerMapping - 547 : Mapped "{[/actuator/loggers/{name}],methods=[POST],consumes=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)
16
2019-06-24 14:18:36.815 [main] INFO  o.s.b.a.e.w.s.WebMvcEndpointHandlerMapping - 547 : Mapped "{[/actuator/heapdump],methods=[GET],produces=[application/octet-stream]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)
17
2019-06-24 14:18:36.815 [main] INFO  o.s.b.a.e.w.s.WebMvcEndpointHandlerMapping - 547 : Mapped "{[/actuator/threaddump],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)
18
2019-06-24 14:18:36.816 [main] INFO  o.s.b.a.e.w.s.WebMvcEndpointHandlerMapping - 547 : Mapped "{[/actuator/metrics/{requiredMetricName}],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)
19
2019-06-24 14:18:36.816 [main] INFO  o.s.b.a.e.w.s.WebMvcEndpointHandlerMapping - 547 : Mapped "{[/actuator/metrics],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)
20
2019-06-24 14:18:36.816 [main] INFO  o.s.b.a.e.w.s.WebMvcEndpointHandlerMapping - 547 : Mapped "{[/actuator/scheduledtasks],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)
21
2019-06-24 14:18:36.817 [main] INFO  o.s.b.a.e.w.s.WebMvcEndpointHandlerMapping - 547 : Mapped "{[/actuator/httptrace],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)
22
2019-06-24 14:18:36.817 [main] INFO  o.s.b.a.e.w.s.WebMvcEndpointHandlerMapping - 547 : Mapped "{[/actuator/mappings],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)
23
2019-06-24 14:18:36.817 [main] INFO  o.s.b.a.e.w.s.WebMvcEndpointHandlerMapping - 547 : Mapped "{[/actuator/refresh],methods=[POST],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)
24
2019-06-24 14:18:36.817 [main] INFO  o.s.b.a.e.w.s.WebMvcEndpointHandlerMapping - 547 : Mapped "{[/actuator/features],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)
25
2019-06-24 14:18:36.818 [main] INFO  o.s.b.a.e.w.s.WebMvcEndpointHandlerMapping - 547 : Mapped "{[/actuator/service-registry],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)
26
2019-06-24 14:18:36.818 [main] INFO  o.s.b.a.e.w.s.WebMvcEndpointHandlerMapping - 547 : Mapped "{[/actuator/service-registry],methods=[POST],consumes=[application/vnd.spring-boot.actuator.v2+json || application/json],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)
27
2019-06-24 14:18:36.819 [main] INFO  o.s.b.a.e.w.s.WebMvcEndpointHandlerMapping - 547 : Mapped "{[/actuator],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto protected java.util.Map<java.lang.String, java.util.Map<java.lang.String, org.springframework.boot.actuate.endpoint.web.Link>> org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping.links(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)

其中:/actuator/service-registry 通过GET请求可以查看服务的状态,通过POST可以设置服务的状态 UP or Down
当设置服务的状态为DOWN时即优雅的关闭(下线)了该服务,查看eureka控制台时,此服务即已下线,通过观察服务的log,当没有任何请求log时可以对服务进行重新发布。

注意:如采用这种方式,请做好api的权限控制,否则:(。

关于Actuator的详细知识,可以点击查看这篇博客,讲的很详细了。

1:老子《道德经》第九章,老子故里,中国鹿邑。

“为学日益,为道日损。
损之又损,以至于无为。
无为而无不为。
取天下常以无事,及其有事,不足以取天下。”1

使用docker-compose方式安装redis-sentinel模式集群

简单说明一下何为哨兵模式?

有两个角色:哨兵节点和存储节点,一般简易各为3个为宜。

Sentinel(哨兵)节点是用于监控redis存储节点中master状态的工具,是Redis的高可用性解决方案,sentinel哨兵模式已经被集成在redis2.4之后的版本中。sentinel是redis高可用的解决方案,sentinel系统可以监视一个或者多个redis master服务,以及这些master服务的所有从服务;当某个master服务下线时,自动将该master下的某个从服务升级为master服务替代已下线的master服务继续处理请求。

sentinel可以让redis实现主从复制,当一个集群中的master失效之后,sentinel可以选举出一个新的master用于自动接替master的工作,集群中的其他redis服务器自动指向新的master同步数据。一般建议sentinel采取奇数台,防止某一台sentinel无法连接到master导致误切换。

哨兵节点配置

文件目录

1
- sentinel
2
   - data1
3
   - data2
4
   - data3
5
   - docker-compose.yml
6
   - sentinel.conf
7
   - sentinel2.conf
8
   - sentinel3.conf

docker-compose.yml

1
version: '2'
2
services:
3
  sentinel1:
4
    image: redis       ## 镜像
5
    container_name: redis-sentinel-1
6
    command: redis-sentinel /usr/local/etc/redis/sentinel.conf
7
    ports:
8
    - "26379:26379"
9
    restart: always ## 容器随宿主机重启
10
    volumes:
11
    - "/marvel/local/docker/sentinel/sentinel.conf:/usr/local/etc/redis/sentinel.conf" ## 挂载配置文件
12
    - "/marvel/local/docker/sentinel/data1:/data" ## 挂载数据存储路径
13
  sentinel2:
14
    image: redis                ## 镜像
15
    container_name: redis-sentinel-2 ## 容器名称
16
    ports:
17
    - "26380:26379"           
18
    restart: always
19
    command: redis-sentinel /usr/local/etc/redis/sentinel.conf
20
    volumes:
21
    - "/marvel/local/docker/sentinel/sentinel2.conf:/usr/local/etc/redis/sentinel.conf"
22
    - "/marvel/local/docker/sentinel/data2:/data"
23
  sentinel3:
24
    image: redis                ## 镜像
25
    container_name: redis-sentinel-3
26
    ports:
27
    - "26381:26379"           
28
    restart: always
29
    command: redis-sentinel /usr/local/etc/redis/sentinel.conf
30
    volumes:
31
    - "/marvel/local/docker/sentinel/sentinel3.conf:/usr/local/etc/redis/sentinel.conf"
32
    - "/marvel/local/docker/sentinel/data3:/data"
33
networks:
34
  default:
35
    external:
36
      name: redis_sentinel-master-ddsh ## 配置网络

sentinel.conf

1
port 26379
2
dir "/tmp"
3
sentinel deny-scripts-reconfig yes
4
sentinel monitor mymaster-ddsh xxx.xxx.xxx.xxx 6379 2
5
sentinel failover-timeout mymaster-ddsh 10000
6
sentinel auth-pass mymaster-ddsh redispasswordXXXX

sentinel2.conf

1
port 26379
2
dir "/tmp"
3
sentinel deny-scripts-reconfig yes
4
sentinel monitor mymaster-ddsh xxx.xxx.xxx.xxx 6379 2
5
sentinel failover-timeout mymaster-ddsh 10000
6
sentinel auth-pass mymaster-ddsh redispasswordXXXX

sentinel3.conf

1
port 26379
2
dir "/tmp"
3
sentinel deny-scripts-reconfig yes
4
sentinel monitor mymaster-ddsh xxx.xxx.xxx.xxx 6379 2
5
sentinel failover-timeout mymaster-ddsh 10000
6
sentinel auth-pass mymaster-ddsh redispasswordXXXX

存储节点配置

docker-compose.yml

1
version: '2'
2
services:
3
  master:
4
    image: redis       ## 镜像
5
    container_name: redis-master
6
    command: redis-server --requirepass 'PASSWORDXXX'
7
    ports:
8
    - "6379:6379"
9
    restart: always
10
    networks:
11
    - redis_sentinel-master-ddsh
12
  slave1:
13
    image: redis                ## 镜像
14
    container_name: redis-slave-1
15
    ports:
16
    - "6380:6379"           ## 暴露端口
17
    restart: always
18
    command: redis-server --slaveof redis-master 6379 --requirepass 'PASSWORDXXX' --masterauth 'PASSWORDXXX'
19
    depends_on:
20
    - master
21
    networks:
22
    - redis_sentinel-master-ddsh
23
  slave2:
24
    image: redis                ## 镜像
25
    container_name: redis-slave-2
26
    ports:
27
    - "6381:6379"           ## 暴露端口
28
    restart: always
29
    command: redis-server --slaveof redis-master 6379 --requirepass 'PASSWORDXXX' --masterauth 'PASSWORDXXX'
30
    depends_on:
31
    - master
32
    networks:
33
    - redis_sentinel-master-ddsh
34
networks:
35
  redis_sentinel-master-ddsh:

启动顺讯

先启动存储节点,再启动哨兵节点。
第一次:
cd到各自相应目录,执行docker-compose up -d -d表示后台启动
卸载,执行docker-compose down
其他时候,执行docker start|restart|stop 容器id来进行启动、重启、停止。

详细代码,见相应Github仓库

1 :老子《道德经》第四十八章,老子故里,中国鹿邑。

“道常无为,而无不为。
侯王若能守之,万物将自化。
化而欲作,吾将镇之以无名之朴。
无名之朴,夫亦将不欲。
不欲以静,天下将自定。”1

docker-compose-yml编写规范

本文不做docker的普及。

在使用docker安装第三方应用或自研服务的时候,不建议使用docker run ...的方式,取而代之的是使用编写相对应的docker-compose.yml文件,从而使用docker-compose ...的方式对容器进行简易且优雅的编排。

然而不规范的编写,也是十分可怕的,我认为从以下四个方面可以整体的把握docker-compose.yml的配置书写,下面以mysql的docker-compose.yml为例进行简单说明:

1
version: '2'
2
services:
3
  mysql-ddsh:
4
    image: mysql:5.6                
5
    network_mode: "host"
6
    ports:
7
    - "3336:3336"
8
    restart: always
9
    volumes:
10
    - "./db:/var/lib/mysql"
11
    - "./my.cnf:/etc/my.cnf"   
12
    environment:
13
      MYSQL_ROOT_PASSWORD: ForAlliance5689
14
      TZ:                  Asia/Shanghai

映射端口配

1
ports:
2
  - "3336:3336" //宿主机端口:容器内端口

挂载配置文件和数据存储路径

1
volumes:
2
  - "./db:/var/lib/mysql" //挂载数据存储路径
3
  - "./my.cnf:/etc/my.cnf"  //挂载配置文件

设置随宿主机自动重启

1
restart: always //随宿主机重启

时区配置

1
TZ: Asia/Shanghai //配置时区

但凡以粗暴的方式写完docker-compose.yml之后,回头检查以下上边四个地方的配置,大体上都不会出问题。

1:老子《道德经》第三十七章,老子故里,中国鹿邑。

“昔之得一者:天一以清,地得一以宁,神得一以灵,谷得一以盈,侯王得一以为天下正,其至也,谓:天毋已清将恐裂,地毋已宁将恐发;神毋已灵将恐歇,故毋已盈将恐竭,侯王毋已贵以高将恐蹶。
故必贵而以贱为本,必高矣而以下为基。
夫是以侯王自谓孤、寡、不榖。
此其贱之本与,非也?
故致数与无与。
是故不欲禄禄如玉,珞珞如石。”1

MaxCompute

MaxCompute,前身ODPS(Open Data Processing Service),是阿里巴巴通用计算平台提供的一种快速、完全托管的 GB/TB/PB 级数据仓库解决方案,MaxCompute 向用户提供了完善的数据导入方案以及多种经典的分布式计算模型,能够更快速的解决用户海量数据计算问题。

本篇参考maxcompute官方文档而来,有兴趣可以前往

SQL

maxcompute sql可以看作是标准sql的子集,但是也有差异

运算符

操作符 说明
like 通配符
如果A或B为NULL,返回NULL,A为字符串,B为要匹配的模式, 如果匹配,返回TRUE,否则返回FALSE。’%’匹配任意多个字符,’_‘匹配单个字符。要匹配’%’或’_’需要用转义符表示’%’,’_’。
rlike 正则;A是字符串,B是字符串常量正则表达式; 如果匹配成功,返回TRUE,否则返回FALSE; 如果B为空串会报错退出;如果A或B为NULL,返回NULL;
A & B 返回A与B进行按位与的结果。例如:1&2返回0,1&3返回1,NULL与任何值按位与都为NULL。 A和B必须为Bigint类型。
A | B 返回A与B进行按位或的结果。例如:1 |2返回3,1 |3返回3,NULL与任何值按位或都为NULL。 A和B 必须为Bigint类型。
1
select * from user where user_name like '%goshine_user%';
2
select * from user where user_name rlike '^goshine_user*';
关于正则表达式的笔记请参考我的另一篇笔记《从java(python)到scala的n种记忆》

由于 double 值存在一定的精度差,因此,不建议您直接使用等号对两个 double 类型的数据进行比较。您可以使用两个 double 类型相减,然后取绝对值的方式进行判断。当绝对值足够小时,认为两个 double 数值相等。

1
abs(0.9999999999 - 1.0000000000) < 0.000000001
2
-- 0.9999999999和1.0000000000为10位精度,而0.000000001为9位精度。
3
-- 此时可以认为0.9999999999和1.0000000000相等。

类型转换

分为隐式类型转换和显示类型转换,显示使用cast:

1
select case(user_id as double) as new_id from user;
2
select cast('2015-10-01 00:00:00' as datetime) as new_date from user;
  • date和string类型之间的转换
    严格按照格式形如:yyyy-mm-dd hh:mi:ss,除正常的转换之外,妖孽一点的可以用TO_DATE:
    1
    to_date('阿里巴巴2010-12*03', '阿里巴巴yyyy-mm*dd') = 2010-12-03 00:00:00
    2
    to_date('20080718', 'yyyymmdd') = 2008-07-18 00:00:00
    3
    to_date('200807182030','yyyymmddhhmi')=2008-07-18 20:30:00
    4
    to_date('2008718', 'yyyymmdd')
    5
    -- 格式不符合,引发异常
    6
    to_date('阿里巴巴2010-12*3', '阿里巴巴yyyy-mm*dd')
    7
    -- 格式不符合,引发异常
    8
    to_date('2010-24-01', 'yyyy')
    9
    -- 格式不符合,引发异常
    同时maxcompute提供了一些列其他的内建函数(DATEADD、DATEDIFF、DATEPART、 DATETRUNC、FROM_UNIXTIME、GETDATE、ISDATE、LASTDAY等),一定会经常用到,可以去看看

DDL语句

  • 创建表
    要加if not exists!
    1
    CREATE [EXTERNAL] TABLE [IF NOT EXISTS] table_name
    2
    [(col_name data_type [COMMENT col_comment], ...)]
    3
    [COMMENT table_comment]
    4
    [PARTITIONED BY (col_name data_type [COMMENT col_comment], ...)]
    5
    [STORED BY StorageHandler] -- 仅限外部表
    6
    [WITH SERDEPROPERTIES (Options)] -- 仅限外部表
    7
    [LOCATION OSSLocation];-- 仅限外部表
    8
    [LIFECYCLE days]
    9
    [AS select_statement]
    10
    CREATE TABLE [IF NOT EXISTS] table_name
    11
    LIKE existing_table_name
    一张表最多允许60000个分区,单表的分区层次不能超过6级。
    lifecycle表的生命周期,单位:天。create table like语句不会复制源表的生命周期属性。
    创建一张表:
    1
    create table if not exists sale_detail(
    2
     shop_name     string,
    3
     customer_id   string,
    4
     total_price   double)
    5
     partitioned by (sale_date string,region string);
    6
     -- 创建一张分区表 sale_detail
    复制表,方式一:建表的同时将数据复制到新表
    1
    create table sale_detail_ctas1 as
    2
        select * from sale_detail;

创建的表不会复制分区属性,只会把源表的分区列作为目标表的一般列处理,即sale_detail_ctas1是一个含有5列的非分区表。
复制表,方式二:指定列的名字

1
create table sale_detail_ctas2 as
2
        select shop_name,
3
            customer_id,
4
            total_price,
5
            '2013' as sale_date,
6
            'China' as region
7
        from sale_detail;

复制表,方式三:不指定列的名字

1
create table sale_detail_ctas3 as
2
        select shop_name,
3
            customer_id,
4
            total_price,
5
            '2013',
6
            'China'
7
        from sale_detail;

创建的表sale_detail_ctas3的第四、五列会是类似_c5_c6
复制表,方式三:表和目标表具有相同的表结构

1
create table sale_detail_like like sale_detail;

注意:貌似,建表的时候只是指定了列的类型,并没有指定列的长度。

  • 查看表信息
    1
    desc <table_name>;
    2
    desc extended <table_name>;--查看外部表信息
  • 删除表
    要加if exists!
    1
    DROP TABLE [IF EXISTS] table_name;
  • 重命名表
    1
    ALTER TABLE table_name RENAME TO new_table_name;
    ^o^,逻辑有if exists的判断,但是形式上却没有嵌入的语法。
  • 修改表的注释
    1
    ALTER TABLE table_name SET COMMENT 'tbl comment';
  • 修改表的修改时间
    MaxCompute SQL提供touch操作用来修改表的LastDataModifiedTime。效果会将表的LastDataModifiedTime修改为当前时间。此操作会改变表的LastDataModifiedTime的值,此时,MaxCompute会认为表的数据有变动,生命周期的计算会重新开始
    1
    ALTER TABLE table_name TOUCH;
  • 清空非分区表里的数据
    将指定的非分区表中的数据清空,该命令不支持分区表。对于分区表,可以用ALTER TABLE table_name DROP PARTITION的方式将分区里的数据清除。
    1
    TRUNCATE TABLE table_name;
  • 修改表的生命周期
    MaxCompute提供数据生命周期管理功能,以方便您释放存储空间,简化回收数据的流程。
    1
    ALTER TABLE table_name SET lifecycle days;
    如:
    1
    create table test_lifecycle(key string) lifecycle 100;
    2
    -- 新建test_lifecycle表,生命周期为100天。
    3
    alter table test_lifecycle set lifecycle 50;
    4
    -- 修改test_lifecycle表,将生命周期设为50天。
  • 禁止生命周期
    某些情况下,部分特定的分区不希望被生命周期功能自动回收掉,比如一个月的月初或双十一期间的数据,此时您可以禁止该分区被生命周期功能回收。
    1
    ALTER TABLE table_name [partition_spec] ENABLE|DISABLE LIFECYCLE;
    如:
    1
    ALTER TABLE trans PARTITION(dt='20141111') DISABLE LIFECYCLE;
  • 创建视图
    要加if not exists!

视图没有分区(partition)的概念

1
CREATE [OR REPLACE] VIEW [IF NOT EXISTS] view_name
2
[(col_name [COMMENT col_comment], ...)]
3
[COMMENT view_comment]
4
[AS select_statement]

1.视图只能包含一个有效的select语句。
2.不允许向视图写入数据,例如使用insert into或者insert overwrite操作视图。
3.当建好视图后,如果视图的引用表发生了变更,有可能导致视图无法访问,例如删除被引用表。您需要自己维护引用表及视图之间的对应关系。

如:

1
create view if not exists sale_detail_view
2
(store_name, customer_id, price, sale_date, region)
3
comment 'a view for table sale_detail'
4
as select * from sale_detail;
  • 删除视图
    要加if exists!
    1
    DROP VIEW [IF EXISTS] view_name;
  • 重命名视图
    1
    ALTER VIEW view_name RENAME TO new_view_name;
    如:
    1
    create view if not exists sale_detail_view
    2
            (store_name, customer_id, price, sale_date, region)
    3
            comment 'a view for table sale_detail'
    4
            as select * from sale_detail;
    5
    alter view sale_detail_view rename to market;
  • 添加分区
    1
    ALTER TABLE TABLE_NAME ADD [IF NOT EXISTS] PARTITION partition_spec
    2
        partition_spec:
    3
            : (partition_col1 = partition_col_value1, partition_col2 = partiton_col_value2, ...)

    1.仅支持新增分区,不支持新增分区字段。
    2.目前MaxCompute单表支持的分区数量上限为6万。
    3.对于多级分区的表,如果想添加新的分区,必须指明全部的分区值。

如:

1
alter table sale_detail add if not exists partition (sale_date='201312', region='hangzhou');
2
        -- 成功添加分区,用来存储2013年12月杭州地区的销售记录。
3
alter table sale_detail add if not exists partition (sale_date='201312', region='shanghai');
4
        -- 成功添加分区,用来存储2013年12月上海地区的销售记录。
5
alter table sale_detail add if not exists partition(sale_date='20111011');
6
        -- 仅指定一个分区sale_date,出错返回
7
alter table sale_detail add if not exists partition(region='shanghai');
8
        -- 仅指定一个分区region,出错返回
  • 删除分区
    1
    ALTER TABLE TABLE_NAME DROP [IF EXISTS] PARTITION partition_spec;
    2
    partition_spec:
    3
            : (partition_col1 = partition_col_value1, partition_col2 = partiton_col_value2, ...)
    如:
    1
    alter table sale_detail drop if exists partition(sale_date='201312',region='hangzhou');
    2
        -- 成功删除2013年12月杭州分区的销售。
  • 添加列
    1
    ALTER TABLE table_name ADD COLUMNS (col_name1 type1, col_name2 type2...)

添加的新列不支持指定顺序,默认在最后一列。

  • 修改列名
    1
    ALTER TABLE table_name CHANGE COLUMN old_col_name RENAME TO new_col_name;
  • 修改列、分区注释
    1
    ALTER TABLE table_name CHANGE COLUMN col_name COMMENT comment_string;
  • 同时修改列名及列注释
    1
    ALTER TABLE table_name CHANGE COLUMN old_col_name new_col_name column_type COMMENT column_comment;
  • 修改表、分区的修改时间
    MaxCompute SQL提供touch操作用来修改分区的LastDataModifiedTime。效果会将分区的LastDataModifiedTime修改为当前时间。
    1
    ALTER TABLE table_name TOUCH PARTITION(partition_col='partition_col_value', ...);

此操作会改变表的LastDataModifiedTime的值,此时,MaxCompute会认为表或分区的数据有变动,生命周期的计算会重新开始。

  • 修改分区值
    MaxCompute SQL支持通过rename操作更改对应表的分区值。
    1
    ALTER TABLE table_name PARTITION (partition_col1 = partition_col_value1, partition_col2 = partiton_col_value2, ...)
    2
    RENAME TO PARTITION (partition_col1 = partition_col_newvalue1, partition_col2 = partiton_col_newvalue2, ...);

    Insert操作

  • 更新表中的数据 insert overwrite/into
    1
    INSERT OVERWRITE|INTO TABLE tablename [PARTITION (partcol1=val1, partcol2=val2 ...)] [(col1,col2 ...)]
    2
    select_statement
    3
    FROM from_statement;

场景:在MaxCompute SQL处理数据的过程中,Insert overwrite/into用于将计算的结果保存目标表中,以供下一步计算使用。
overwrite/into区别:Insert into会向表或表的分区中追加数据,而Insert overwrite则会在向表或分区中插入数据前清空表中的原有数据。
如:

1
create table sale_detail_insert like sale_detail;
2
alter table sale_detail_insert add partition(sale_date='2013', region='china');
3
insert overwrite table sale_detail_insert partition (sale_date='2013', region='china')
4
        select shop_name, customer_id, total_price from sale_detail;

注意:在进行Insert更新数据操作时,源表与目标表的对应关系依赖于在select子句中列的顺序,而不是表与表之间列名的对应关系:

1
insert overwrite table sale_detail_insert partition (sale_date='2013', region='china')
2
        select customer_id, shop_name, total_price from sale_detail;
3
    -- 在创建sale_detail_insert表时,列的顺序为:
4
    -- shop_name string, customer_id string, total_price bigint
5
    -- 而从sale_detail向sale_detail_insert插入数据是,sale_detail的插入顺序为:
6
    -- customer_id, shop_name, total_price
7
    -- 此时,会将sale_detail.customer_id的数据插入sale_detail_insert.shop_name
8
    -- 将sale_detail.shop_name的数据插入sale_detail_insert.customer_id

非法语句,场景一:向某个分区插入数据时,分区列不允许出现在select列表中:

1
insert overwrite table sale_detail_insert partition (sale_date='2013', region='china')
2
        select shop_name, customer_id, total_price, sale_date, region  from sale_detail;
3
    -- 报错返回,sale_date,region 为分区列,不允许出现在静态分区的 insert 语句中。

非法语句,场景二:partition的值只能是常量,不可以出现表达式。

1
insert overwrite table sale_detail_insert partition (sale_date=datepart('2016-09-18 01:10:00', 'yyyy') , region='china')
2
        select shop_name, customer_id, total_price from sale_detail;
  • 多路输出 Multi insert
    MaxCompute SQL支持在一个语句中插入不同的结果表或者分区。
    1
    FROM from_statement
    2
            INSERT OVERWRITE | INTO TABLE tablename1 [PARTITION (partcol1=val1, partcol2=val2 ...)]
    3
                select_statement1 [FROM from_statement]
    4
            [INSERT OVERWRITE | INTO TABLE tablename2 [PARTITION (partcol1=val3, partcol2=val4 ...)]
    5
                select_statement2 [FROM from_statement]]

注意:对于同一张分区表的不同分区,不能同时有Insert overwrite和Insert into操作,否则报错返回。

1
create table sale_detail_multi like sale_detail;
2
3
from sale_detail
4
        insert overwrite table sale_detail_multi partition (sale_date='2010', region='china' )
5
            select shop_name, customer_id, total_price where .....
6
        insert overwrite table sale_detail_multi partition (sale_date='2011', region='china' )
7
            select shop_name, customer_id, total_price where .....;
8
    -- 成功返回,将 sale_detail 的数据插入到 sales 里的 2010 年及 2011 年中国大区的销售记录中。
9
10
from sale_detail
11
        insert overwrite table sale_detail_multi partition (sale_date='2010', region='china' )
12
            select shop_name, customer_id, total_price
13
        insert overwrite table sale_detail_multi partition (sale_date='2010', region='china' )
14
            select shop_name, customer_id, total_price;
15
    -- 出错返回,同一分区出现多次。
16
17
from sale_detail
18
        insert overwrite table sale_detail_multi partition (sale_date='2010', region='china' )
19
            select shop_name, customer_id, total_price
20
        insert into table sale_detail_multi partition (sale_date='2011', region='china' )
21
            select shop_name, customer_id, total_price;
22
    -- 出错返回,同一张表的不同分区,不能同时有 insert overwrite 和 insert into 操作。
  • 输出到动态分区 Dynamic partition
    在Insert overwrite到一张分区表时,可以在语句中指定分区的值。也可以用另外一种更加灵活的方式,在分区中指定一个分区列名,但不给出值。相应地,在select子句中的对应列来提供分区的值。
    1
    insert overwrite table tablename partition (partcol1, partcol2 ...) select_statement from from_statement;
    如:
    1
    create table total_revenues (revenue bigint) partitioned by (region string);
    2
        insert overwrite table total_revenues partition(region)
    3
            select total_price as revenue, region
    4
                from sale_detail;
    在SQL运行之前,是不知道会产生哪些分区的,只有在select运行结束后,才能由region字段产生的值确定会产生哪些分区,这也是叫做动态分区的原因。

“一个顺序”需要谨记:

1
create table sale_detail_dypart like sale_detail;--创建示例目标表
2
3
insert overwrite table sale_detail_dypart partition (sale_date, region)
4
        select shop_name,customer_id,total_price,sale_date,region from sale_detail;
5
    -- 成功返回;

此时sale_detail表中,sale_date的值决定目标表的sale_date分区值,region的值决定目标表的region分区值。约定俗成select的最后的字段为partition的字段,双发的值只与其顺序有关!

  • Values
    通常在业务测试阶段,需要给一个小数据表准备些基本数据,您可以通过INSERT … VALUES的方法快速对测试表写入一些测试数据。
    1
    INSERT  INTO  TABLE  tablename [PARTITION (partcol1=val1, partcol2=val2 ...)][co1name1,colname2...] VALUES (col1_value,col2_value,...)[,(col1_value,col2_value,...),...]
    场景一,插入整行数据:
    1
    drop table if exists srcp;
    2
    create table if not exists srcp (key string ,value bigint) partitioned by (p string);
    3
    insert into table srcp partition (p='abc') values ('a',1),('b',2),('c',3);
    4
    5
    --
    6
    +-----+------------+---+
    7
    | key | value      | p |
    8
    +-----+------------+---+
    9
    | a   | 1          | abc |
    10
    | b   | 2          | abc |
    11
    | c   | 3          | abc |
    12
    +-----+------------+---+

场景二,插入部分列:

1
drop table if exists srcp;
2
create table if not exists srcp (key string ,value bigint) partitioned by (p string);
3
insert into table srcp partition (p)(key,p) values ('d','20170101'),('e','20170101'),('f','20170101');
4
5
--
6
+-----+------------+---+
7
| key | value      | p |
8
+-----+------------+---+
9
| d   | NULL       | 20170101 |
10
| e   | NULL       | 20170101 |
11
| f   | NULL       | 20170101 |
12
+-----+------------+---+

对于在values中没有制定的列,可以看到取缺省值为NULL。插入列表功能不一定和values一起用,对于Insert into…select…,同样可以使用。

Insert…values有一个限制:values必须是常量,但是有时候希望在插入的数据中进行一些简单的运算,此时可以使用MaxCompute的values table功能,详情见场景三。

场景三,动态插入:

1
drop table if exists srcp;
2
create table if not exists srcp (key string ,value bigint) partitioned by (p string);
3
insert into table srcp partition (p) select concat(a,b), length(a)+length(b),'20170102' from  values ('d',4),('e',5),('f',6) t(a,b);
4
5
--
6
+-----+------------+---+
7
| key | value      | p |
8
+-----+------------+---+
9
| d4  | 2          | 20170102 |
10
| e5  | 2          | 20170102 |
11
| f6  | 2          | 20170102 |
12
+-----+------------+---+

其中的values (…), (…) t (a, b),相当于定义了一个名为t,列为a,b的表,类型为(a string,b bigint),其中的类型从values列表中推导。这样在不准备任何物理表的时候,可以模拟一个有任意数据的,多行的表,并进行任意运算。

Select操作

1
SELECT [ALL | DISTINCT] select_expr, select_expr, ...
2
        FROM table_reference
3
        [WHERE where_condition]
4
        [GROUP BY col_list]
5
        [ORDER BY order_condition]
6
        [DISTRIBUTE BY distribute_condition [SORT BY sort_condition] ]
7
        [LIMIT number]

设置分区条件来指定扫描的分区!,当需要对分区进行全表扫描的时候,加上set odps.sql.allow.fullscan=true;执行的时候,set语句和sql语句一起提交执行。如:

1
set odps.sql.allow.fullscan=true;
2
select * from sale_detail;

如果需要整个项目都允许全表扫描,可以通过开关自行打开或关闭(true/false),命里如下:

1
setproject odps.sql.allow.fullscan=true;

distribute by:对数据按照某几列的值做 hash 分片,必须使用 Select 的输出列别名(当没有别名时用列名,当有别名时必须用别名,否则报错!)。
sort by:局部排序,语句前必须加 distribute by。实际上 sort by 是对 distribute by 的结果进行局部排序。必须使用 Select 的输出列别名。
order by:对所有数据按照某几列进行全局排序,order by 必须与 limit 共同使用。

  • select 语序

    1
    SELECT key, max(value) FROM src t WHERE value > 0 GROUP BY key HAVING sum(value) > 100 ORDER BY key LIMIT 100;

    实际上的逻辑执行顺序是FROM->WHERE->GROUY BY->HAVING->SELECT->ORDER BY->LIMIT
    MaxCompute 支持以执行顺序书写查询语句,例如上面的语句可以写为:

    1
    FROM src t WHERE value > 0 GROUP BY key HAVING sum(value) > 100 SELECT key, max(value) ORDER BY key LIMIT 100;
  • 子查询

子查询必须有别名。
在 from 子句中,子查询可以当作一张表来使用,与其它的表或子查询进行 Join 操作,如下所示:

1
create table shop as select * from sale_detail;
2
select a.shop_name, a.customer_id, a.total_price from
3
        (select * from shop) a join sale_detail on a.shop_name = sale_detail.shop_name;

1:老子《道德经》第三十九章,老子故里,中国鹿邑。

“道可道,非常道;名可名,非常名。
无名,万物之始,有名,万物之母。
故常无欲,以观其妙,常有欲,以观其徼。
此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。” ^1

Nosql的鼻祖往前追朔在Java这一支是要落到HashMap的头上的。关于HashMap,jdk1.8和之前的版本相比有较大的改动。在JDK1.8之前,HashMap采用数组+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。而JDK1.8中,HashMap采用数组+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。

先来看一下 HashMap 的继承图:
类继承图

HashMap 根据键的 hashCode 值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap 最多只允许一条记录的键为 null ,允许多条记录的值为 null 。HashMap 非线程安全,即任一时刻可以有多个线程同时写 HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections的synchronizedMap 方法使 HashMap 具有线程安全的能力,或者使用ConcurrentHashMap 。
#####HashMap 存储结构
HashMap是数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的,链表和红黑树都是为了优化hash值冲突而实行的优化方案,当链表的长度大于8时链表将转换成红黑树,如下图所示:
存储结构

#####HashMap 各常量、成员变量作用  

1
/**
2
     * The table, initialized on first use, and resized as
3
     * necessary. When allocated, length is always a power of two.
4
     * (We also tolerate length zero in some operations to allow
5
     * bootstrapping mechanics that are currently not needed.)
6
     * table就是存储Node类的数组
7
*/  
8
transient Node<K,V>[] table;  
9
10
/**
11
  * The number of key-value mappings contained in this map.
12
  *  记录hashmap中存储键-值对的数量
13
  */  
14
transient int size;  
15
16
/**
17
  * hashmap结构被改变的次数,fail-fast机制
18
  */  
19
transient int modCount;  
20
21
    /**
22
     * The next size value at which to resize (capacity * load factor).
23
     * 扩容的门限值,当size大于这个值时,table数组进行扩容
24
     */  
25
    int threshold;  
26
27
    /**
28
     * The load factor for the hash table.
29
     *
30
     */  
31
 float loadFactor;  
32
/**
33
     * The default initial capacity - MUST be a power of two.
34
     * 默认初始化数组大小为16
35
     */  
36
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16  
37
38
    /**
39
     * The maximum capacity, used if a higher value is implicitly specified
40
     * by either of the constructors with arguments.
41
     * MUST be a power of two <= 1<<30.
42
     */  
43
    static final int MAXIMUM_CAPACITY = 1 << 30;  
44
45
    /**
46
     * The load factor used when none specified in constructor.
47
     * 默认装载因子,
48
     */  
49
    static final float DEFAULT_LOAD_FACTOR = 0.75f;  
50
51
    /**
52
     * The bin count threshold for using a tree rather than list for a
53
     * bin.  Bins are converted to trees when adding an element to a
54
     * bin with at least this many nodes. The value must be greater
55
     * than 2 and should be at least 8 to mesh with assumptions in
56
     * tree removal about conversion back to plain bins upon
57
     * shrinkage.
58
     * 这是链表的最大长度,当大于这个长度时,链表转化为红黑树
59
     */  
60
    static final int TREEIFY_THRESHOLD = 8;  
61
62
    /**
63
     * The bin count threshold for untreeifying a (split) bin during a
64
     * resize operation. Should be less than TREEIFY_THRESHOLD, and at
65
     * most 6 to mesh with shrinkage detection under removal.
66
     */  
67
    static final int UNTREEIFY_THRESHOLD = 6;  
68
69
    /**
70
     * The smallest table capacity for which bins may be treeified.
71
     * (Otherwise the table is resized if too many nodes in a bin.)
72
     * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
73
     * between resizing and treeification thresholds.
74
     */  
75
    static final int MIN_TREEIFY_CAPACITY = 64;

“执大象,天下往。
往而不害,安平太。
乐与饵,过客止。
道之出口,淡乎其无味,视之不足见,听之不足闻,用之不足既。”1

scala作为支持函数式编程的语言,scala函数式编程是scala的重中之重,spark当中的计算都是用scala函数式编程来做,高级函数也是其独特的一个特性,并且spark基于集合,这样可以使scala发挥其对于集合计算的强大功能。首先,函数/变量同是一等公民,函数与变量同等地位,函数的定义可以单独定义,可以不依赖于类、接口或者object,而且独立存在,独立使用,并且可以赋值给变量。

函数传名调用(Call-by-Name)、传值调用(Cal-by-Value)

Scala的解释器在解析函数参数(function arguments)时有两种方式:

  • 传值调用(call-by-value)
    先计算参数表达式的值(reduce the arguments),再应用到函数内部;
  • 传名调用(call-by-name)
    将未计算的参数表达式直接应用到函数内部。
    1
    package com.cgoshine.sh.demo  
    2
    3
    object Add {  
    4
      def addByName(a: Int, b: => Int) = a + b   
    5
      def addByValue(a: Int, b: Int) = a + b   
    6
    }
    addByName是传名调用,addByValue是传值调用。语法上可以看出,使用传名调用时,在参数名称和参数类型中间有一个=>符号。

觉得传名参数会很少用到,跟java的传参习惯看起就可以了。

指定函数参数名

一般情况下函数调用参数,就按照函数定义时的参数顺序一个个传递。但是我们也可以通过指定函数参数名,并且不需要按照顺序向函数传递参数。

1
object Test {
2
   def main(args: Array[String]) {
3
        printInt(b=5, a=7);
4
   }
5
   def printInt( a:Int, b:Int ) = {
6
      println("Value of a : " + a );
7
      println("Value of b : " + b );
8
   }
9
}

可变参数

Scala 通过在参数的类型之后放一个*星号来设置可变参数(可重复的参数)。

1
object Test {
2
   def main(args: Array[String]) {
3
        printStrings("Java", "Scala", "Python");
4
   }
5
   def printStrings( args:String* ) = {
6
      var i : Int = 0;
7
      for( arg <- args ){
8
         println("Arg value[" + i + "] = " + arg );
9
         i = i + 1;
10
      }
11
   }
12
}

递归函数

1
object Test {
2
   def main(args: Array[String]) {
3
      for (i <- 1 to 10)
4
         println(i + " 的阶乘为: = " + factorial(i) )
5
   }
6
7
   def factorial(n: BigInt): BigInt = {  
8
      if (n <= 1)
9
         1  
10
      else    
11
         n * factorial(n - 1)
12
   }
13
}

默认参数值

Scala 可以为函数参数指定默认参数值,使用了默认参数,你在调用函数的过程中可以不需要传递参数,这时函数就会调用它的默认参数值,如果传递了参数,则传递值会取代默认值。

1
object Test {
2
   def main(args: Array[String]) {
3
        println( "返回值 : " + addInt() );
4
   }
5
   def addInt( a:Int=5, b:Int=7 ) : Int = {
6
      var sum:Int = 0
7
      sum = a + b
8
      return sum
9
   }
10
}

高阶函数

高阶函数(Higher-Order Function)就是操作其他函数的函数。
Scala 中允许使用高阶函数, 高阶函数可以使用其他函数作为参数,或者使用函数作为输出结果。

  • 将定义的函数赋给某变量
    1
    def func1(s:String):Unit = {
    2
        println(s)
    3
      }
    4
    5
    val func2 = func1 _

    val变量名 = 函数名+空格+_

这里函数名后面必须要有空格,表明是函数的原型。
高阶函数是函数的参数也是函数。(因为函数的参数可以是变量,而函数又可以赋值给变量,即函数和变量地位一样,所以函数参数也可以是函数)。

1
scala> val iGreeting = (content:String) => println(content)
2
iGreeting: String => Unit = $$Lambda$1052/5181771@257f30f7
3
scala> def sendGreeting(func:(String) => Unit,content:String){func(content)}
4
sendGreeting: (func: String => Unit, content: String)Unit
5
scala> sendGreeting(iGreeting,"scala")
6
scala

首先我们定义了一个函数sendGreeting,这个函数有两个参数,第一个参数是一个函数,函数名是func,他有一个String类型的参数并且返回值是unit空的;第二个参数是String类型的变量名为content的变量,函数体是将第二个参数作为第一个参数也就是函数func的参数,来调用第一个函数,整个函数返回值为unit空。这里只要传入的函数的格式与定义的一致就行。

1
scala> val array = Array(1,2,3,4,5,6)
2
array: Array[Int] = Array(1, 2, 3, 4, 5, 6)
3
4
scala> array.map(item => item*2)
5
res2: Array[Int] = Array(2, 4, 6, 8, 10, 12)

Array.map()作用,他会遍历array中每一个元素,并将每个元素作为具体的值传给map中的作为参数的函数。

高阶函数有个非常有用的特性是类型推断。其可以自动推断出参数的类型,而且对于只有一个的参数的函数,可以省略掉小括号,并且在函数的参数作用的函数体内只是用一次函数的输入参数的值话,就可省略掉函数名,用下划线(_)代替。

1
scala> val array = Array(1,2,3,4,5,6)
2
array: Array[Int] = Array(1, 2, 3, 4, 5, 6)
3
4
scala> array.map(_ * 2)
5
res3: Array[Int] = Array(2, 4, 6, 8, 10, 12)
6
7
scala> array.map(_ * 2).foreach(println(_))
8
2
9
4
10
6
11
8
12
10
13
12
14
15
scala> array.map(_ * 2).foreach(println _)
16
2
17
4
18
6
19
8
20
10
21
12
22
23
scala> array.map(_ * 2).foreach(println)
24
2
25
4
26
6
27
8
28
10
29
12
30
31
scala> array.map(_ * 2).filter(_ > 6).foreach(println)
32
8
33
10
34
12

内嵌函数

我们可以在 Scala 函数内定义函数,定义在函数内的函数称之为局部函数。

1
object Test {
2
   def main(args: Array[String]) {
3
      println( factorial(0) )
4
      println( factorial(1) )
5
      println( factorial(2) )
6
      println( factorial(3) )
7
   }
8
9
   def factorial(i: Int): Int = {
10
      def fact(i: Int, accumulator: Int): Int = {
11
         if (i <= 1)
12
            accumulator
13
         else
14
            fact(i - 1, i * accumulator)
15
      }
16
      fact(i, 1)
17
   }
18
}

匿名函数

spark中大都用的是匿名函数(不为函数命名),然后将其复制个一个变量。
匿名函数格式:

Val 变量名 = (参数:类型) => 函数体

1
val func1 = (s:String) => println(s)
2
func1("scala")

偏应用函数

Scala 偏应用函数是一种表达式,你不需要提供函数需要的所有参数,只需要提供部分,或不提供所需参数。

1
import java.util.Date
2
3
object Test {
4
   def main(args: Array[String]) {
5
      val date = new Date
6
      log(date, "message1" )
7
      Thread.sleep(1000)
8
      log(date, "message2" )
9
      Thread.sleep(1000)
10
      log(date, "message3" )
11
   }
12
13
   def log(date: Date, message: String)  = {
14
     println(date + "----" + message)
15
   }
16
}

输出结果:

1
$ scalac Test.scala
2
$ scala Test
3
Tue May 22 15:53:39 CST 2018----message1
4
Tue May 22 15:53:39 CST 2018----message2
5
Tue May 22 15:53:39 CST 2018----message3

log() 方法接收两个参数:date 和 message。我们在程序执行时调用了三次,参数 date 值都相同,message 不同。
我们可以使用偏应用函数优化以上方法,绑定第一个 date 参数,第二个参数使用下划线(_)替换缺失的参数列表,并把这个新的函数值的索引的赋给变量。

1
import java.util.Date
2
3
object Test {
4
   def main(args: Array[String]) {
5
      val date = new Date
6
      val logWithDateBound = log(date, _ : String)
7
8
      logWithDateBound("message1" )
9
      Thread.sleep(1000)
10
      logWithDateBound("message2" )
11
      Thread.sleep(1000)
12
      logWithDateBound("message3" )
13
   }
14
15
   def log(date: Date, message: String)  = {
16
     println(date + "----" + message)
17
   }
18
}

输出结果:

1
$ scalac Test.scala
2
$ scala Test
3
Tue May 22 15:53:39 CST 2018----message1
4
Tue May 22 15:53:39 CST 2018----message2
5
Tue May 22 15:53:39 CST 2018----message3

函数柯里化(Function Currying)

柯里化(Currying)指的是将原来接受两个参数的函数变成新的接受一个参数的函数的过程。新的函数返回一个以原有第二个参数为参数的函数。其也利用了闭包的特性。
定义一个函数:

1
def add(x:Int,y:Int)=x+y

把这个函数变一下形:

1
def add(x:Int)(y:Int) = x + y

这种方式(过程)就叫柯里化。

1:老子《道德经》第三十五章,老子故里,中国鹿邑。