有了kubernetes你为什么还要用 springcloud

本来想的标题是 《springcloud 和 kubernetes 比较 》

不过想着想着就放弃了,因为本来的出发点就是,有了kubernetes我为啥还要用springcloud … 想了半天,完全没理由啊

就好像机箱为啥买太阳神,就为了ROG全家桶?没理由啊

第一次用springcloud 还是在 2017年,那时候微服务概念刚刚兴起 ( 也可能是孤陋寡闻的我刚刚听说 ),于是做了一个基于 Docker 的 springcloud 架构

原理就是 Docker 封装的 Springboot应用 然后构成了一个 springcloud 平台,反正当时kubernetes还不熟

不过时至今日,我真心想不出如果重新为一个新项目做技术选型,我为啥还要用springcloud

微服务架构,主要组成有,简单列一些

看来看去大家都一样么,这里你看看 配置中心,既然,kubernetes的 configmap 能做这么多,为什么还要用 Config server 呢?

你可以理解成,springcloud只能用在 springboot的java环境中,而kubernetes明显是通吃,只要能被塞进 docker 的应用,包括 springboot ,都可以在kubernetes上运行,而且更轻量,更简单

这里最关键的点还在于,Java本身就不适合做微服务,即便是一个最简单的 helloworld springboot 镜像,也需要300M,没有其他办法,JVM的大小毕竟摆在那里,一个简单的 tomcat从启动到完成,也需要大概10秒才能初始化一个 ioc 容器, 这些都太耗时了,大规模的微服务平台,一个非常重要的特点就是 容器可以 优雅的启动 然后 优雅的销毁,Java根本做不到,论易用性不如Python,论性能和易于部署不如Golang,论前后端统一不如 Nodejs

如果但得你还有精力学习除了java之外的新知识,我都不建议使用java或者springcloud来构建微服务架构,虽然做过10年Java,但是在微服务云架构上,我要给Java头完全反对票

总上所述,spring cloud 也好,kubernetes 也好,实际上都是 云原生十二要素的一种实践

  1. 一份代码,多份部署
  2. 依赖
  3. 配置中心
  4. 服务
  5. 构建,发布,运行
  6. 无状态独立进程
  7. 数据隔离
  8. 并发
  9. 易处理,能被优雅的启动和销毁
  10. 开发环境和生产环境等价
  11. 日志管理
  12. 管理进程 ( job )

What is devops

最近这一年里, 我谈论了太多devops, 就像几年前我们谈论了太多敏捷

DevOps 这儿… DevOps 那儿… but what is exactly DevOps is …

如果你打开百度, 搜索关键字 DevOps 你会得到

 

DevOps(Development和Operations的组合词)是一组过程、方法与系统的统称,用于促进开发(应用程序/软件工程)、技术运营和质量保障(QA)部门之间的沟通、协作与整合。

但是, 我们真的需要说得这样 概念化么? 它到底是什么, 它又如何在项目中落地, 它的核心思想到底是什么呢, 这几天这个问题一直在困扰我.

早在2010年, 当时我还是一个只有2年半工作经验的Junior developer, 我们有差不多130个人工作在一个网银系统项目中, banking 系统, 信用卡系统, 登录系统, core系统, 基于SOAP的webservice接口系统… 所有这些都必需编译并且集成到一起

那么在那个年代我们是如何把这些都组织在一起的? 我们使用 Hudson, 也就是Jenkins的前身, 每天下午3点之前每个模块的 BA 就会把自己模块对应的changes打上tag, 然后等待编译.

会有一个专门的团队在3点半的时候启动每天的构建工作. 在那个时候还没有DevOps这个词, 也没有什么敏捷的概念, 我们只是这样做了, 因为这个模式确实是行之有效的.

每天3点半都会进行构建、集成, 然后保证第二天每个模块的owner和developer都可以看到自己前一天的工作是否按预期正确运行在了整个系统上.

然后直到 2012年, 我从亚马逊上发现了那本著名的<持续交付>

我意识到 这不正是这几年我们一直在做的事情么?

当然也许DevOps的概念更大一些, 也许DevOps只是现代的版本的持续交付, whatever … 我们确实从这种模式中受益匪浅.

流水线改变世界

1910年,福特汽车在引入流水线生产之后,Model-T 的组装时间缩短了8倍,从12.5小时降到了1.5小时,这就是流水线改变世界的神话,造就了汽车上的国家!

那流水线怎么改变软件交付的世界呢?

马丁福勒大爷在2013年曾经写过这样一篇博客 <DeploymentPipeline>

https://martinfowler.com/bliki/DeploymentPipeline.html

One of the challenges of an automated build and test environment is you want your build to be fast, so that you can get fast feedback, but comprehensive tests take a long time to run. A deployment pipeline is a way to deal with this by breaking up your build into stages. Each stage provides increasing confidence, usually at the cost of extra time. Early stages can find most problems yielding faster feedback, while later stages provide slower and more through probing. Deployment pipelines are a central part of ContinuousDelivery

是的, FAST … FAST 是DevOps 的核心价值, 这个世界上没有任何一个系统是没有bug的, 也就是说出错是一种常态, 不过你有能力做的更快, 就能更早发现问题, 就能更早快速做出相应, 就能更快得到Feedback.

所以不论你尝试如何构建你的 DevOps 工作流, FAST 都是你的核心命题.

那么如何才能做到更快呢?

在2012年, 你似乎无法找到一些特别合适的工具, 软件需要一个一个的安装配置, 虚拟机技术也不是特别完善(至少在生产环境还不是特别完善), 系统管理员往往使用Puppet或者Ansible之类的工具才能同时维护多个服务器.

看看现在, 环顾一下四周,你会发现有太多好用的工具可以选择

Jenkins 已经变得比以前更强大了, 你可以使用 Pipeline 来设计创建出更加复杂的构建集成工作流, Dockerhub可以让你方便的管理和维护你的软件发布包, Kubernetes类似的容器编排工具可以维护更加复杂和庞大的集群,基础架构即代码 如 Terraform 让你可以随时重新创建基础架构,例如在灾备环境重新创建生产环境. 那么终于是时候让所有的概念落地了.

可是好像似乎又缺少些什么, 现代版本的DevOps只是更好的 CI/CD 么? 它和敏捷开发又是什么关系呢?

也就是说DevOps实际上是CI/CD的一种延续, 延续到 运维 方面

在DevOps的工具链中, 我们还是用诸如 ELK、Sonar Qube 之类的工具进行一些 日志收集过滤、代码质量管理之类的工作。

总之,现代DevOps已经融合了很多思想和工具,这里我们更进一步的说,现代 DevOps 实际上是 敏捷开发,CI/CD ,编排工具 的混合产物。

我想到这里我已经把要说的都说清楚了,不过仅仅有DevOps并不能让你适应现代软件开发模式。

IaaS 云服务 + 微服务 才是最适合DevOps的软件架构, 云服务 让程序员突然有了爆炸式的能力去操作更多服务器,微服务的拆分模式也使DevOps可以操作的颗粒度进一步变小。

DevOps + IaaS + 微服务 才是未来。

软件正在吃掉这个世界,而容器正在吃掉软件。

关于 hybris 在 kubernetes 集群中部署的一点感想

从4月份开始着手搭建这套环境已经有半年了,幸亏我有两只手,要不还真不够用

one hand 。。。the other hand 。。。

跟我一开始想的一样,hybris 这种安装完成 就变成几个G 大小的
传统java 应用确实不太适合在 容器编排工具上使用,不是说它不能用,而是,它完全享受不到容器化的好处。

1. 镜像太大,无法拆分

首先,现在的结构里我们可以把hybris拆分成3个镜像(当然每个镜像还是很大)

hybris、solr、mysql 都可以独立成为镜像,
solr 和 mysql 本身都可以做成集群的,分布式也很容易,
mysql 分布式太麻烦或者吞吐量不够的话你还可以花钱解决问题,选择一些和 mysql 兼容的云数据库比如 RDS 什么的,同时也可以解决 数据安全和备份的问题。

但是 hybris,理想状态下,sap一直宣称 hybris 是基于 模块的,addon 什么的,说它易于扩展,方便二次开发,可复用

但是实际上,每个 OOTB 原生功能的addon都不能独立以微服务的方式运行,甚至如果你基于addon的模式进行开发,你的自定义addon也无法以微服务的形式运行。

不仅仅是addon,连  搜索、订单、付款之类的独立服务 也无法拆分,这就造成了一个结果,在容器编排工具环境下,hybris 实例也必需运行在 一个tomcat下,只能做成一个镜像。

2. 无法套用 redis 的 session 管理模型

这个问题每次跟tony老师(我项目组里的资深级hybris老专家,资深到全国可能找不出第二个了,当然是他自己说的,不过我真信了)讨论的时候tony老师都讳莫如深的微微一笑(卿不倾城我不 jidao)

这事得从在东京那次我尝试开始把 hybris集群化开始说起,当时我手里已经有了可用的 hybris 镜像,而且外围系统已经构筑完毕,在我的概念里,我只需要把 hybris 运行环境——我的tomcat做一下克隆,然后把 session管理器替换成基于 redis 的就ok了,说干就干 然后使用helm 我轻松得到了一个可用的 redis 集群,替换了 session manager 类包,spring session 配置之后,我甚至可以看到我的redis中得到了session值

然后我给Jon发了一条消息说,我已经看见session存到redis里了,Jon 贱贱的 (我觉得应该是这个表情)回了一句

try backoffice …

然后果然, backoffice 崩了,前端服务登录都没有问题,backoffice为什么会崩。。。去看kibana,发现log里写的是,session中有对象不能序列化,不能序列化。。。好吧我实在没有本事去改hybris源码,然后暂时选择放弃了这种

3. 分布式方式太老

官方文档中给出的 hybris ootb cluster 实现是这样的

stick session + 数据库session 备份

HTTP Session Failover

Sessions used by clients that are bound to an individual cluster node, stick with the cluster node. Therefore, the SAP Commerce Cluster uses the so-called sticky sessions. In addition, SAP Commerce offers a session failover mechanism. For more details, see HTTP Session Failover.

SAP Commerce session failover is designed with sticky-session load balancing in mind. A load balancer with configured sticky sessions assigns clients to specific cluster nodes. It always forwards requests from the same client to the same server. The server holds the client session cached in memory. A session is a single up-to-date object and there is no need to read it from the database. When the node stops, the load balancer redirects the client requests to another server. That server doesn’t have the respective session object in the cache, so it loads it from the database. Lack of session invalidation makes this approach unsuitable for round-robin session load balancing. 

也就是说,当用户请求进 hybris 之后,负载均衡器会把用户固定在 第一次分配到 的节点上,不支持robbin规则的轮询机制,就更别提基于性能的什么分发规则了,如果这个 节点 宕机了,那么好,它会从数据库里读取对应的session备份,当然仅仅是节点宕机时,session备份才会起作用,所以它倒是不用担心什么吞吐量的问题。

4. jgroups 不支持 kubernetes 

hybris 的集群配置信息里并没有关于 master 节点啊, slave 节点 之类的配置

我禁不住产生了这种疑问,它是如何知道自己集群内部的其他机器的,然后我查阅了一下官方文档

Rate Limit 限流

如果你还记得12306网站那个著名的 “系统太忙请稍后再试”你一定遇到过限流

或者是jd的抢购活动的”服务器太忙请稍后再试” 都是限流的结果

限流的好处在于防止非预期的请求对系统压力过大而引起的系统崩溃

一般Web系统的访问限制都可以用容器本身来实现,比如tomcat就可以在connector上面配置connection数目的限制,servlet thread限制

不过如果不同的URL需要配置不同的限制, tomcat显然是做不到的

这个时候就需要实现自己的限制

常用的限流算法有两种   漏桶算法和令牌桶算法

 漏桶算法

请求会先进入一个漏桶里, 假设漏桶每分钟滴4滴水, 那么就有4个请求在这一分钟里被处理,如果水桶满了, 就意味着可以goto error page 了

令牌桶算法

和漏桶正好相反, 它是生产可用的token然后放进桶里,每个请求到达以后去桶里拿token,如果拿不到就可以 goto error page 了

这两种算法其实都是一个缓冲队列, 如果你要自己实现我推荐你使用rabbitmq之类的消息队列, 如果颗粒度太细的话而且只是实现简单计数功能也可以使用redis incr

如果不需要自己实现使用Google的guava也是一个不错的选择,guava 使用的是令牌桶算法

下面是一个使用springboot+guava的例子

/ratelimitsample/pom.xml

<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>name.lizhe</groupId>
    <artifactId>ratelimitsample</artifactId>
    <version>0.0.1-SNAPSHOT</version>    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.2.RELEASE</version>
    </parent>    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-jasper</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
           <groupId>javax.servlet</groupId>
           <artifactId>jstl</artifactId>
       </dependency>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>14.0.1</version>
        </dependency>
    </dependencies>
</project>

/ratelimitsample/src/main/webapp/WEB-INF/jsp/myerror.jsp

<%@ page language=”java” contentType=”text/html; charset=ISO-8859-1″
    pageEncoding=”ISO-8859-1″%>
<!DOCTYPE html PUBLIC “-//W3C//DTD HTML 4.01 Transitional//EN” “http://www.w3.org/TR/html4/loose.dtd”>
<html>
<head>
<meta http-equiv=”Content-Type” content=”text/html; charset=ISO-8859-1″>
<title>Insert title here</title>
</head>
<body>
error
</body>
</html>

/ratelimitsample/src/main/resources/application.properties

server.port=8080 
spring.mvc.view.prefix=/WEB-INF/jsp/
spring.mvc.view.suffix=.jsp

/ratelimitsample/src/main/java/name/lizhe/filter/RateLimitFilter.java

package name.lizhe.filter;import java.io.IOException;import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;import com.google.common.util.concurrent.RateLimiter;public class RateLimitFilter implements Filter{
    
    private RateLimiter limiter = null;    public void destroy() {
        
    }    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse res = (HttpServletResponse) response;
        
        System.out.println(“doFilter … “);
        
        if(limiter.tryAcquire()) {
             System.out.println(“Accessed sucessfully”);
             chain.doFilter(request, response);
        } else {
            System.out.println(“Accessed Failed”);
             req.getRequestDispatcher(“/myerrorjsp”).forward(req,res);
        }
        
    }    public void init(FilterConfig arg0) throws ServletException {
        limiter = RateLimiter.create(1); //10 request per second
    }}
 

/ratelimitsample/src/main/java/name/lizhe/controller/Test.java

package name.lizhe.controller;import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
public class Test {
    @RequestMapping(“/ratelimit/test”)
    public String test() {
        System.out.println(“test…”);
        return “accessed test”;
    }}

/ratelimitsample/src/main/java/name/lizhe/controller/ErrorController.java

package name.lizhe.controller;import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;@Controller
public class ErrorController {    @RequestMapping(“/myerrorjsp”)
    public String errorJsp() {
        return “myerror”;    }}

/ratelimitsample/src/main/java/name/lizhe/app/SpringRestApplication.java

package name.lizhe.app;import java.util.ArrayList;
import java.util.List;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScans;import name.lizhe.filter.RateLimitFilter;  
    
@SpringBootApplication  
@ComponentScan(“name.lizhe”)
public class SpringRestApplication {  
  
    public static void main(String[] args) {  
        SpringApplication.run(SpringRestApplication.class, args);  
    }  
      
    @Bean  
    public FilterRegistrationBean  filterRegistrationBean() {  
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();  
        RateLimitFilter rateLimitFilter = new RateLimitFilter();  
        registrationBean.setFilter(rateLimitFilter);  
        List<String> urlPatterns = new ArrayList<String>();  
        urlPatterns.add(“/ratelimit/*”);  
        registrationBean.setUrlPatterns(urlPatterns);  
        return registrationBean;  
    }  
}  
 

关于”在用户设定的时间发送邮件”的功能设计

这个问题其实还是Brian问的, 他真的去做那个 “拍卖系统” 了

这个拍卖系统有一个需求是  ” 假设用户设定的拍卖开始时间是2017年1月1日中午12点, 那么系统需要在拍卖会开始之前半个小时给参与的bidder们发送提醒邮件”

这个功能看起来貌似很简单, 不过仔细想起来以前好像还真没碰到类似的

1. 发送邮件的job触发一次之后, 就会被移除 ( 不像springbatch或者crontab那种定时器触发, 要触发好多次)

2. job的数量不固定, 以前做的job都是程序员设定的, 不会由用户创建, 所以如果job数量特别大,可能会出问题

3. job的触发时间是用户设定的, 如果同一时间有大量job需要触发, 而且如果邮件发送特别慢的话,也可能会出问题

下面给出两个解决方案

  • 使用atd服务实现
  • 使用redis+rabbitmq写代码实现

总的来说各有利弊, 下面请看详细


第一种, 使用atd服务实现

安装atd服务
yum -y install at

启动atd服务
systemctl start atd
systemctl status atd

直接手动调用
[root@vagrant spool]# at 10:20 7/17/2017 
at> ls
at> <EOT>   ( 键盘 crtl+D )
job 3 at Mon Jul 17 10:20:00 2017
[root@vagrant spool]# at -l
3       Mon Jul 17 10:20:00 2017 a root


通过管道调用
touch testad |  at 10:22 7/17/2017


安装sendmail
yum install sendmail

Sendmail的配置文件由m4来生成,m4工具在sendmail-cf包中
yum install sendmail-cf

如果需要其他ip访问修改

vi /etc/mail/sendmail.mc
DAEMON_OPTIONS(`Port=smtp,Addr=127.0.0.1, Name=MTA’)dnl

发邮件命令
echo “mail content”|mail -s test admin@test.com

我们要做的就是,把上面两个命令结合起来

echo “mail content at 10:45 7/17/2017 “|mail -s test admin@test.com |  at 10:45 7/17/2017

采用这种方式的话如果是简单的邮件内容和需求很简单而且几乎没有什么开发成本,但是如果需要监控邮件发送状态或者其他一些复杂业务
可能需要使用额外的脚本(例如使用java或者Python)来完成业务逻辑

这种方式还有一个问题是,如果等待发送的邮件特别多, 比如超过1万封邮件要在某个时间段发出去,而服务器根本处理不过来怎么办

实际上我也没想出什么”万全之策” 

不过如果你也没有好办法, 可以考虑我下面的模型


第二种写代码用redis+rabbitmq实现

使用一个简单的数据库(我建议你使用redis或类似产品以获得最大的查询性能及持久化支持)
每次客户端创建了一个定时发送邮件请求之后, 存储相关信息到数据库
例如
Tom 需要在下午1点发送一封指定内容的邮件给客户 t1,t2,t3,t4,t5 
Jerry 需要在下午1点半发送一封指定内容的邮件给客户 j1,j2,j3,j4,j5,j6

这里我使用json格式保存邮件内容信息
{“title”:”Test Mail Subject”,”content”:”this is a test content”,”receivers”:[“zhe.li@test.com”,”test@test.com”]}

在存入redis数据库时, 该数据会按照zset (有序集合) 的类型存入, 而有序集合这个key, 我们使用Long型的TimeInMillis
在这个例子中,我会取得当前时间now, 然后设定邮件需要在2分钟之后被发送
例如 现在是2017年7月17日8:00, 例子中设定2分钟之后发送是8:02

Calendar now = Calendar.getInstance();
now.add(Calendar.MINUTE, 2);
Long after2mins = now.getTimeInMillis();

然后使用camel (或者spring integration) 来轮询这个数据库 ( 例如每分钟查询一次 ), 以获取当前时间1分钟之内需要发送的邮件
当时间来到8:02时, camel会得到之前设定的8:02需要发送的数据
由于redis的遍历速度非常快, 所以即使在查询间隔中有大量数据涌入也不用怕

因为假设了是大量的邮件等待发送, 所以这里不建议你直接对数据进行操作,而是通过camel的processor直接将数据库的数据整理,
然后发给rabbitmq, rabbitmq会作为一个缓冲存在, 在转发给mq之后, 删除redis中的数据

例如我将 json数据 {“title”:”Test Mail Subject”,”content”:”this is a test content”,”receivers”:[“zhe.li@test.com”,”test@test.com”]} 存入数据库

camel遍历出这条记录之后会向rabbitmq发送两条记录(拆分成两个邮件)
Producer Send +'{“title”:”Test Mail Subject”,”content”:”this is a test content”,”receiver”:”zhe.li@test.com”}’
Producer Send +'{“title”:”Test Mail Subject”,”content”:”this is a test content”,”receiver”:”test@test.com”}’

注意 redis 对于范围查询的api
获得key=schmails 下的一个范围的集合
Set<String> results = jedis.zrangeByScore(“schmails”, before1mins.getTimeInMillis(), now.getTimeInMillis());
删除key=schmails 下的一个范围的集合
jedis.zremrangeByScore(“schmails”, before1mins.getTimeInMillis(), now.getTimeInMillis());


然后接着会有一个(或多个)camel processor 每30秒就来轮询这个mq, 一得到消息, 直接发送邮件, 这个解决方案的好处在于
由于发送邮件的业务逻辑中,往往网络开销比较大,服务器在发送邮件时可能会遭遇性能瓶颈, 但是我们这个方案因为采用了 生产者+消费者 模式, 所以你可以很容易的构建多个 “邮件发送服务器”构成一个发送集群来作为消费者


假设有3台server不停的轮询从mq里取得消息,然后一起发送邮件,性能和扩展性可以得到大大改善

存入mq时

可以看到mq中的两条消息

如何快速构建开发原型

Brian (还是稍不留神就能打成brain =。=) 的offer差不多到手了, 为了感谢我的技术支持拉着Roc和Maven 晚上小聚了一下

总的来说我还是非常喜欢他抛给我那两套题的 ( 额…如果这东西能算是题的话 ) , 基本就是个大学生的期末大作业

第一个项目我已经提过了 如何创建restful api

个人觉得第一个项目命题的面试官明显是想考察interviewee如何进行技术选型, 如何组织代码结构, 如何处理异常, 如何进行集成和发布

第二个项目是一个竞拍系统, 题目大概是这样的

我需要一个竞拍系统, 参与的角色有一些owner,bidder什么的, 被拍卖物品只有一种是钻石, owner需要知道当前的最高价, bidder需要知道自己的报价在第几名, 然后这些数据要实时刷新

这个命题是中午11点给出的,下午1点就要面试,打电话的时候Brian在北京, 他讲完需求以后就开始问我

你看我这样设计类行不行 ( 然后他开始巴拉巴拉的讲了一堆class Owner, class Bidder 什么的 )

故事就讲到这里, 对这个故事我的感触是, 其实很多人在一个新项目开始的时候, 往往切入点是错误的( 我认为是错误的 ), 例如 一开始就选择什么SSH 反正现成的框架一把一把的随手抓

然后马上就会陷入到无穷无尽的接口和数据定义的泥坑里不能自拔

然后我只能打断了Brian, 提了3个问题

1. 我们手中有多少可以被抽象的实物 , 也就是真实存在这个世界上的东西 ? 

Brian: owner, bidder , 钻石, 拍卖会 …

2. 那么现在是否能全部先按照范式整理成数据库表

Brian: owner 表的数据有 用户名

              bidder 表有姓名

             钻石 表 有钻石编号

             拍卖会表 有开始时间, 参与的bidder, 发起的owner, 当前最高出价等

             ……

3. 那么现在你能把表之间的关系整理一下么?

Brian:   一个owner 可以发起多个拍卖会 ( 我: 也就是一对多的关系, 拍卖会表需要一个owner的列)

              一个拍卖会有多个bidder , 一个bidder可以参与多个拍卖会 ( 我: 多对多, 需要一个中间表 )

             …….

这3个问题都整理完成后, 我们得到了一个最基础的数据库模型, 剩下的事其实很简单, 只需要根据需求把所有的sql 先写一遍, 剩下的java 类就已经呼之欲出了

当拿到一个新需求之后,我的经验是, 既然上学的时候我们都学过  ” 程序 = 算法+ 数据结构 “, 其实我特别不喜欢这句话, 可能是这句话对于我们这些做应用层的人不太实用,

我更喜欢 “程序是一切现实事物的抽象表现” , 你要写的代码永远不可能脱离现实世界的实际物体而存在, 所以当新需求发布之后, 整理需求的第一个步骤就是整理 “实体”

例如一个票据管理的工作流系统, 首先应该明确的是

  • 这个系统中一共有多少参与者,角色都是什么( 例如你可能会关心他们的 员工ID, 角色, 权限等)  ==> 对参与的人进行抽象
  • 这个系统中有多少种需要处理的票据, 都有哪些内容 ( 例如发票, 入库单据, 出库单据, 出纳提供的财务票据之类, 当然这些可能会被抽象成一个表 ) ==> 对需要处理的数据抽象
  • 上面提到的角色和票据都有哪些状态, 可能会进行的动作 ==> 对业务抽象

有了上面这些基础数据之后应该优先按照范式来设计数据库, 这样你会对整个模块要处理的数据有一个完整认识, 可以避免自己陷入复杂的对象关系中

但仅仅到这里还是远远不够的, 尽管类似上面的 拍卖系统 只是一个小样例, 但是命题人的高明在于仍然能考察出被面试者的基本功底, 因为这是一个对性能有要求的实时系统

这里在设计完数据库模型之后需要一个逆范式的动作, 逆范式的基本思路在于, 冗余需要冗余的数据, 也就是空间换性能

所有的性能调优都是一门玄学, 但越是玄学你越要抓住一些 “摸得着看得见” 的东西 ==> 你能预见到的SQL 语句, 尤其是能被预见到要频繁调用的SQL语句(或复杂运算)

例如 bidder需要查询自己的出价当前是第几位

          owner需要知道当前的最高报价

这两个动作在这个系统中可能是最容易被频繁调用的, 需要有针对性的优化

对于第一个, 出价排名问题, 非冗余的常规做法是 select count 然后找出比自己出价高的人, 这样得到一个比自己出价高的人的人数, 然后 +1 就是自己的排名

这样做的问题在于假设系统用户人很多, 每个人都要频繁的实时获取自己的排名,而且又分布在不同的拍卖会中, 好了, 现在你需要不停的进行count运算, 可能还需要一堆where条件语句, 势必大大影响数据库的响应, 尤其是在索引设计有问题的情况下, 没完没了的全表扫描几乎在所难免 ( 索引的问题我们一会再说 )

如果只有数据库的话, 你可能需要一个类似临时表一样的东西当做缓存来保存每个人的排名, 这样你就不用使用count之类的聚合函数, 也可以使用memcached或者redis之类的作为缓存层,直接在缓存层上操作排名, 当然了竞标的原始数据还是要存储到数据库中的, memcached没法进行数据持久化而redis的持久化似乎性能开销更大, 如果这样还觉得不够简单粗暴,还可以直接将值缓存在java虚拟机内存中, 因为报价总是只加不减, 每次有新报价出现后, 把所有之前的报价排名+1就好了, 总之你要好多种选择把这些数据 冗余 到其他地方

上面的说法对于第二个问题也试用

PS:

为什么我不着急设计索引, 索引? 说得极端一点, 系统Go live之后重新设计索引都没有问题, 第一它不影响数据库结构, 第二它的”重要性”简直太低了, 修改成本几乎为零

关于拍卖系统的题目最后隐含着一个对 数据如何显示到浏览器 上的问题

构建基于B/S的实时系统以前确实没有什么好办法,除非你使用浏览器插件之类的, 过去只能使用Ajax不停的请求, 因为http协议本身就是基于request和response的

针对这个问题 Jquery甚至还提供了一个像是 “伪链接” 一样的东西, 底层还是不断的通过loop去获取数据

如果你没注意过现在tomcat已经全部都是http1.1标准了, 这道题你可能要悲剧, 至少如果你给出 ” 不断的循环去获取数据 ” 这样的答案肯定不是面试时的加分项, 正确的答案是

长链接 和 websocket … 

最后我还是要表达一下自己对这两个题目的钦佩之情, 第一个题目考察了技术选型, 代码结构, source管理器和发布流程的设计(甚至还要了dockerfile,提供一键部署功能)

第二个题目考察了完整的项目设计, 而且如果面试官愿意的话, 面试时基于第二个题目还会有一堆例如 如何提供并发性能,如何扩展,如何做分布式之类的问题等待着候选人

非常不错的题目, 不枉费我上周六周日给Brian抱了两天佛脚 😛

“缓存”问题

cache everything you can

缓存 实际上仍然是一种通过 消耗硬件资源和存储空间 换取 运行速度的手段

通过将数据转移到高速载体或者预先完成初始化动作来保证程序运行期间的速度

  • 数据库连接池
  • 线程池
  • spring 对象容器

等都是缓存技术的一种实现

我们来看几种典型应用

1. 使用静态块或静态变量

想象一下你的”商城”应用中有一个<select>选项, 用于让用户选择商品类别, 这些类别可能几年都不会更新一次, 它们被存储在文件或者数据库中, 当用户打开页面时每次都从文件系统或数据加载一遍这些数据显然不合适

最简单的做法是通过 静态变量 , 静态变量的生命周期与class在类加载器中的生命周期是一样的,所以很难被回收掉, 这个对象会一直存储在java 堆中,也就构成了一个简单的构建在堆内存上的缓存

private static List<String> types = getTypes();

2. 使用Spring对象池中的单例对象

spring容器启动之后会一直持有一个保存单例对象的hashmap(猜测是静态的,跟上面原理一样) , 每个单例对象在容器容器启动时初始化, 也就是说每个单例对象自有的类成员变量不会被销毁, 通过这些成员变量很容易做出一种懒汉式的缓存机制, 用户第一次调用时,或者启动应用时通过其他手段调用一次(比如构造函数,当对象被new出来时候就调用一下), 之后就可以一直返回这个已经处理过的对象

例如

public List<String> getTypes(){

    if(types==null){

        return initTypes();

    }else{

        return types

    }

}

Spring的对象容器本身就是一个用于缓存对象的缓存, 这也是”我们为什么要使用spring的一个重要原因之一”

3. 使用memcache

memcache可以将文本或者可序列化对象直接存储在其他server的内存中, 这种模型甚至可以超出一般缓存的定义转而成为共享数据的方式 ( 比如tomcat集群中它可以解决session共享问题 )

4. get请求

不要诧异, get请求实际上也使用了缓存, 对于一些 通知, 文本内容的信息, 完全可以避免使用post方法转而使用get, get方法会充分利用浏览器本地缓存, 减少对服务器的依赖

5. 只读缓存

典型的只读缓存实际上是爬虫产生的搜索index, 这些结果集并不需要实时更新, 你可能需要准备一个batch(springbatch是一个不错的选择,或者直接使用cron调用脚本也可以处理简单的更新), 如果产生的缓存数据量特别大的话你甚至可以考虑使用Hadoop的HDFS文件系统或者使用HBase或者其他NoSql数据库

“重要性”问题

当你尝试构建或者更新一个系统时, 你可能经常需要考虑哪些工作是最重要的

根据以往的经验, 框架选型, 数据库设计, 开发环境的搭建 都会让人觉得很重要, 实际不然

这里 “重要性” 的概念是指 ” 修改成本最大, 维护成本最高, 一旦完成就几乎无法修改 ” 的部分

一个功能,一个模块或者是一个定义好的API, 如果修改它的成本越高, 它的重要性就越大

上面提到的内容中 框架选型和数据库设计 几乎是最重要的 , 改动数据库表结构意味着之前的大量DAO层工作都要重新, 如果你尝试将一个Struts项目更换成springmvc, 那么意味着所有的controller代码都要推倒重来, 在开始设计和构建系统之前,一定要明确 什么才是你系统中 “最重要” 的部分

一般来说(个人意见)

重要的部分包括

  • framework的选型
  • 数据库设计
  • 使用的数据库类型
  • 代码的组织结构(是按照层划分还是按照模块划分)
  • BO和DAO层
  • 核心业务接口

相对重要的部分包括

  • 使用的服务器类型
  • 构建工具的选择(例如Jenkins)
  • 测试工具的选择
  • java bean 的数据模型 (基本上和数据库对应)
  • release流程

不太重要的部分

  • source管理器的选择(SVN或者Git等)
  • Jsp页面中不可复用的部分
  • controller层的代码

多花一点时间在重要性更大的部分会让你后续的工作更加smooth, 盲目开工只能后患无穷

更近一步”重要性”这个概念其实还影响我们的原始设计

假设我们有3个表

  • Company 存储公司信息
  • Department 存储公司下属的各个部门
  • Employee 存储各个部门下的员工

这3个表一次构成一对多的关系

我们假设有一个重要需求是通过 员工 来获取 这个员工所在的公司

在数据库设计中(下图) , Department 表包含一个company_id 列, 构成1对多, Employee 表包含一个department_id列, 构成1对多

如果要通过员工获得所属公司的信息 sql 应该是

select company_info from company 

  join department on company.id = department.company_id

    join employee on department.id = employee.department_id

where employee.id = “1024”

这样你需要join两个表, employee只有通过department表才能知道自己所在的公司,由于实际上员工在公司内部并不会频繁更换部门或者公司,那么这3个表的关系就应该更倾向于

“面向读取” 优化

将数据库结构改成

这样你只需要一次join就能获得想要的信息

select company_info from company

  join employee on company.id = employee.company_id

这样的改动的根本原因在于, 数据库表关系实际上是”重要性”非常高的部分, 一旦一个数据库关系确定, 你就不能频繁改动它, 也就没有必要”为将来的修改进行优化”

之所以将department表和company表相关联基于以下两点考虑

  1. 数据库表结构不会频繁变化
  2. 表中的数据不会频繁被更新

    (如果频繁更新的话员工更换公司时需要改动company_id和department_id, 不关联的情况只需要改动department_id,不过因为可以在一个sql语句中更新,性能开销几乎可以忽略,这里只是举个例子)

以上是对于”重要性”较高的数据库设计的样例, 那么对于”重要性”较低的java 对象之间的关系要如何设计呢

首先java对象之间相互调用的性能损耗几乎可以忽略不计, 而java代码一般往往需要经常改动, 所以反而要避免这种设计, 对于”重要性” 较低的部分, 应该围绕”为了修改优化”展开

试想如果java对象之间完全照搬数据库设计, 在Employee类中加入 getCompany() 方法 和 getDepartment() 方法, 一旦 Company 和 Department 两个类中有任何改动, 可能都会对Employee类造成影响, 如果Employee 方法只包含getDepartment() 方法, 你可以使用employee.getDepartment().getCompany() 来获得company信息, 当且仅当Department类改动时才有可能影响到Employee

目的不同,设计也就不同, 在设计初期区分”重要性”可以更有效的帮助完成更好的设计, 降低项目风险, 节约开发成本