分类: 未分类

  • SpringBoot + Druid DataSource 实现监控 MySQL 性能

    快乐分享,Java干货及时送达👇

    来源:blog.csdn.net/lvoelife/article/details/128092586

    1. 基本概念

    我们都使用过连接池,比如C3P0,DBCP,hikari, Druid,虽然HikariCP的速度稍快,但Druid能够提供强大的监控和扩展功能,也是阿里巴巴的开源项目。

    Druid是阿里巴巴开发的号称为监控而生的数据库连接池,在功能、性能、扩展性方面,都超过其他数据库连接池,包括DBCP、C3P0、BoneCP、Proxool、JBoss DataSource等等,秒杀一切。

    Druid可以很好的监控DB池连接和SQL的执行情况,天生就是针对监控而生的DB连接池。

    Spring Boot默认数据源HikariDataSourceJdbcTemplate中已经介绍Spring Boot 2.x默认使用Hikari数据源,可以说Hikari与Driud都是当前Java Web上最优秀的数据源。

    而Druid已经在阿里巴巴部署了超过600个应用,经过好几年生产环境大规模部署的严苛考验!

    • stat: Druid内置提供一个StatFilter,用于统计监控信息。
    • wall: Druid防御SQL注入攻击的WallFilter就是通过Druid的SQL Parser分析。Druid提供的SQL Parser可以在JDBC层拦截SQL做相应处理,比如说分库分表、审计等。
    • log4j2: 这个就是 日志记录的功能,可以把sql语句打印到log4j2供排查问题。

    2. 相关配置

    2.1 添加依赖


        1.8
        1.2.11



        com.alibaba
        druid-spring-boot-starter
        ${alibabaDruidStarter.version}

    2.2 配置属性

    • 配置Druid数据源(连接池): 如同c3p0、dbcp数据源可以设置数据源连接初始化大小、最大连接数、等待时间、最小连接数 等一样,Druid数据源同理可以进行设置。
    • 配置Druid web监控filter(WebStatFilter): 这个过滤器的作用就是统计web应用请求中所有的数据库信息,比如 发出的sql语句,sql执行的时间、请求次数、请求的url地址、以及seesion监控、数据库表的访问次数等等。
    • 配置Druid后台管理Servlet(StatViewServlet): Druid数据源具有监控的功能,并提供了一个web界面方便用户查看,类似安装 路由器 时,人家也提供了一个默认的web页面;需要设置Druid的后台管理页面的属性,比如 登录账号、密码等。

    【注意】:Druid Spring Boot Starter配置属性的名称完全遵照Druid,可以通过Spring Boot配置文件来配置Druid数据库连接池和监控,如果没有配置则使用默认值,如下在application.yml配置相关属性:

    # spring 配置
    spring:
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        password: 123456
        username: root
        url: jdbc:mysql://localhost:3306/superjson?useUnicode=true&characterEncoding=utf8&useSSL=false
        # 连接池配置
        druid:
          # 初始化大小,最小,最大
          initial-size: 5
          min-idle: 5
          max-active: 20
          # 配置获取连接等待超时的时间
          max-wait: 60000
          # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位毫秒
          time-between-eviction-runs-millis: 60000
          # 配置一个连接在池中最小生存时间
          min-evictable-idle-time-millis: 300000
          validation-query: SELECT 1 FROM user
          test-while-idle: true
          test-on-borrow: false
          test-on-return: false
          # 打开 PSCache,并且指定每个连接上 PSCache 的大小
          pool-prepared-statements: true
          max-pool-prepared-statement-per-connection-size: 20
          # 配置监控统计拦截的 Filter,去掉后监控界面 SQL 无法统计,wall 用于防火墙
          filters: stat,wall,slf4j
          # 通过 connection-properties 属性打开 mergeSql 功能;慢 SQL 记录
          connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
          # 配置 DruidStatFilter
          web-stat-filter:
            enabled: true
            url-pattern: /*
            exclusions: .js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*
          # 配置 DruidStatViewServlet
          stat-view-servlet:
            url-pattern: /druid/*
            # IP 白名单,没有配置或者为空,则允许所有访问
            allow: 127.0.0.1
            # IP 黑名单,若白名单也存在,则优先使用
            deny: 192.168.31.253
            # 禁用 HTML 中 Reset All 按钮
            reset-enable: false
            # 登录用户名/密码
            login-username: root
            login-password: 123456
            # 需要设置enabled=true,否则会报出There was an unexpected error (type=Not Found, status=404).错误,或者将druid-spring-boot-starter的版本降低到1.1.10及以下
            # 是否启用StatViewServlet(监控页面)默认值为false(考虑到安全问题默认并未启动,如需启用建议设置密码或白名单以保障安全)
            enabled: true

    上述配置文件的参数可以在com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatPropertiesorg.springframework.boot.autoconfigure.jdbc.DataSourcePropertie中找到。

    2.3 配置Filter

    可以通过spring.datasource.druid.filters=stat,wall,log4j ...的方式来启用相应的内置Filter,不过这些Filter都是默认配置。如果默认配置不能满足需求,可以放弃这种方式,通过配置文件来配置Filter,如下所示:

    # 配置StatFilter 
    spring.datasource.druid.filter.stat.enabled=true
    spring.datasource.druid.filter.stat.db-type=h2
    spring.datasource.druid.filter.stat.log-slow-sql=true
    spring.datasource.druid.filter.stat.slow-sql-millis=2000

    # 配置WallFilter 
    spring.datasource.druid.filter.wall.enabled=true
    spring.datasource.druid.filter.wall.db-type=h2
    spring.datasource.druid.filter.wall.config.delete-allow=false
    spring.datasource.druid.filter.wall.config.drop-table-allow=false

    目前为以下Filter提供了配置支持,根据(spring.datasource.druid.filter.*)进行配置。

    • StatFilter
    • WallFilter
    • ConfigFilter
    • EncodingConvertFilter
    • Slf4jLogFilter
    • Log4jFilter
    • Log4j2Filter
    • CommonsLogFilter

    不想使用内置的Filters,要想使自定义Filter配置生效需要将对应Filter的enabled设置为true,Druid Spring Boot Starter默认禁用StatFilter,可以将其enabled设置为true来启用它。

    3 监控页面

    1. 启动项目后,访问http://localhost:8081/druid/login.html来到登录页面,输入用户名密码登录,如下所示:

    图片
    1. 数据源页面 是当前DataSource配置的基本信息,上述配置的Filter可以在里面找到,如果没有配置 Filter(一些信息会无法统计,例如SQL监控会无法获取JDBC相关的SQL执行信息)

    图片
    1. SQL监控页面,统计了所有SQL语句的执行情况

    图片
    1. URL监控页面,统计了所有Controller接口的访问以及执行情况

    图片
    1. Spring监控页面,利用aop对指定接口的执行时间,jdbc数进行记录

    图片
    1. SQL防火墙页面

    druid提供了黑白名单的访问,可以清楚的看到sql防护情况。

    1. Session监控页面

    可以看到当前的session状况,创建时间、最后活跃时间、请求次数、请求时间等详细参数。

    1. JSONAPI页面

    通过api的形式访问Druid的监控接口,api接口返回Json形式数据。

    4. sql监控

    配置Druid web监控filter(WebStatFilter)这个过滤器,作用就是统计web应用请求中所有的数据库信息,比如 发出的sql语句,sql执行的时间、请求次数、请求的url地址、以及seesion监控、数据库表的访问次数,如下配置:

    spring:
      datasource:
        druid:
          ########## 配置WebStatFilter,用于采集web关联监控的数据 ##########
          web-stat-filter:
            enabled: true                   # 启动 StatFilter
            url-pattern: /*                 # 过滤所有url
            exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*" # 排除一些不必要的url
            session-stat-enable: true       # 开启session统计功能
            session-stat-max-count: 1000    # session的最大个数,默认100

    5. 慢sql记录

    有时候,系统中有些SQL执行很慢,我们希望使用日志记录下来,可以开启Druid的慢SQL记录功能,如下配置:

    spring:
      datasource:
        druid:
          filter:
            stat:
              enabled: true         # 开启DruidDataSource状态监控
              db-type: mysql        # 数据库的类型
              log-slow-sql: true    # 开启慢SQL记录功能
              slow-sql-millis: 2000 # 默认3000毫秒,这里超过2s,就是慢,记录到日志

    启动后,如果遇到执行慢的SQL,便会输出到日志中

    6. spring 监控

    访问之后spring监控默认是没有数据的,但需要导入SprngBoot的AOP的Starter,如下所示:



        org.springframework.boot
        spring-boot-starter-aop

    同时需要在application.yml按如下配置:

    Spring监控AOP切入点,如com.springboot.template.dao.*,配置多个英文逗号分隔

    spring.datasource.druid.aop-patterns="com.springboot.template.dao.*"

    7. 去广告(Ad)

    访问监控页面的时候,你可能会在页面底部(footer)看到阿里巴巴的广告,如下所示:

    图片

    原因:引入的druid的jar包中的common.js(里面有一段js代码是给页面的footer追加广告的)

    如果想去掉,有两种方式:

    1. 直接手动注释这段代码

    如果是使用Maven,直接到本地仓库中,查找这个jar包,注释如下代码:

    // this.buildFooter();

    common.js的位置:

    com/alibaba/druid/1.1.23/druid-1.1.23.jar!/support/http/resources/js/common.js

    2. 使用过滤器过滤

    注册一个过滤器,过滤common.js的请求,使用正则表达式替换相关的广告内容,如下代码所示:

    @Configuration
    @ConditionalOnWebApplication
    @AutoConfigureAfter(DruidDataSourceAutoConfigure.class)
    @ConditionalOnProperty(name = "spring.datasource.druid.stat-view-servlet.enabled",
    havingValue = "true", matchIfMissing = true)
    public class RemoveDruidAdConfig {

        /**
        * 方法名: removeDruidAdFilterRegistrationBean
        * 方法描述 除去页面底部的广告
        * @param properties com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties
        * @return org.springframework.boot.web.servlet.FilterRegistrationBean
        */
        @Bean
        public FilterRegistrationBean removeDruidAdFilterRegistrationBean(DruidStatProperties properties) {

            // 获取web监控页面的参数
            DruidStatProperties.StatViewServlet config = properties.getStatViewServlet();
            // 提取common.js的配置路径
            String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*";
            String commonJsPattern = pattern.replaceAll("\*""js/common.js");

            final String filePath = "support/http/resources/js/common.js";

            //创建filter进行过滤
            Filter filter = new Filter() {
                @Override
                public void init(FilterConfig filterConfig) throws ServletException {}

                @Override
                public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
                    chain.doFilter(request, response);
                    // 重置缓冲区,响应头不会被重置
                    response.resetBuffer();
                    // 获取common.js
                    String text = Utils.readFromResource(filePath);
                    // 正则替换banner, 除去底部的广告信息
                    text = text.replaceAll("
    "
    "");
                    text = text.replaceAll("powered.*?shrek.wang""");
                    response.getWriter().write(text);
                }

                @Override
                public void destroy() {}
            };

            FilterRegistrationBean registrationBean = new FilterRegistrationBean();
            registrationBean.setFilter(filter);
            registrationBean.addUrlPatterns(commonJsPattern);
            return registrationBean;
        }
    }

    两种方式都可以,建议使用的是第一种,从根源解决。

    8. 获取 Druid 的监控数据

    Druid的监控数据可以在开启StatFilter后,通过DruidStatManagerFacade进行获取;

    DruidStatManagerFacade#getDataSourceStatDataList该方法可以获取所有数据源的监控数据,除此之外DruidStatManagerFacade还提供了一些其他方法,可以按需选择使用。

    @RestController
    @RequestMapping(value = "/druid")
    public class DruidStatController {

        @GetMapping("/stat")
        public Object druidStat(){
            // 获取数据源的监控数据
            return DruidStatManagerFacade.getInstance().getDataSourceStatDataList();
        }
    }
    
    

  • ES+Redis+MySQL,这个高可用架构设计太顶了!

    快乐分享,Java干货及时送达👇

    来源:dbaplus

    • 一、背景
    • 二、ES高可用方案
    • 三、会员Redis缓存方案
    • 四、高可用会员主库方案
    • 五、异常会员关系治理
    • 六、展望:更精细化的流控和降级策略

    一、背景


    会员系统是一种基础系统,跟公司所有业务线的下单主流程密切相关。如果会员系统出故障,会导致用户无法下单,影响范围是全公司所有业务线。所以,会员系统必须保证高性能、高可用,提供稳定、高效的基础服务。


    随着同程和艺龙两家公司的合并,越来越多的系统需要打通同程APP、艺龙APP、同程微信小程序、艺龙微信小程序等多平台会员体系。例如微信小程序的交叉营销,用户买了一张火车票,此时想给他发酒店红包,这就需要查询该用户的统一会员关系。因为火车票用的是同程会员体系,酒店用的是艺龙会员体系,只有查到对应的艺龙会员卡号后,才能将红包挂载到该会员账号。除了上述讲的交叉营销,还有许多场景需要查询统一会员关系,例如订单中心、会员等级、里程、红包、常旅、实名,以及各类营销活动等等。所以,会员系统的请求量越来越大,并发量越来越高,今年五一小长假的秒并发tps甚至超过2万多。在如此大流量的冲击下,会员系统是如何做到高性能和高可用的呢?这就是本文着重要讲述的内容。


    二、ES高可用方案


    1. ES双中心主备集群架构


    同程和艺龙两家公司融合后,全平台所有体系的会员总量是十多亿。在这么大的数据体量下,业务线的查询维度也比较复杂。有的业务线基于手机号,有的基于微信unionid,也有的基于艺龙卡号等查询会员信息。这么大的数据量,又有这么多的查询维度,基于此,我们选择ES用来存储统一会员关系。ES集群在整个会员系统架构中非常重要,那么如何保证ES的高可用呢?


    首先我们知道,ES集群本身就是保证高可用的,如下图所示:


    当ES集群有一个节点宕机了,会将其他节点对应的Replica Shard升级为Primary Shard,继续提供服务。但即使是这样,还远远不够。例如ES集群都部署在机房A,现在机房A突然断电了,怎么办?例如服务器硬件故障,ES集群大部分机器宕机了,怎么办?或者突然有个非常热门的抢购秒杀活动,带来了一波非常大的流量,直接把ES集群打死了,怎么办?面对这些情况,让运维兄弟冲到机房去解决?这个非常不现实,因为会员系统直接影响全公司所有业务线的下单主流程,故障恢复的时间必须非常短,如果需要运维兄弟人工介入,那这个时间就太长了,是绝对不能容忍的。那ES的高可用如何做呢?我们的方案是ES双中心主备集群架构。


    我们有两个机房,分别是机房A和机房B。我们把ES主集群部署在机房A,把ES备集群部署在机房B。会员系统的读写都在ES主集群,通过MQ将数据同步到ES备集群。此时,如果ES主集群崩了,通过统一配置,将会员系统的读写切到机房B的ES备集群上,这样即使ES主集群挂了,也能在很短的时间内实现故障转移,确保会员系统的稳定运行。最后,等ES主集群故障恢复后,打开开关,将故障期间的数据同步到ES主集群,等数据同步一致后,再将会员系统的读写切到ES主集群。


    2. ES流量隔离三集群架构


    双中心ES主备集群做到这一步,感觉应该没啥大问题了,但去年的一次恐怖流量冲击让我们改变了想法。那是一个节假日,某个业务上线了一个营销活动,在用户的一次请求中,循环10多次调用了会员系统,导致会员系统的tps暴涨,差点把ES集群打爆。这件事让我们后怕不已,它让我们意识到,一定要对调用方进行优先级分类,实施更精细的隔离、熔断、降级、限流策略。首先,我们梳理了所有调用方,分出两大类请求类型。第一类是跟用户的下单主流程密切相关的请求,这类请求非常重要,应该高优先级保障。第二类是营销活动相关的,这类请求有个特点,他们的请求量很大,tps很高,但不影响下单主流程。基于此,我们又构建了一个ES集群,专门用来应对高tps的营销秒杀类请求,这样就跟ES主集群隔离开来,不会因为某个营销活动的流量冲击而影响用户的下单主流程。如下图所示:



    3. ES集群深度优化提升


    讲完了ES的双中心主备集群高可用架构,接下来我们深入讲解一下ES主集群的优化工作。有一段时间,我们特别痛苦,就是每到饭点,ES集群就开始报警,搞得每次吃饭都心慌慌的,生怕ES集群一个扛不住,就全公司炸锅了。那为什么一到饭点就报警呢?因为流量比较大, 导致ES线程数飙高,cpu直往上窜,查询耗时增加,并传导给所有调用方,导致更大范围的延时。那么如何解决这个问题呢?通过深入ES集群,我们发现了以下几个问题: 


    • ES负载不合理,热点问题严重。ES主集群一共有几十个节点,有的节点上部署的shard数偏多,有的节点部署的shard数很少,导致某些服务器的负载很高,每到流量高峰期,就经常预警。


    • ES线程池的大小设置得太高,导致cpu飙高。我们知道,设置ES的threadpool,一般将线程数设置为服务器的cpu核数,即使ES的查询压力很大,需要增加线程数,那最好也不要超过“cpu core * 3 / 2 + 1”。如果设置的线程数过多,会导致cpu在多个线程上下文之间频繁来回切换,浪费大量cpu资源。


    • shard分配的内存太大,100g,导致查询变慢。我们知道,ES的索引要合理分配shard数,要控制一个shard的内存大小在50g以内。如果一个shard分配的内存过大,会导致查询变慢,耗时增加,严重拖累性能。


    • string类型的字段设置了双字段,既是text,又是keyword,导致存储容量增大了一倍。会员信息的查询不需要关联度打分,直接根据keyword查询就行,所以完全可以将text字段去掉,这样就能节省很大一部分存储空间,提升性能。


    • ES查询,使用filter,不使用query。因为query会对搜索结果进行相关度算分,比较耗cpu,而会员信息的查询是不需要算分的,这部分的性能损耗完全可以避免。


    • 节约ES算力,将ES的搜索结果排序放在会员系统的jvm内存中进行。


    • 增加routing key。我们知道,一次ES查询,会将请求分发给所有shard,等所有shard返回结果后再聚合数据,最后将结果返回给调用方。如果我们事先已经知道数据分布在哪些shard上,那么就可以减少大量不必要的请求,提升查询性能。


    经过以上优化,成果非常显著,ES集群的cpu大幅下降,查询性能大幅提升。ES集群的cpu使用率: 



    会员系统的接口耗时:


    三、会员Redis缓存方案


    一直以来,会员系统是不做缓存的,原因主要有两个:第一个,前面讲的ES集群性能很好,秒并发3万多,99线耗时5毫秒左右,已经足够应付各种棘手的场景。第二个,有的业务对会员的绑定关系要求实时一致,而会员是一个发展了10多年的老系统,是一个由好多接口、好多系统组成的分布式系统。所以,只要有一个接口没有考虑到位,没有及时去更新缓存,就会导致脏数据,进而引发一系列的问题,例如:用户在APP上看不到微信订单、APP和微信的会员等级、里程等没合并、微信和APP无法交叉营销等等。那后来为什么又要做缓存呢?是因为今年机票的盲盒活动,它带来的瞬时并发太高了。虽然会员系统安然无恙,但还是有点心有余悸,稳妥起见,最终还是决定实施缓存方案。


    1. ES近一秒延时导致的Redis缓存数据不一致问题的解决方案


    在做会员缓存方案的过程中,遇到一个ES引发的问题,该问题会导致缓存数据的不一致。我们知道,ES操作数据是近实时的,往ES新增一个Document,此时立即去查,是查不到的,需要等待1秒后才能查询到。如下图所示:



    ES的近实时机制为什么会导致redis缓存数据不一致呢?具体来讲,假设一个用户注销了自己的APP账号,此时需要更新ES,删除APP账号和微信账号的绑定关系。而ES的数据更新是近实时的,也就是说,1秒后你才能查询到更新后的数据。而就在这1秒内,有个请求来查询该用户的会员绑定关系,它先到redis缓存中查,发现没有,然后到ES查,查到了,但查到的是更新前的旧数据。最后,该请求把查询到的旧数据更新到redis缓存并返回。就这样,1秒后,ES中该用户的会员数据更新了,但redis缓存的数据还是旧数据,导致了redis缓存跟ES的数据不一致。如下图所示:



    面对该问题,如何解决呢?我们的思路是,在更新ES数据时,加一个2秒的redis分布式并发锁,为了保证缓存数据的一致性,接着再删除redis中该会员的缓存数据。如果此时有请求来查询数据,先获取分布式锁,发现该会员ID已经上锁了,说明ES刚刚更新的数据尚未生效,那么此时查询完数据后就不更新redis缓存了,直接返回,这样就避免了缓存数据的不一致问题。如下图所示:



    上述方案,乍一看似乎没什么问题了,但仔细分析,还是有可能导致缓存数据的不一致。例如,在更新请求加分布式锁之前,恰好有一个查询请求获取分布式锁,而此时是没有锁的,所以它可以继续更新缓存。但就在他更新缓存之前,线程block了,此时更新请求来了,加了分布式锁,并删除了缓存。当更新请求完成操作后,查询请求的线程活过来了,此时它再执行更新缓存,就把脏数据写到缓存中了。发现没有?主要的问题症结就在于“删除缓存”和“更新缓存”发生了并发冲突,只要将它们互斥,就能解决问题。如下图所示:



    实施了缓存方案后,经统计,缓存命中率90%+,极大缓解了ES的压力,会员系统整体性能得到了很大提升。


    2. Redis双中心多集群架构


    接下来,我们看一下如何保障Redis集群的高可用。如下图所示: 


    关于Redis集群的高可用,我们采用了双中心多集群的模式。在机房A和机房B各部署一套Redis集群。更新缓存数据时,双写,只有两个机房的redis集群都写成功了,才返回成功。查询缓存数据时,机房内就近查询,降低延时。这样,即使机房A整体故障,机房B还能提供完整的会员服务。


    四、高可用会员主库方案


    上述讲到,全平台会员的绑定关系数据存在ES,而会员的注册明细数据存在关系型数据库。最早,会员使用的数据库是SqlServer,直到有一天,DBA找到我们说,单台SqlServer数据库已经存储了十多亿的会员数据,服务器已达到物理极限,不能再扩展了。按照现在的增长趋势,过不了多久,整个SqlServer数据库就崩了。你想想,那是一种什么样的灾难场景:会员数据库崩了,会员系统就崩了;会员系统崩了,全公司所有业务线就崩了。想想就不寒而栗,酸爽无比,为此我们立刻开启了迁移DB的工作。


    1. MySql双中心Partition集群方案


    经过调研,我们选择了双中心分库分表的MySql集群方案,如下图所示:



    会员一共有十多亿的数据,我们把会员主库分了1000多个分片,平分到每个分片大概百万的量级,足够使用了。MySql集群采用1主3从的架构,主库放在机房A,从库放在机房B,两个机房之间通过专线同步数据,延迟在1毫秒内。会员系统通过DBRoute读写数据,写数据都路由到master节点所在的机房A,读数据都路由到本地机房,就近访问,减少网络延迟。这样,采用双中心的MySql集群架构,极大提高了可用性,即使机房A整体都崩了,还可以将机房B的Slave升级为Master,继续提供服务。


    双中心MySql集群搭建好后,我们进行了压测,测试下来,秒并发能达到2万多,平均耗时在10毫秒内,性能达标。


    2. 会员主库平滑迁移方案


    接下来的工作,就是把会员系统的底层存储从SqlServer切到MySql上,这是个风险极高的工作,主要有以下几个难点:


    • 会员系统是一刻都不能停机的,要在不停机的情况下完成SqlServer到MySql的切换,就像是在给高速行驶的汽车换轮子。


    • 会员系统是由很多个系统和接口组成的,毕竟发展了10多年,由于历史原因,遗留了大量老接口,逻辑错综复杂。这么多系统,必须一个不落的全部梳理清楚,DAL层代码必须重写,而且不能出任何问题,否则将是灾难性的。


    • 数据的迁移要做到无缝迁移,不仅是存量10多亿数据的迁移,实时产生的数据也要无缝同步到mysql。另外,除了要保障数据同步的实时性,还要保证数据的正确性,以及SqlServer和MySql数据的一致性。


    基于以上痛点,我们设计了“全量同步、增量同步、实时流量灰度切换”的技术方案。


    首先,为了保证数据的无缝切换,采用实时双写的方案。因为业务逻辑的复杂,以及SqlServer和MySql的技术差异性,在双写mysql的过程中,不一定会写成功,而一旦写失败,就会导致SqlServer和MySql的数据不一致,这是绝不允许的。所以,我们采取的策略是,在试运行期间,主写SqlServer,然后通过线程池异步写MySql,如果写失败了,重试三次,如果依然失败,则记日志,然后人工排查原因,解决后,继续双写,直到运行一段时间,没有双写失败的情况。通过上述策略,可以确保在绝大部分情况下,双写操作的正确性和稳定性,即使在试运行期间出现了SqlServer和MySql的数据不一致的情况,也可以基于SqlServer再次全量构建出MySql的数据,因为我们在设计双写策略时,会确保SqlServer一定能写成功,也就是说,SqlServer中的数据是全量最完整、最正确的。如下图所示:


     讲完了双写,接下来我们看一下“读数据”如何灰度。整体思路是,通过A/B平台逐步灰度流量,刚开始100%的流量读取SqlServer数据库,然后逐步切流量读取MySql数据库,先1%,如果没有问题,再逐步放流量,最终100%的流量都走MySql数据库。在逐步灰度流量的过程中,需要有验证机制,只有验证没问题了,才能进一步放大流量。那么这个验证机制如何实施呢?方案是,在一次查询请求里,通过异步线程,比较SqlServer和 MySql的查询结果是否一致,如果不一致,记日志,再人工检查不一致的原因,直到彻底解决不一致的问题后,再逐步灰度流量。如下图所示:



    所以,整体的实施流程如下:



    首先,在一个夜黑风高的深夜,流量最小的时候,完成SqlServer到MySql数据库的全量数据同步。接着,开启双写,此时,如果有用户注册,就会实时双写到两个数据库。那么,在全量同步和实时双写开启之间,两个数据库还相差这段时间的数据,所以需要再次增量同步,把数据补充完整,以防数据的不一致。剩下的时间,就是各种日志监控,看双写是否有问题,看数据比对是否一致等等。这段时间是耗时最长的,也是最容易发生问题的,如果有的问题比较严重,导致数据不一致了,就需要从头再来,再次基于SqlServer全量构建MySql数据库,然后重新灰度流量,直到最后,100%的流量全部灰度到MySql,此时就大功告成了,下线灰度逻辑,所有读写都切到MySql集群。


    3. MySql和ES主备集群方案


    做到这一步,感觉会员主库应该没问题了,可dal组件的一次严重故障改变了我们的想法。那次故障很恐怖,公司很多应用连接不上数据库了,创单量直线往下掉,这让我们意识到,即使数据库是好的,但dal组件异常,依然能让会员系统挂掉。所以,我们再次异构了会员主库的数据源,双写数据到ES,如下所示:



    如果dal组件故障或MySql数据库挂了,可以把读写切到ES,等MySql恢复了,再把数据同步到MySql,最后把读写再切回到MySql数据库。如下图所示:



    五、异常会员关系治理


    会员系统不仅仅要保证系统的稳定和高可用,数据的精准和正确也同样重要。举个例子,一个分布式并发故障,导致一名用户的APP账户绑定了别人的微信小程序账户,这将会带来非常恶劣的影响。首先,一旦这两个账号绑定了,那么这两个用户下的酒店、机票、火车票订单是互相可以看到的。你想想,别人能看到你订的酒店订单,你火不火,会不会投诉?除了能看到别人的订单,你还能操作订单。例如,一个用户在APP的订单中心,看到了别人订的机票订单,他觉得不是自己的订单,就把订单取消了。这将会带来非常严重的客诉,大家知道,机票退订费用是挺高的,这不仅影响了该用户的正常出行,还导致了比较大的经济损失,非常糟糕。


    针对这些异常会员账号,我们进行了详细的梳理,通过非常复杂烧脑的逻辑识别出这些账号,并对会员接口进行了深度优化治理,在代码逻辑层堵住了相关漏洞,完成了异常会员的治理工作。如下图所示:



    六、展望:更精细化的流控和降级策略


    任何一个系统,都不能保证百分之一百不出问题,所以我们要有面向失败的设计,那就是更精细化的流控和降级策略。


    1. 更精细化的流控策略


    热点控制。针对黑产刷单的场景,同一个会员id会有大量重复的请求,形成热点账号,当这些账号的访问超过设定阈值时,实施限流策略。


    基于调用账号的流控规则。这个策略主要是防止调用方的代码bug导致的大流量。例如,调用方在一次用户请求中,循环很多次来调用会员接口,导致会员系统流量暴增很多倍。所以,要针对每个调用账号设置流控规则,当超过阈值时,实施限流策略。


    全局流控规则。我们会员系统能抗下tps 3万多的秒并发请求量,如果此时,有个很恐怖的流量打过来,tps高达10万,与其让这波流量把会员数据库、es全部打死,还不如把超过会员系统承受范围之外的流量快速失败,至少tps 3万内的会员请求能正常响应,不会让整个会员系统全部崩溃。


    2. 更精细化的降级策略


    基于平均响应时间的降级。会员接口也有依赖其他接口,当调用其他接口的平均响应时间超过阈值,进入准降级状态。如果接下来 1s 内进入的请求,它们的平均响应时间都持续超过阈值,那么在接下的时间窗口内,自动地熔断。


    基于异常数和异常比例的降级。当会员接口依赖的其他接口发生异常,如果1分钟内的异常数超过阈值,或者每秒异常总数占通过量的比值超过阈值,进入降级状态,在接下的时间窗口之内,自动熔断。


    目前,我们最大的痛点是会员调用账号的治理。公司内,想要调用会员接口,必须申请一个调用账号,我们会记录该账号的使用场景,并设置流控、降级策略的规则。但在实际使用的过程中,申请了该账号的同事,可能异动到其他部门了,此时他可能也会调用会员系统,为了省事,他不会再次申请会员账号,而是直接沿用以前的账号过来调用,这导致我们无法判断一个会员账号的具体使用场景是什么,也就无法实施更精细的流控和降级策略。所以,接下来,我们将会对所有调用账号进行一个个的梳理,这是个非常庞大且繁琐的工作,但无路如何,硬着头皮也要做好。

  • SpringBoot 部署打包成 jar 和 war 有什么不同?

    快乐分享,Java干货及时送达👇

    首先给大家来讲一个我们遇到的一个奇怪的问题:

    1. 我的一个springboot项目,用mvn install打包成jar,换一台有jdk的机器就直接可以用java -jar 项目名.jar的方式运行,没任何问题,为什么这里不需要tomcat也可以运行了?

    2. 然后我打包成war放进tomcat运行,发现端口号变成tomcat默认的8080(我在server.port中设置端口8090)项目名称也必须加上了。

    也就是说我在原来的机器的IDEA中运行,项目接口地址为 ip:8090/listall,打包放进另一台机器的tomcat就变成了ip:8080/项目名/listall。这又是为什么呢?

    • 通过jar运行实际上是启动了内置的tomcat,所以用的是应用的配置文件中的端口

    • 直接部署到tomcat之后,内置的tomcat就不会启用,所以相关配置就以安装的tomcat为准,与应用的配置文件就没有关系了

    哎,现在学编程的基本都不会教历史了,也没人有兴趣去钻研。

    总体来说吧,很多年前,Sun 还在世的那个年代,在度过了早期用 C++写 Html 解析器的蛮荒时期后,有一批最早的脚本程序进入了 cgi 时代,此时的 Sun 决定进军这个领域,为了以示区别并显得自己高大上,于是研发了 servlet 标准,搞出了最早的 jsp。并给自己起了个高大上的称号 JavaEE ( Java 企业级应用标准,其实不就是一堆服务器以 http 提供服务吗,吹逼)。

    既然是企业级标准那自然得有自己的服务器标准。于是 Servlet 标准诞生,以此标准实现的服务器称为 Servle 容器服务器,Tomcat 就是其中代表,被 Sun 捐献给了 Apache 基金会,那个时候的 Web 服务器还是个高大上的概念,当时的 Java Web 程序的标准就是 War 包(其实就是个 Zip 包),这就是 War 包的由来。

    后来随着服务器领域的屡次进化,人们发现我们为什么要这么笨重的 Web 服务器,还要实现一大堆 Servlet 之外的管理功能,简化一下抽出核心概念 servlet 不是更好吗,最早这么干的似乎是 Jetty,出现了可以内嵌的 Servelet 服务器。

    去掉了一大堆非核心功能。后来 tomcat 也跟进了,再后来,本来很笨重的传统 JavaEE 服务器 Jboss 也搞了个 undertow 来凑热闹。正好这个时候微服务的概念兴起,“ use Jar,not War ”。要求淘汰传统 Servlet 服务器的呼声就起来了。

    jar包和war包的区别

    1、war是一个web模块,其中需要包括WEB-INF,是可以直接运行的WEB模块;jar一般只是包括一些class文件,在声明了Main_class之后是可以用java命令运行的。

    2、war包是做好一个web应用后,通常是网站,打成包部署到容器中;jar包通常是开发时要引用通用类,打成包便于存放管理。

    3、war是Sun提出的一种Web应用程序格式,也是许多文件的一个压缩包。这个包中的文件按一定目录结构来组织;classes目录下则包含编译好的Servlet类和Jsp或Servlet所依赖的其它类(如JavaBean)可以打包成jar放到WEB-INF下的lib目录下。

    JAR文件格式以流行的ZIP文件格式为基础。与ZIP文件不同的是,JAR 文件不仅用于压缩和发布,而且还用于部署和封装库、组件和插件程序,并可被像编译器和 JVM 这样的工具直接使用。

    【格式特点】:
    • 安全性 可以对 JAR 文件内容加上数字化签名。这样,能够识别签名的工具就可以有选择地为您授予软件安全特权,这是其他文件做不到的,它还可以检测代码是否被篡改过。

    • 减少下载时间 如果一个 applet 捆绑到一个 JAR 文件中,那么浏览器就可以在一个 HTTP 事务中下载这个 applet 的类文件和相关的资源,而不是对每一个文件打开一个新连接。

    • 压缩 JAR 格式允许您压缩文件以提高存储效率。

    • 传输平台扩展 Java 扩展框架(Java Extensions Framework)提供了向 Java 核心平台添加功能的方法,这些扩展是用 JAR 文件打包的(Java 3D 和 JavaMail 就是由 Sun 开发的扩展例子)。

    WAR文件就是一个Web应用程序,建立WAR文件,就是把整个Web应用程序(不包括Web应用程序层次结构的根目录)压缩起来,指定一个war扩展名。

    【建立的条件】:
    • 需要建立正确的Web应用程序的目录层次结构。

    • 建立WEB-INF子目录,并在该目录下建立classes与lib两个子目录。

    • 将Servlet类文件放到WEB-INFclasses目录下,将Web应用程序所使用Java类库文件(即JAR文件)放到WEB-INFlib目录下。

    • 将JSP页面或静态HTML页面放到上下文根路径下或其子目录下。

    • 建立META-INF目录,并在该目录下建立context.xml文件。

    下面给大家讲讲怎么将springboot项目打包成jar和war

    SpringBoot项目打包成jar很简单,也是SpringBoot的常用打包格式;本篇博客将SpringBoot打包成jar和war两种方式都记录下来;

    先介绍将SpringBoot打包成jar包的方式:(以下示例是在idea中演示)

    一、打包成jar

    1)先new 一个Spring Starter Project

    这里注意packaging默认为jar,不用修改.

    2)创建完成后项目的pom如下:


    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.0modelVersion>
     parent>
      groupId>org.springframework.bootgroupId>
      artifactId>spring-boot-starter-parentartifactId>
      version>2.1.4.RELEASEversion>
      relativePath/> 
     parent>
     groupId>com.examplegroupId>
     artifactId>demoartifactId>
     version>0.0.1-SNAPSHOTversion>
     name>demoname>
     description>Demo project for Spring Bootdescription>
     
     properties>
      java.version>1.8java.version>
     properties>
     
     dependencies>
      dependency>
       groupId>org.springframework.bootgroupId>
       artifactId>spring-boot-starterartifactId>
      dependency>
     
      dependency>
       groupId>org.springframework.bootgroupId>
       artifactId>spring-boot-starter-testartifactId>
       scope>testscope>
      dependency>
     dependencies>
     
     build>
      plugins>
       plugin>
        groupId>org.springframework.bootgroupId>
        artifactId>spring-boot-maven-pluginartifactId>
       plugin>
      plugins>
     build>
     
    project>

    3)打成jar包(通过maven命令的方式):

    在Terminal窗口,使用 mvn clean package 命令打包:

    然后在target目录下就能看到打包好的jar包了

    二、打包成war包形式

    1)可以在刚才创建的项目上做改动,首先打包成war需要一个ServletInitializer类,这个类的位置需要和启动类在同一个文件下

    如果一开始选择war包形式,会自动创建此类

    2)修改pom.xml

    修改pom.xml的war将原先的jar改为war;

    3)如果我们的SpringBoot是使用html作为前端页面开发没有问题,但是如果我们想用jsp开发,这个时候就需要配置一些依赖了:主要是排除SpringBoot的内置Tomcat,添加javax.servlet-apitomcat-servlet-api(SpringMVC还需要配置后缀);

    最后的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.0modelVersion>
     parent>
      groupId>org.springframework.bootgroupId>
      artifactId>spring-boot-starter-parentartifactId>
      version>2.1.4.RELEASEversion>
      relativePath/> 
     parent>
     groupId>com.examplegroupId>
     artifactId>demoartifactId>
     version>0.0.1-SNAPSHOTversion>
     packaging>warpackaging>
     name>demoname>
     description>Demo project for Spring Bootdescription>
     
     properties>
      java.version>1.8java.version>
     properties>
     
     dependencies>
            dependency>
                groupId>org.springframework.bootgroupId>
                artifactId>spring-boot-starter-thymeleafartifactId>
                exclusions>
                    exclusion>
                        groupId>org.springframework.bootgroupId>
                        artifactId>spring-boot-starter-tomcatartifactId>
                    exclusion>
                exclusions>
            dependency>
            dependency>
                groupId>javax.servletgroupId>
                artifactId>javax.servlet-apiartifactId>
                scope>providedscope>
            dependency>
     
            dependency>
                groupId>org.apache.tomcatgroupId>
                artifactId>tomcat-servlet-apiartifactId>
                version>8.0.36version>
                scope>providedscope>
            dependency>
     
            dependency>
                groupId>org.springframework.bootgroupId>
                artifactId>spring-boot-starter-webartifactId>
            dependency>
     
            dependency>
                groupId>org.springframework.bootgroupId>
                artifactId>spring-boot-starter-tomcatartifactId>
                scope>providedscope>
            dependency>
     
      dependency>
       groupId>org.springframework.bootgroupId>
       artifactId>spring-boot-starter-testartifactId>
       scope>testscope>
      dependency>
     dependencies>
     
     build>
      plugins>
       plugin>
        groupId>org.springframework.bootgroupId>
        artifactId>spring-boot-maven-pluginartifactId>
       plugin>
      plugins>
     build>
     
    project>

    因为SpringBoot默认推荐的是html,而不是jsp;经过上面的修改就可以使用jsp进行开发了;

    4)打包成war:使用mvn clean package

    如下:

    打包成功后,就可以将war包放在tomcat下的webapps下,然后运行tomcat,启动项目了;

    记录下来,以后用到的时候看 ^_^;

    当然了,在创建项目的时候直接选择package为war,直接就能打成war包了

    当选择war为打包方式创建项目时,ServletInitializer是默认直接创建的

    此时,pom文件如下


    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.0modelVersion>
     parent>
      groupId>org.springframework.bootgroupId>
      artifactId>spring-boot-starter-parentartifactId>
      version>2.1.4.RELEASEversion>
      relativePath/> 
     parent>
     groupId>com.examplegroupId>
     artifactId>demoartifactId>
     version>0.0.1-SNAPSHOTversion>
     packaging>warpackaging>
     name>demoname>
     description>Demo project for Spring Bootdescription>
     
     properties>
      java.version>1.8java.version>
     properties>
     
     dependencies>
      dependency>
       groupId>org.springframework.bootgroupId>
       artifactId>spring-boot-starter-webartifactId>
      dependency>
     
      dependency>
       groupId>org.springframework.bootgroupId>
       artifactId>spring-boot-starter-tomcatartifactId>
       scope>providedscope>
      dependency>
      dependency>
       groupId>org.springframework.bootgroupId>
       artifactId>spring-boot-starter-testartifactId>
       scope>testscope>
      dependency>
     dependencies>
     
     build>
      plugins>
       plugin>
        groupId>org.springframework.bootgroupId>
        artifactId>spring-boot-maven-pluginartifactId>
       plugin>
      plugins>
     build>
     
    project>

    直接mvn clean package就能打包成功

    感谢阅读,希望对你有所帮助 🙂 

    来源:https://blog.csdn.net/weixin_40910372/

  • SpringBoot 生产中 16 条最佳实践

    快乐分享,Java干货及时送达👇

    来源:e4developer.com/2018/08/06/

    • 1、使用自定义 BOM 来维护第三方依赖

    • 2、使用自动配置

    • 3、使用 Spring Initializr 来开始一个新的 Spring Boot 项目

    • 4、考虑为常见的组织问题创建自己的自动配置

    • 5、正确设计代码目录结构

    • 6、保持 @Controller 的简洁和专注

    • 7、围绕业务功能构建 @Service

    • 8、使数据库独立于核心业务逻辑之外

    • 9、保持业务逻辑不受 Spring Boot 代码的影响

    • 10、推荐使用构造函数注入

    • 11、熟悉并发模型

    • 12、加强配置管理的外部化

    • 13、提供全局异常处理

    • 14、使用日志框架

    • 15、测试你的代码

    • 16、使用测试切片让测试更容易,并且更专注

    • 总结


    Spring Boot 是最流行的用于开发微服务的 Java 框架。在本文中,我将与你分享自 2016 年以来我在专业开发中使用 Spring Boot 所采用的最佳实践。这些内容是基于我的个人经验和一些熟知的 Spring Boot 专家的文章。

    在本文中,我将重点介绍 Spring Boot 特有的实践(大多数时候,也适用于 Spring 项目)。以下依次列出了最佳实践,排名不分先后。

    1、使用自定义 BOM 来维护第三方依赖

    这条实践是我根据实际项目中的经历总结出的。

    Spring Boot 项目本身使用和集成了大量的开源项目,它帮助我们维护了这些第三方依赖。但是也有一部分在实际项目使用中并没有包括进来,这就需要我们在项目中自己维护版本。如果在一个大型的项目中,包括了很多未开发模块,那么维护起来就非常的繁琐。

    怎么办呢?事实上,Spring IO Platform 就是做的这个事情,它本身就是 Spring Boot 的子项目,同时维护了其他第三方开源库。我们可以借鉴 Spring IO Platform 来编写自己的基础项目 platform-bom,所有的业务模块项目应该以 BOM 的方式引入。这样在升级第三方依赖时,就只需要升级这一个依赖的版本而已。


       
           
               io.spring.platform
               platform-bom
               Cairo-SR3
               type>pomtype>
               import
           

       


    2、使用自动配置

    Spring Boot 的一个主要特性是使用自动配置。这是 Spring Boot 的一部分,它可以简化你的代码并使之工作。当在类路径上检测到特定的 jar 文件时,自动配置就会被激活。

    使用它的最简单方法是依赖 Spring Boot Starters。因此,如果你想与 Redis 进行集成,你可以首先包括:


       org.springframework.boot
       spring-boot-starter-data-redis

    如果你想与 MongoDB 进行集成,需要这样:


       org.springframework.boot
       spring-boot-starter-data-mongodb

    借助于这些 starters,这些繁琐的配置就可以很好地集成起来并协同工作,而且它们都是经过测试和验证的。这非常有助于避免可怕的 Jar 地狱。

    https://dzone.com/articles/what-is-jar-hell

    通过使用以下注解属性,可以从自动配置中排除某些配置类:

    @EnableAutoConfiguration(exclude = {ClassNotToAutoconfigure.class})

    但只有在绝对必要时才应该这样做。

    有关自动配置的官方文档可在此处找到:

    https://docs.spring.io/spring-boot/docs/current/reference/html/using-boot-auto-configuration.html。

    3、使用 Spring Initializr 来开始一个新的 Spring Boot 项目

    这一条最佳实践来自 Josh Long (Spring Advocate,@starbuxman)。

    Spring Initializr 提供了一个超级简单的方法来创建一个新的 Spring Boot 项目,并根据你的需要来加载可能使用到的依赖。

    https://start.spring.io/

    使用 Initializr 创建应用程序可确保你获得经过测试和验证的依赖项,这些依赖项适用于 Spring 自动配置。你甚至可能会发现一些新的集成,但你可能并没有意识到这些。

    4、考虑为常见的组织问题创建自己的自动配置

    这一条也来自 Josh Long(Spring Advocate,@starbuxman)——这个实践是针对高级用户的。

    如果你在一个严重依赖 Spring Boot 的公司或团队中工作,并且有共同的问题需要解决,那么你可以创建自己的自动配置。

    这项任务涉及较多工作,因此你需要考虑何时获益是值得投入的。与多个略有不同的定制配置相比,维护单个自动配置更容易。

    如果将这个提供 Spring Boot 配置以开源库的形式发布出去,那么将极大地简化数千个用户的配置工作。

    5、正确设计代码目录结构

    尽管允许你有很大的自由,但是有一些基本规则值得遵守来设计你的源代码结构。

    避免使用默认包。确保所有内容(包括你的入口点)都位于一个名称很好的包中,这样就可以避免与装配和组件扫描相关的意外情况;

    将 Application.java(应用的入口类)保留在顶级源代码目录中;

    我建议将控制器和服务放在以功能为导向的模块中,但这是可选的。一些非常好的开发人员建议将所有控制器放在一起。不论怎样,坚持一种风格!

    6、保持 @Controller 的简洁和专注

    Controller 应该非常简单。你可以在此处阅读有关 GRASP 中有关控制器模式部分的说明。你希望控制器作为协调和委派的角色,而不是执行实际的业务逻辑。以下是主要做法:

    https://en.wikipedia.org/wiki/GRASP_(object-oriented_design)#Controller

    控制器应该是无状态的!默认情况下,控制器是单例,并且任何状态都可能导致大量问题;

    控制器不应该执行业务逻辑,而是依赖委托;

    控制器应该处理应用程序的 HTTP 层,这不应该传递给服务;

    控制器应该围绕用例 / 业务能力来设计。

    要深入这个内容,需要进一步地了解设计 REST API 的最佳实践。无论你是否想要使用 Spring Boot,都是值得学习的。

    7、围绕业务功能构建 @Service

    Service 是 Spring Boot 的另一个核心概念。我发现最好围绕业务功能 / 领域 / 用例(无论你怎么称呼都行)来构建服务。

    在应用中设计名称类似AccountService, UserService, PaymentService这样的服务,比起像DatabaseService、ValidationService、CalculationService这样的会更合适一些。

    你可以决定使用 Controler 和 Service 之间的一对一映射,那将是理想的情况。但这并不意味着,Service 之间不能互相调用!

    8、使数据库独立于核心业务逻辑之外

    我之前还不确定如何在 Spring Boot 中最好地处理数据库交互。在阅读了罗伯特 ·C· 马丁的 “Clear Architecture” 之后,对我来说就清晰多了。

    你希望你的数据库逻辑于服务分离出来。理想情况下,你不希望服务知道它正在与哪个数据库通信,这需要一些抽象来封装对象的持久性。

    罗伯特 C. 马丁强烈地说明,你的数据库是一个 “细节”,这意味着不将你的应用程序与特定数据库耦合。过去很少有人会切换数据库,我注意到,使用 Spring Boot 和现代微服务开发会让事情变得更快。

    9、保持业务逻辑不受 Spring Boot 代码的影响

    考虑到 “Clear Architecture” 的教训,你还应该保护你的业务逻辑。将各种 Spring Boot 代码混合在一起是非常诱人的…… 不要这样做。如果你能抵制诱惑,你将保持你的业务逻辑可重用。

    部分服务通常成为库。如果不从代码中删除大量 Spring 注解,则更容易创建。

    10、推荐使用构造函数注入

    这一条实践来自 Phil Webb(Spring Boot 的项目负责人, @phillip_webb)。

    保持业务逻辑免受 Spring Boot 代码侵入的一种方法是使用构造函数注入。不仅是因为@Autowired注解在构造函数上是可选的,而且还可以在没有 Spring 的情况下轻松实例化 bean。

    11、熟悉并发模型

    我写过的最受欢迎的文章之一是 “介绍 Spring Boot 中的并发”。我认为这样做的原因是这个领域经常被误解和忽视。如果使用不当,就会出现问题。

    Introduction to Concurrency in Spring Boot

    在 Spring Boot 中,Controller 和 Service 是默认是单例。如果你不小心,这会引入可能的并发问题。你通常也在处理有限的线程池。请熟悉这些概念。

    如果你正在使用新的 WebFlux 风格的 Spring Boot 应用程序,我已经解释了它在 “Spring’s WebFlux/Reactor Parallelism and Backpressure” 中是如何工作的。

    12、加强配置管理的外部化

    这一点超出了 Spring Boot,虽然这是人们开始创建多个类似服务时常见的问题……

    你可以手动处理 Spring 应用程序的配置。如果你正在处理多个 Spring Boot 应用程序,则需要使配置管理能力更加强大。

    我推荐两种主要方法:

    使用配置服务器,例如 Spring Cloud Config;

    将所有配置存储在环境变量中(可以基于 git 仓库进行配置)。

    这些选项中的任何一个(第二个选项多一些)都要求你在 DevOps 更少工作量,但这在微服务领域是很常见的。

    13、提供全局异常处理

    你真的需要一种处理异常的一致方法。Spring Boot 提供了两种主要方法:

    你应该使用 HandlerExceptionResolver 定义全局异常处理策略;

    你也可以在控制器上添加 @ExceptionHandler 注解,这在某些特定场景下使用可能会很有用。

    这与 Spring 中的几乎相同,并且 Baeldung 有一篇关于 REST 与 Spring 的错误处理的详细文章,非常值得一读。

    https://www.baeldung.com/exception-handling-for-rest-with-spring

    14、使用日志框架

    你可能已经意识到这一点,但你应该使用 Logger 进行日志记录,而不是使用 System.out.println() 手动执行。这很容易在 Spring Boot 中完成,几乎没有配置。只需获取该类的记录器实例:

    Logger logger = LoggerFactory.getLogger(MyClass.class);

    这很重要,因为它可以让你根据需要设置不同的日志记录级别。

    15、测试你的代码

    这不是 Spring Boot 特有的,但它需要提醒——测试你的代码!如果你没有编写测试,那么你将从一开始就编写遗留代码。

    如果有其他人使用你的代码库,那边改变任何东西将会变得危险。当你有多个服务相互依赖时,这甚至可能更具风险。

    由于存在 Spring Boot 最佳实践,因此你应该考虑将 Spring Cloud Contract 用于你的消费者驱动契约,它将使你与其他服务的集成更容易使用。

    16、使用测试切片让测试更容易,并且更专注

    这一条实践来自 Madhura Bhave(Spring 开发者, @madhurabhave23)。

    使用 Spring Boot 测试代码可能很棘手——你需要初始化数据层,连接大量服务,模拟事物…… 实际上并不是那么难!答案是使用测试切片。

    使用测试切片,你可以根据需要仅连接部分应用程序。这可以为你节省大量时间,并确保你的测试不会与未使用的内容相关联。来自 spring.io 的一篇名为 Custom test slice with Spring test 1.4 的博客文章解释了这种技术。

    https://spring.io/blog/2016/08/30/custom-test-slice-with-spring-boot-1-4

    总结

    感谢 Spring Boot,编写基于 Spring 的微服务正变得前所未有的简单。我希望通过这些最佳实践,你的实施过程不仅会变得很快,而且从长远来看也会更加强大和成功。

  • 23 种设计模式的通俗解释,虽然有点污,但是秒懂

    快乐分享,Java干货及时送达👇

    文章源:https://zhuanlan.zhihu.com/p/100746724


    01 工厂方法


    追 MM 少不了请吃饭了,麦当劳的鸡翅和肯德基的鸡翅都是 MM 爱吃的东西,虽然口味有所不同,但不管你带 MM 去麦当劳或肯德基,只管向服务员说「来四个鸡翅」就行了。麦当劳和肯德基就是生产鸡翅的 Factory 工厂模式:客户类和工厂类分开。

    消费者任何时候需要某种产品,只需向工厂请求即可。消费者无须修改就可以接纳新产品。缺点是当产品修改时,工厂类也要做相应的修改。如:如何创建及如何向客户端提供。


    02 建造者模式


    MM 最爱听的就是「我爱你」这句话了,见到不同地方的 MM,要能够用她们的方言跟她说这句话哦,我有一个多种语言翻译机,上面每种语言都有一个按键,见到 MM 我只要按对应的键,它就能够用相应的语言说出「我爱你」这句话了,国外的 MM 也可以轻松搞掂,这就是我的「我爱你」builder。

    建造模式:将产品的内部表象和产品的生成过程分割开来,从而使一个建造过程生成具有不同的内部表象的产品对象。建造模式使得产品内部表象可以独立的变化,客户不必知道产品内部组成的细节。建造模式可以强制实行一种分步骤进行的建造过程。


    03 抽象工厂


    请 MM 去麦当劳吃汉堡,不同的 MM 有不同的口味,要每个都记住是一件烦人的事情,我一般采用 Factory Method 模式,带着 MM 到服务员那儿,说「要一个汉堡」,具体要什么样的汉堡呢,让 MM 直接跟服务员说就行了。

    工厂方法模式:核心工厂类不再负责所有产品的创建,而是将具体创建的工作交给子类去做,成为一个抽象工厂角色,仅负责给出具体工厂类必须实现的接口,而不接触哪一个产品类应当被实例化这种细节。


    04 原型模式


    跟 MM 用 QQ 聊天,一定要说些深情的话语了,我搜集了好多肉麻的情话,需要时只要 copy 出来放到 QQ 里面就行了,这就是我的情话 prototype 了。(100 块钱一份,你要不要)

    原始模型模式:通过给出一个原型对象来指明所要创建的对象的类型,然后用复制这个原型对象的方法创建出更多同类型的对象。原始模型模式允许动态的增加或减少产品类,产品类不需要非得有任何事先确定的等级结构,原始模型模式适用于任何的等级结构。缺点是每一个类都必须配备一个克隆方法。


    05 单态模式


    俺有 6 个漂亮的老婆,她们的老公都是我,我就是我们家里的老公 Sigleton,她们只要说道「老公」,都是指的同一个人,那就是我 (刚才做了个梦啦,哪有这么好的事)

    单例模式:单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例单例模式。单例模式只应在有真正的 “单一实例” 的需求时才可使用。


    06 适配器模式


    在朋友聚会上碰到了一个美女 Sarah,从香港来的,可我不会说粤语,她不会说普通话,只好求助于我的朋友 kent 了,他作为我和 Sarah 之间的 Adapter,让我和 Sarah 可以相互交谈了 (也不知道他会不会耍我)

    适配器(变压器)模式:把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口原因不匹配而无法一起工作的两个类能够一起工作。适配类可以根据参数返还一个合适的实例给客户端。


    07 桥梁模式


    早上碰到 MM,要说早上好,晚上碰到 MM,要说晚上好;碰到 MM 穿了件新衣服,要说你的衣服好漂亮哦,碰到 MM 新做的发型,要说你的头发好漂亮哦。不要问我 “早上碰到 MM 新做了个发型怎么说” 这种问题,自己用 BRIDGE 组合一下不就行了

    桥梁模式:将抽象化与实现化脱耦,使得二者可以独立的变化,也就是说将他们之间的强关联变成弱关联,也就是指在一个软件系统的抽象化和实现化之间使用组合 / 聚合关系而不是继承关系,从而使两者可以独立的变化。


    08 合成模式


    Mary 今天过生日。“我过生日,你要送我一件礼物。”“嗯,好吧,去商店,你自己挑。”“这件 T 恤挺漂亮,买,这条裙子好看,买,这个包也不错,买。”“喂,买了三件了呀,我只答应送一件礼物的哦。”“什么呀,T 恤加裙子加包包,正好配成一套呀,小姐,麻烦你包起来。”“……”,MM 都会用 Composite 模式了,你会了没有?

    合成模式:合成模式将对象组织到树结构中,可以用来描述整体与部分的关系。合成模式就是一个处理对象的树结构的模式。合成模式把部分与整体的关系用树结构表示出来。合成模式使得客户端把一个个单独的成分对象和由他们复合而成的合成对象同等看待。


    09 装饰模式


    Mary 过完轮到 Sarly 过生日,还是不要叫她自己挑了,不然这个月伙食费肯定玩完,拿出我去年在华山顶上照的照片,在背面写上 “最好的的礼物,就是爱你的 Fita”,再到街上礼品店买了个像框(卖礼品的 MM 也很漂亮哦),再找隔壁搞美术设计的 Mike 设计了一个漂亮的盒子装起来……,我们都是 Decorator,最终都在修饰我这个人呀,怎么样,看懂了吗?

    装饰模式:装饰模式以对客户端透明的方式扩展对象的功能,是继承关系的一个替代方案,提供比继承更多的灵活性。动态给一个对象增加功能,这些功能可以再动态的撤消。增加由一些基本功能的排列组合而产生的非常大量的功能。


    10 门面模式


    我有一个专业的 Nikon 相机,我就喜欢自己手动调光圈、快门,这样照出来的照片才专业,但 MM 可不懂这些,教了半天也不会。幸好相机有 Facade 设计模式,把相机调整到自动档,只要对准目标按快门就行了,一切由相机自动调整,这样 MM 也可以用这个相机给我拍张照片了。门面模式:外部与一个子系统的通信必须通过一个统一的门面对象进行。

    门面模式提供一个高层次的接口,使得子系统更易于使用。每一个子系统只有一个门面类,而且此门面类只有一个实例,也就是说它是一个单例模式。但整个系统可以有多个门面类。


    11 享元模式


    每天跟 MM 发短信,手指都累死了,最近买了个新手机,可以把一些常用的句子存在手机里,要用的时候,直接拿出来,在前面加上 MM 的名字就可以发送了,再不用一个字一个字敲了。共享的句子就是 Flyweight,MM 的名字就是提取出来的外部特征,根据上下文情况使用。享元模式:FLYWEIGHT 在拳击比赛中指最轻量级。

    享元模式以共享的方式高效的支持大量的细粒度对象。享元模式能做到共享的关键是区分内蕴状态和外蕴状态。内蕴状态存储在享元内部,不会随环境的改变而有所不同。外蕴状态是随环境的改变而改变的。外蕴状态不能影响内蕴状态,它们是相互独立的。

    将可以共享的状态和不可以共享的状态从常规类中区分开来,将不可以共享的状态从类里剔除出去。客户端不可以直接创建被共享的对象,而应当使用一个工厂对象负责创建被共享的对象。享元模式大幅度的降低内存中对象的数量。


    12 代理模式


    跟 MM 在网上聊天,一开头总是 “hi, 你好”,“你从哪儿来呀?”“你多大了?”“身高多少呀?” 这些话,真烦人,写个程序做为我的 Proxy 吧,凡是接收到这些话都设置好了自己的回答,接收到其他的话时再通知我回答,怎么样,酷吧。

    代理模式:代理模式给某一个对象提供一个代理对象,并由代理对象控制对源对象的引用。代理就是一个人或一个机构代表另一个人或者一个机构采取行动。某些情况下,客户不想或者不能够直接引用一个对象,代理对象可以在客户和目标对象直接起到中介的作用。

    客户端分辨不出代理主题对象与真实主题对象。代理模式可以并不知道真正的被代理对象,而仅仅持有一个被代理对象的接口,这时候代理对象不能够创建被代理对象,被代理对象必须有系统的其他角色代为创建并传入。


    13 责任链模式


    晚上去上英语课,为了好开溜坐到了最后一排,哇,前面坐了好几个漂亮的 MM 哎,找张纸条,写上 “Hi, 可以做我的女朋友吗?如果不愿意请向前传”,纸条就一个接一个的传上去了,糟糕,传到第一排的 MM 把纸条传给老师了,听说是个老处女呀,快跑!

    责任链模式:在责任链模式中,很多对象由每一个对象对其下家的引用而接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。客户并不知道链上的哪一个对象最终处理这个请求,系统可以在不影响客户端的情况下动态的重新组织链和分配责任。处理者有两个选择:承担责任或者把责任推给下家。一个请求可以最终不被任何接收端对象所接受。


    14 命令模式


    俺有一个 MM 家里管得特别严,没法见面,只好借助于她弟弟在我们俩之间传送信息,她对我有什么指示,就写一张纸条让她弟弟带给我。这不,她弟弟又传送过来一个 COMMAND,为了感谢他,我请他吃了碗杂酱面,哪知道他说:“我同时给我姐姐三个男朋友送 COMMAND,就数你最小气,才请我吃面。”

    命令模式:命令模式把一个请求或者操作封装到一个对象中。命令模式把发出命令的责任和执行命令的责任分割开,委派给不同的对象。命令模式允许请求的一方和发送的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求是怎么被接收,以及操作是否执行,何时被执行以及是怎么被执行的。系统支持命令的撤消。


    15 解释器模式


    俺有一个《泡 MM 真经》,上面有各种泡 MM 的攻略,比如说去吃西餐的步骤、去看电影的方法等等,跟 MM 约会时,只要做一个 Interpreter,照着上面的脚本执行就可以了。

    解释器模式:给定一个语言后,解释器模式可以定义出其文法的一种表示,并同时提供一个解释器。客户端可以使用这个解释器来解释这个语言中的句子。解释器模式将描述怎样在有了一个简单的文法后,使用模式设计解释这些语句。

    在解释器模式里面提到的语言是指任何解释器对象能够解释的任何组合。在解释器模式中需要定义一个代表文法的命令类的等级结构,也就是一系列的组合规则。每一个命令对象都有一个解释方法,代表对命令对象的解释。命令对象的等级结构中的对象的任何排列组合都是一个语言。


    16 迭代子模式


    我爱上了 Mary,不顾一切的向她求婚。Mary:“想要我跟你结婚,得答应我的条件” 我:“什么条件我都答应,你说吧” Mary:“我看上了那个一克拉的钻石” 我:“我买,我买,还有吗?” Mary:“我看上了湖边的那栋别墅” 我:“我买,我买,还有吗?” Mary:“我看上那辆法拉利跑车” 我脑袋嗡的一声,坐在椅子上,一咬牙:“我买,我买,还有吗?”

    迭代子模式:迭代子模式可以顺序访问一个聚集中的元素而不必暴露聚集的内部表象。多个对象聚在一起形成的总体称之为聚集,聚集对象是能够包容一组对象的容器对象。迭代子模式将迭代逻辑封装到一个独立的子对象中,从而与聚集本身隔开。

    迭代模式简化了聚集的界面。每一个聚集对象都可以有一个或一个以上的迭代子对象,每一个迭代子的迭代状态可以是彼此独立的。迭代算法可以独立于聚集角色变化。


    17 调停者模式


    四个 MM 打麻将,相互之间谁应该给谁多少钱算不清楚了,幸亏当时我在旁边,按照各自的筹码数算钱,赚了钱的从我这里拿,赔了钱的也付给我,一切就 OK 啦,俺得到了四个 MM 的电话。调停者模式:调停者模式包装了一系列对象相互作用的方式,使得这些对象不必相互明显作用。从而使他们可以松散偶合。

    当某些对象之间的作用发生改变时,不会立即影响其他的一些对象之间的作用。保证这些作用可以彼此独立的变化。调停者模式将多对多的相互作用转化为一对多的相互作用。调停者模式将对象的行为和协作抽象化,把对象在小尺度的行为上与其他对象的相互作用分开处理。


    18 备忘录模式


    同时跟几个 MM 聊天时,一定要记清楚刚才跟 MM 说了些什么话,不然 MM 发现了会不高兴的哦,幸亏我有个备忘录,刚才与哪个 MM 说了什么话我都拷贝一份放到备忘录里面保存,这样可以随时察看以前的记录啦。

    备忘录模式:备忘录对象是一个用来存储另外一个对象内部状态的快照的对象。备忘录模式的用意是在不破坏封装的条件下,将一个对象的状态捉住,并外部化,存储起来,从而可以在将来合适的时候把这个对象还原到存储起来的状态。


    19 观察者模式


    想知道咱们公司最新 MM 情报吗?加入公司的 MM 情报邮件组就行了,tom 负责搜集情报,他发现的新情报不用一个一个通知我们,直接发布给邮件组,我们作为订阅者(观察者)就可以及时收到情报啦。

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


    20 状态模式


    跟 MM 交往时,一定要注意她的状态哦,在不同的状态时她的行为会有不同,比如你约她今天晚上去看电影,对你没兴趣的 MM 就会说 “有事情啦”,对你不讨厌但还没喜欢上的 MM 就会说 “好啊,不过可以带上我同事么?”,已经喜欢上你的 MM 就会说 “几点钟?看完电影再去泡吧怎么样?”,当然你看电影过程中表现良好的话,也可以把 MM 的状态从不讨厌不喜欢变成喜欢哦。

    状态模式:状态模式允许一个对象在其内部状态改变的时候改变行为。这个对象看上去象是改变了它的类一样。状态模式把所研究的对象的行为包装在不同的状态对象里,每一个状态对象都属于一个抽象状态类的一个子类。

    状态模式的意图是让一个对象在其内部状态改变的时候,其行为也随之改变。状态模式需要对每一个系统可能取得的状态创立一个状态类的子类。当系统的状态变化时,系统便改变所选的子类。


    21 策略模式


    跟不同类型的 MM 约会,要用不同的策略,有的请电影比较好,有的则去吃小吃效果不错,有的去海边浪漫最合适,单目的都是为了得到 MM 的芳心,我的追 MM 锦囊中有好多 Strategy 哦。策略模式:策略模式针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它们可以相互替换。

    策略模式使得算法可以在不影响到客户端的情况下发生变化。策略模把行为和环境分开。环境类负责维持和查询行为类,各种算法在具体的策略类中提供。由于算法和环境独立开来,算法的增减,修改都不会影响到环境和客户端。


    22 模板方法模式


    看过《如何说服女生上床》这部经典文章吗?女生从认识到上床的不变的步骤分为巧遇、打破僵局、展开追求、接吻、前戏、动手、爱抚、进去八大步骤 (Template method),但每个步骤针对不同的情况,都有不一样的做法,这就要看你随机应变啦 (具体实现);

    模板方法模式:模板方法模式准备一个抽象类,将部分逻辑以具体方法以及具体构造子的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现。先制定一个顶级逻辑框架,而将逻辑的细节留给具体的子类去实现。


    23 访问者模式


    情人节到了,要给每个 MM 送一束鲜花和一张卡片,可是每个 MM 送的花都要针对她个人的特点,每张卡片也要根据个人的特点来挑,我一个人哪搞得清楚,还是找花店老板和礼品店老板做一下 Visitor,让花店老板根据 MM 的特点选一束花,让礼品店老板也根据每个人特点选一张卡,这样就轻松多了;

    访问者模式:访问者模式的目的是封装一些施加于某种数据结构元素之上的操作。一旦这些操作需要修改的话,接受这个操作的数据结构可以保持不变。访问者模式适用于数据结构相对未定的系统,它把数据结构和作用于结构上的操作之间的耦合解脱开,使得操作集合可以相对自由的演化。访问者模式使得增加新的操作变的很容易,就是增加一个新的访问者类。

    访问者模式将有关的行为集中到一个访问者对象中,而不是分散到一个个的节点类中。当使用访问者模式时,要将尽可能多的对象浏览逻辑放在访问者类中,而不是放到它的子类中。访问者模式可以跨过几个类的等级结构访问属于不同的等级结构的成员类。

  • Redis 官方可视化工具,功能真心强大!高颜值

    快乐分享,Java干货及时送达👇

    文章源:https://blog.csdn.net/weixin_46902396/article/details/120807629/


    一、RedisInsight 简介


    RedisInsight 是一个直观高效的 Redis GUI 管理工具,它可以对 Redis 的内存、连接数、命中率以及正常运行时间进行监控,并且可以在界面上使用 CLI 和连接的 Redis 进行交互(RedisInsight 内置对 Redis 模块支持):

    地址:https://docs.redis.com/latest/ri/

    RedisInsight 提供的功能:

    • 唯一支持 Redis Cluster 的 GUI 工具;
    • 可以基于 Browser 的界面来进行搜索键、查看和编辑数据;
    • 支持基于 SSL/TLS 的连接,同时还可以在界面上进行内存分析;


    二、RedisInsight 安装与使用

    1.物理安装


    1)下载 RedisInsight 软件包:

    地址:https://redis.com/redis-enterprise/redis-insight/#insight-form

    [root@Redis ~]# ls
    anaconda-ks.cfg  redisinsight-linux64-1.11.0
    [root@Redis ~]# mkdir /usr/local/redisinsight
    [root@Redis ~]# mv redisinsight-linux64-1.11.0 /usr/local/redisinsight/redisinsight-1.11.0
    [root@Redis ~]# chmod +x /usr/local/redisinsight/redisinsight-1.11.0

    2)配置 RedisInsight 的环境变量

    [root@Redis ~]# echo "export REDISINSIGHT_HOST=192.168.1.1" >> ~/.bash_profile
    [root@Redis ~]# echo "export REDISINSIGHT_HOST_DIR=/usr/local/redisinsight/.redisinsight" >> ~/.bash_profile
    [root@Redis ~]# source ~/.bash_profile

    注解:

    • REDISINSIGHT_PORT:配置 RedisInsight 的监听端口(default:8001
    • REDISINSIGHT_HOST:配置 RedisInsight 的 IP 地址(default:0.0.0.0
    • LOG_DIR:配置 RedisInsight 的日志存放路径(default:REDISINSIGHT_HOST_DIR
    • REDISINSIGHT_HOST_DIR:配置 RedisInsight 的数据存放路径(default:~/.redisinsight

    3)启动 RedisInsight 服务

    [root@Redis ~]# nohup /usr/local/redisinsight/redisinsight-linux64-1.4.0 &  // 后台运行
    [root@Redis ~]# ps aux | grep redis            // 查看进程是否存在

    2.Kubernetes 安装


    1)创建 RedisInsight 的 yaml 文件:

    [root@Redis ~]# vim redisinsight.yaml
    apiVersion: v1
    kind: Service
    metadata:
      name: redisinsight-service
    spec:
      type: NodePort
      ports:
      - port: 80
        targetPort: 8001
        nodePort: 31888
      selector:
        app: redisinsight
    ---
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: redisinsight
      labels:
        app: redisinsight
    spec:
      replicas: 1
      selector:
        matachLabels:
          app: redisinsight
      template:
        metadata:
          labels:
            app: redisinsight
        spec:
          containers:
          - name: redisinsight
            image: redislabs/redisinsight:1.7.0
            imagePullPolicy: IfNotPresent
            volumeMounts:
            - name: db
              mountPath: /db
            ports:
            - containerPort: 8001
              protocol: TCP
          volumes:
          - name: db
            emptyDir: {}

    2)启动 RedisInsight

    [root@Redis ~]# kubectl apply -f redisinsight.yaml


    3.RedisInsight 基本使用


    安装 Redis(已安装可直接跳过)

    [root@Redis ~]# wget https://download.redis.io/releases/redis-6.2.6.tar.gz
    [root@Redis ~]# tar zxf redis-6.2.6.tar.gz
    [root@Redis ~]# cd redis-6.2.6
    [root@Redis redis-6.2.6]# make PREFIX=/usr/local/redis install
    [root@Redis redis-6.2.6]# sed -i '/^bind 127.0.0.1/s/127.0.0.1/192.168.1.1/g' redis.conf  # 修改监听 IP
    [root@Redis redis-6.2.6]# sed -i '/protected-mode/s/yes/no/g' redis.conf      # 关闭保护模式
    [root@Redis redis-6.2.6]# sed -i '/daemonize/s/no/yes/g' redis.conf        # 开启后台运行
    [root@Redis redis-6.2.6]# sed -i '/requirepass/s/foobared/123123/g' redis.conf     # 配置密码
    [root@Redis redis-6.2.6]# sed -i '/requirepass 123123/s/^#//g' redis.conf      # 将密码前的 # 删除
    [root@Redis redis-6.2.6]# cp redis.conf /usr/local/redis/
    [root@Redis redis-6.2.6]# /usr/local/redis/bin/redis-server /usr/local/redis/redis.conf   # 启动 Redis

    1)通过配置的 IP 和端口,来访问 RedisInsight 的管理界面:

    2)在这里可以看到 Redis 的各种信息:

    3)同时 RedisInsight 还可以在界面上进行操作:

    4)还可以在界面上对 Redis 使用的内存进行分析:

    
    

  • Optional 是个好东西,你真的会用么?

    快乐分享,Java干货及时送达👇

    文章源:https://blog.csdn.net/zjhred/article/details/84976734

    目录

    • 引言
    • API介绍
    • 实战使用


    引言


    在文章的开头,先说下NPE问题,NPE问题就是,我们在开发中经常碰到的NullPointerException.假设我们有两个类,他们的UML类图如下图所示

    在这种情况下,有如下代码

    user.getAddress().getProvince();

    这种写法,在user为null时,是有可能报NullPointerException异常的。为了解决这个问题,于是采用下面的写法

    if(user!=null){
        Address address = user.getAddress();
        if(address!=null){
            String province = address.getProvince();
        }
    }

    这种写法是比较丑陋的,为了避免上述丑陋的写法,让丑陋的设计变得优雅。JAVA8提供了Optional类来优化这种写法,接下来的正文部分进行详细说明


    API介绍


    先介绍一下API,与其他文章不同的是,本文采取类比的方式来讲,同时结合源码。而不像其他文章一样,一个个API罗列出来,让人找不到重点。

    1、Optional(T value),empty(),of(T value),ofNullable(T value)

    这四个函数之间具有相关性,因此放在一组进行记忆。

    先说明一下,Optional(T value),即构造函数,它是private权限的,不能由外部调用的。其余三个函数是public权限,供我们所调用。那么,Optional的本质,就是内部储存了一个真实的值,在构造的时候,就直接判断其值是否为空。好吧,这么说还是比较抽象。直接上Optional(T value)构造函数的源码,如下图所示

    那么,of(T value)的源码如下

    public static  Optional of(T value) {
        return new Optional(value);
    }

    也就是说of(T value)函数内部调用了构造函数。根据构造函数的源码我们可以得出两个结论:

    • 通过of(T value)函数所构造出的Optional对象,当Value值为空时,依然会报NullPointerException。

    • 通过of(T value)函数所构造出的Optional对象,当Value值不为空时,能正常构造Optional对象。

    除此之外呢,Optional类内部还维护一个value为null的对象,大概就是长下面这样的

    public final class OptionalT> {
        //省略....
        private static final Optional> EMPTY = new Optional();
        private Optional() {
            this.value = null;
        }
        //省略...
        public static Optional empty() {
            @SuppressWarnings("unchecked")
            Optional t = (Optional) EMPTY;
            return t;
        }
    }

    那么,empty()的作用就是返回EMPTY对象。

    好了铺垫了这么多,可以说ofNullable(T value)的作用了,上源码

     public static  Optional ofNullable(T value) {
        return value == null ? empty() : of(value);
    }

    好吧,大家应该都看得懂什么意思了。相比较of(T value)的区别就是,当value值为null时,of(T value)会报NullPointerException异常;ofNullable(T value)不会throw Exception,ofNullable(T value)直接返回一个EMPTY对象。

    那是不是意味着,我们在项目中只用ofNullable函数而不用of函数呢?

    不是的,一个东西存在那么自然有存在的价值。当我们在运行过程中,不想隐藏NullPointerException。而是要立即报告,这种情况下就用Of函数。但是不得不承认,这样的场景真的很少。博主也仅在写junit测试用例中用到过此函数。

    2、orElse(T other),orElseGet(Supplier extends T> other)和orElseThrow(Supplier extends X> exceptionSupplier)

    这三个函数放一组进行记忆,都是在构造函数传入的value值为null时,进行调用的。orElseorElseGet的用法如下所示,相当于value值为null时,给予一个默认值:

    @Test
    public void test() {
        User user = null;
        user = Optional.ofNullable(user).orElse(createUser());
        user = Optional.ofNullable(user).orElseGet(() -> createUser());

    }
    public User createUser(){
        User user = new User();
        user.setName("zhangsan");
        return user;
    }

    这两个函数的区别:当user值不为null时,orElse函数依然会执行createUser()方法,而orElseGet函数并不会执行createUser()方法,大家可自行测试。

    至于orElseThrow,就是value值为null时,直接抛一个异常出去,用法如下所示

    User user = null;
    Optional.ofNullable(user).orElseThrow(()->new Exception("用户不存在"));

    3、map(Function super T, ? extends U> mapper)和flatMap(Function super T, Optional> mapper)

    这两个函数放在一组记忆,这两个函数做的是转换值的操作。

    直接上源码

     public final class OptionalT> {
        //省略....
         public Optional map(Function super T, ? extends U> mapper) {
            Objects.requireNonNull(mapper);
            if (!isPresent())
                return empty();
            else {
                return Optional.ofNullable(mapper.apply(value));
            }
        }
        //省略...
         public Optional flatMap(Function super T, Optional> mapper) {
            Objects.requireNonNull(mapper);
            if (!isPresent())
                return empty();
            else {
                return Objects.requireNonNull(mapper.apply(value));
            }
        }
    }

    这两个函数,在函数体上没什么区别。唯一区别的就是入参,map函数所接受的入参类型为Function super T, ? extends U>,而flapMap的入参类型为Function super T, Optional>

    在具体用法上,对于map而言:

    如果User结构是下面这样的

    public class User {
        private String name;
        public String getName() {
            return name;
        }
    }

    这时候取name的写法如下所示

    String city = Optional.ofNullable(user).map(u-> u.getName()).get();

    对于flatMap而言:

    如果User结构是下面这样的

    public class User {
        private String name;
        public Optional getName() {
            return Optional.ofNullable(name);
        }
    }

    这时候取name的写法如下所示

    String city = Optional.ofNullable(user).flatMap(u-> u.getName()).get();

    4、isPresent()和ifPresent(Consumer super T> consumer)

    这两个函数放在一起记忆,isPresent即判断value值是否为空,而ifPresent就是在value值不为空时,做一些操作。这两个函数的源码如下

    public final class OptionalT> {
        //省略....
        public boolean isPresent() {
            return value != null;
        }
        //省略...
        public void ifPresent(Consumer super T> consumer) {
            if (value != null)
                consumer.accept(value);
        }
    }

    需要额外说明的是,大家千万不要把

    if (user != null){
       // TODO: do something
    }

    给写成

    User user = Optional.ofNullable(user);
    if (Optional.isPresent()){
       // TODO: do something
    }

    因为这样写,代码结构依然丑陋。博主会在后面给出正确写法

    至于ifPresent(Consumer super T> consumer),用法也很简单,如下所示

    Optional.ofNullable(user).ifPresent(u->{
        // TODO: do something
    });

    5、filter(Predicate super T> predicate)

    不多说,直接上源码

    public final class OptionalT> {
        //省略....
       Objects.requireNonNull(predicate);
            if (!isPresent())
                return this;
            else
                return predicate.test(value) ? this : empty();
    }

    filter 方法接受一个 Predicate 来对 Optional 中包含的值进行过滤,如果包含的值满足条件,那么还是返回这个 Optional;否则返回 Optional.empty

    用法如下

    Optional user1 = Optional.ofNullable(user).filter(u -> u.getName().length()6);

    如上所示,如果user的name的长度是小于6的,则返回。如果是大于6的,则返回一个EMPTY对象。


    实战使用

    例一

    在函数方法中

    以前写法

    public String getCity(User user)  throws Exception{
            if(user!=null){
                if(user.getAddress()!=null){
                    Address address = user.getAddress();
                    if(address.getCity()!=null){
                        return address.getCity();
                    }
                }
            }
            throw new Excpetion("取值错误");
        }

    JAVA8写法

    public String getCity(User user) throws Exception{
        return Optional.ofNullable(user)
                       .map(u-> u.getAddress())
                       .map(a->a.getCity())
                       .orElseThrow(()->new Exception("取指错误"));
    }

    例二

    比如,在主程序中

    以前写法

    if(user!=null){
        dosomething(user);
    }

    JAVA8写法

     Optional.ofNullable(user)
        .ifPresent(u->{
            dosomething(u);
    });

    例三

    以前写法

    public User getUser(User user) throws Exception{
        if(user!=null){
            String name = user.getName();
            if("zhangsan".equals(name)){
                return user;
            }
        }else{
            user = new User();
            user.setName("zhangsan");
            return user;
        }
    }

    java8写法

    public User getUser(User user) {
        return Optional.ofNullable(user)
                       .filter(u->"zhangsan".equals(u.getName()))
                       .orElseGet(()-> {
                            User user1 = new User();
                            user1.setName("zhangsan");
                            return user1;
                       });
    }

    其他的例子,不一一列举了。不过采用这种链式编程,虽然代码优雅了。但是,逻辑性没那么明显,可读性有所降低,大家项目中看情况酌情使用。

  • 从微服务转为单体架构、成本降低 90%!是的,你没看反!

    快乐分享,Java干货及时送达👇

    来源:InfoQ

    • 1 众说纷纭:我们的架构设计究竟出了什么问题?
    • 2 看看亚马逊 CTO 和前云架构战略副总裁怎么说

    编译 | 明知山、Tina

    Ruby on Rails 之父:“即使是亚马逊也无法理解无服务器或微服务。”

    来自亚马逊 Prime Video 团队的一个案例研究在开发者社区中掀起了轩然大波。

    在该案例中,Prime Video 团队将一个监控系统从微服务架构迁移到单体架构,并避免使用昂贵的服务(如 AWS Step Functions 和 Lambda 无服务器函数),并对此举所带来的降本效果进行了评估。

    他们的需求是使用一个监控工具来识别“用户查看的视频流”的质量问题,因为有“成千上万个并发流”,所以这个工具必须是高度可伸缩的。团队最初构建了一个由 AWS Step Functions 编排而成的分布式组件解决方案,一个基于状态机和任务的无服务器编排服务。结果发现,Step Functions 居然是瓶颈所在。

    案例文章中写道:“对于视频流的每一秒,我们的服务都需要执行多次状态转换,所以很快就达到了账户限制。除此之外,AWS Step Functions 是按照状态转换向用户收费的。”还有另外一个“成本问题”,即存在大量对 S3 的一级调用(用于临时存储捕获的视频帧)。

    图片

    Prime Video 监控应用程序的初始架构,可见成本很高,伸缩性也很差

    案例文章中写道:“我们意识到分布式架构在这个特定的场景中并没有为我们带来太大的好处,所以我们将所有组件都合并到一个进程中”,从而消除了对 S3 的依赖。“我们还实现了在单个实例中控制组件的编排。”现在,这个解决方案运行在 EC2(Elastic Compute Cloud)和 ECS(Elastic Container Service)上,其中有“一个轻量级的编排层来分发用户请求”。

    案例文章总结道:“微服务和无服务器组件可以被用在大规模的场景中,但是否一定要舍弃单体而使用它们则需要根据具体情况具体分析。将服务迁移成单体让我们的基础设施成本降低了 90% 以上,还提升了我们的伸缩能力。 ”文中还提到了他们通过 EC2 节省计划来降低成本,这说明即使是内部 AWS 用户也会按照与外部用户类似的模式收取费用。

    1 众说纷纭:我们的架构设计究竟出了什么问题?

    这篇文章最初是在三月份发布的,但是直到这个月才引起整个工程界和技术界的关注。

    Hacker News 上的一条评论说:“这篇文章的存在让我感到很惊讶,甚至都有点看傻眼了”。

    图片

    在我们看来,AWS 总是习惯于将微服务和无服务器架构宣扬成“现代化”应用程序的最佳实践。例如,“AWS Well-Architected”文档的“Reliability”一项下面给出了一些建议:

    “使用面向服务架构(SOA)或微服务架构构建高度可伸缩和可靠的工作负载。面向服务架构是一种通过服务接口让软件组件变得可重用的实践。微服务架构则进一步让组件变得更小、更简单。”

    在关于.NET 应用程序现代化的“AWS Prescriptive Guidance”文档中,亚马逊列出了微服务的好处,包括更快的创新、高可用性和可靠性、更高的敏捷性和按需可伸缩性、现代 CI/CD(持续集成和部署)管道,以及强大的模块边界,尽管它也将“运维复杂性”列为缺点。

    然而,这篇案例文章似乎证实了开发者的一些猜疑。其中的一个猜疑是,AWS 推荐的解决方案可能不是最具成本效益的,因为它们总是涉及到使用多种昂贵的服务。另一个猜疑是微服务相对于单体应用的优点似乎被夸大了。

    Ruby on Rails 作者 David Heinemeier Hansson 一直在倡导减少对云服务的使用,他在评论亚马逊的这个案例研究时表示,它“对一度席卷科技行业的理论上的微服务热潮来了一次大总结 。现在,这些理论的实际结果终于出来了。很明显,在实践中,微服务可能会给系统带来不必要的复杂性。而无服务器只会让情况变得更糟。一个团队,一个应用程序,却用网络调用和服务分区取代方法调用和模块分离,这在任何一种情况下几乎都是很疯狂的。”

    图片

    “即使是亚马逊也无法理解无服务器或微服务。”

    2020 年,“Building Microservices”和“Monolith to Microservices”等书的作者 Sam Newman 在一次开发者大会上表示,“微服务不应该是默认的选择 ”,并向软件架构师提出了一些在采用微服务架构之前需要考虑的事项:“你做过价值链分析吗?你了解过瓶颈出现在哪里吗?你尝试过模块化吗?微服务应该是最后才去考虑的。”

    现在,Newman 在推特上对这篇案例文章做了评论:“这篇文章实际上更多的是关于函数与长时间运行的虚拟机的定价模型。这仍然是完全合乎逻辑的架构驱动,但从这个案例中学到的东西,其适用性范围可能相对要狭窄一些。”他接着写道:“人们之所以不公开谈论撤离微服务,是因为这可能会让一些人认为‘他们做错了’ 。视情况变化而改变你的想法才是对的做法。”

    图片

    关于这篇文章的讨论随处可见,它激起了大量的思考,在 Hacker News、Twitter、Reddit 等平台上涌现了非常多的有意思的评论和分析。

    就像一位网友在 Hacker News 上评论的那样:“这是一个很好的教训,一个很好的故事,而且很重要的是它来自亚马逊内部团队。”

    对于该案例的初始架构设计存在的问题,一位 DataDog 高级软件工程师 Lambros Petrou 在推特上发表了分析:“Prime Video 案例文章中的设计是有问题的。滥用服务并不能解决架构问题,它只会把问题暴露出来”。前首席技术官 Steve Chambers 在某种程度上也同意这一观点,他说:“基本上,他们(现在)使用的是相同的架构,只是将组件塞到了容器中,这样他们就不会在不同的云服务之间通过网络进行昂贵的调用和数据传输……这很明显是一种优化!”

    一名工程师在 Reddit 上针对这个案例争论道:“微服务是有额外开销的。本来简单的进程间通信,或者系统两个部件之间的内存调用,变成了 HTTPS、OAuth、JSON 编码 / 解码,而且每次都需要发生这些简短的对话。当你的系统被分解成 50 万个部件,每一次通信都需要做这些事情,并且你要为每一个事务付费时,成本和复杂性就会增加。拆解单体需要一次性替换整个应用程序,这意味着开发人员需要做一些测试。但 DevOps 意味着不会有更多的测试,所以在生产环境中会发生故障,我们唯一能做的是让功能块变小,这样才可能快速找到和修复问题。”

    但总的来说,这篇案例文章对于 AWS 来说未必是坏消息。一方面,这似乎与这家云计算巨头所宣扬的最佳实践背道而驰,但另一方面,这也是一个令人耳目一新的关于如何通过简单的架构来降低成本的实践。与许多促销案例研究不同,这个案例看起来对 AWS 客户真的很有用。

    2 看看亚马逊 CTO 和前云架构战略副总裁怎么说

    这件事情被大家传得沸沸扬扬的,甚至还惊动了亚马逊 CTO Werner Vogels 博士。

    他对此表示,构建可演进的软件系统是一种策略,我们必须以开放的心态重新审视自己的架构。

    “软件架构不像桥梁和房屋的架构。桥梁建成后就很难改变,但软件不一样。软件一旦运行起来,我们就可以更深入地了解我们的工作负载,然后再选择一个可演进的架构,在不影响客户体验的情况下进行更改。我的经验法则是,随着每个数量级的增长,你都应该重新审视你的架构,并确定它是否仍能支持下一个数量级的增长。”

    “Prime Video 就是个很好的例子。(架构设计)没有放之四海而皆准的方法。我们总是敦促我们的工程师找到最佳解决方案,并且我们没有强制要求特定的架构风格。”“我想重申,没有一种架构模式可以满足所有的情况……单体没有消亡(恰恰相反),可演进的架构也在不断变化的技术格局中扮演着越来越重要的角色。”

    在 Werner Vogels 博士发表看法之后,去年已宣布退休的前亚马逊云科技可持续发展架构副总裁、前云架构战略副总裁 Adrian Cockcroft 也忍不住出来点评了一下。

    “关于 Prime Video 的案例,虽然互联网上涌现了堆积如山的意见和观点,但大多没有说到点子上。”

    Prime Video 团队只是遵循了一条“Serverless First(无服务器优先)”的原则:首先尝试使用 Step Functions 和 Lambda 进行快速构建。通过无服务器方法,在几天或几周内构建一个原型,这是重点。然后在需要应对高流量时,再进行重构。“Serverless First”和“Serverless Only”是有区别的,“我不提倡 Serverless Only”。

    他认为这个案例之所以会引起这么大的反应,关键是“在微服务被过度营销的情况下,大家以为 Prime Video 团队返回到了单体架构”。

    “我确实认为微服务被过度宣传了,把微服务作为解决所有问题的答案。而且这可能是因为厂商想通过简单的营销信息来销售 Kubernetes,即需要通过使用 Kubernetes 来实现云原生的微服务。大家对这种信息传递方式表示了强烈的反对。Kubernetes 的复杂性是有代价的,除非你的团队和规模非常大,否则你不需要它。(建议阅读 Sam Newman 的《构建微服务:设计细粒度系统》一书。)”

    总之,Prime Video 团队最初的设计还是很棒的,帮助他们快速进入市场并带来收益。而现在,他们在退后一步进行下一阶段的分析。“该团队遵循了我认为的最佳实践”,“我认为 Prime 团队发布的这篇文章非常富有洞察力……”

    参考链接:

    https://www.primevideotech.com/video-streaming/scaling-up-the-prime-video-audio-video-monitoring-service-and-reducing-costs-by-90

    https://world.hey.com/dhh/even-amazon-can-t-make-sense-of-serverless-or-microservices-59625580

    Reduce costs by 90% by moving from microservices to monolith: Amazon internal case study raises eyebrows

    https://thestack.technology/amazon-prime-video-microservices-monolith/

    https://news.ycombinator.com/item?id=35811741

    https://twitter.com/samnewman/status/1654432661337788416

    https://www.allthingsdistributed.com/2023/05/monoliths-are-not-dinosaurs.html

    https://adrianco.medium.com/so-many-bad-takes-what-is-there-to-learn-from-the-prime-video-microservices-to-monolith-story-4bd0970423d4

  • Java 8 腰斩!Java 17 暴涨 430%!!

    快乐分享,Java干货及时送达👇

    来源:OSC开源社区(ID:oschina2013)
    • Java 17 用户采用率在一年内增长了 430%
    • Java 14 是最受欢迎的非 LTS 版本
    • Amazon 是现在最受欢迎的 JDK 供应商

    New Relic 最新发布了一份 “2023 年 Java 生态系统状况报告”,旨在提供有关当今 Java 生态系统状态的背景和见解。该报告基于从数百万个提供性能数据的应用程序中收集的数据,对生产中使用最多的版本、最受欢迎的 JDK 供应商、容器的兴起等多方面进行了调研分析。

    Java 17 用户采用率在一年内增长了 430%

    Java 每两到三年推出一个长期支持 (LTS) 版本。其中,目前最受欢迎的一个版本是 2018 年 9 月发布的 Java 11;有超过 56% 的应用程序在生产中使用它,高于 2022 年的 48% 和 2020 年的 11% 占比。2014 年发布的 Java 8 紧随其后,近 33% 的应用程序在生产中使用该版本,低于 2022 年的 46%。

    报告称,虽然 Java 11 已经连续两年占据榜首,但 Java 17 (2021 年 9 月发布) 的采用率增长远远超过了 Java 11 推出时的情况。现共有超过 9% 的应用程序在生产中使用 Java 17 (2022 年还不到 1%),这意味着其在一年内实现了 430% 的增长率。而 Java 11 花了多年时间才达到接近这个水平的增长。

    对 Java 7 (2011 年 7 月发布的 LTS 版本) 的支持已于 2022 年结束,现只有 0.28% 的应用程序仍在生产中使用该版本。大多数使用 Java 7 的应用程序都是尚未升级的遗留应用程序。

    图片

    Java 14 是最受欢迎的非 LTS 版本

    与 LTS 版本相比,仅支持六个月的非 LTS Java 版本的使用率仍然极低;只有 1.6% 的应用程序使用非 LTS Java 版本,低于 2022 年的 2.7%。其中,于 2020 年 1 月发布的 Java 14 仍然是最受欢迎的非 LTS 版本;使用率为 0.57%,低于 2022 年的 0.95%。Java 15 紧随其后为 0.44%,低于 2022 年的 0.70%。

    图片

    报告认为,导致非 LTS 版本使用率下降的一些可能因素包括:

    • 缺乏支持
    • 特性的感知吸引力
    • 距离下一个 LTS 版本的时间长度

    Amazon 是现在最受欢迎的 JDK 供应商

    2020 年,甲骨文是最受欢迎的 JDK 供应商,占据了大约 75% 的市场份额。2022 年,甲骨文虽然保住了第一的市占位置,但其份额已经缩水了近一半,跌至 34%,2023 年还继续下滑到了 28%。New Relic 认为,导致这一现象的主要原因是甲骨文对 Java 11 的限制性许可。此后,该公司对 Java 17 恢复了更加开放的立场。

    与此同时,Amazon Corretto 使用率大幅增加成为最常用的 Java 开发工具包,市场份额达到 31%。使得该公司成为最受欢迎的 JDK 供应商,其 2020 和 2022 年的市占分别为 2.18% 和 22%。

    图片

    报告中的一些其他内容还包括:

    • 容器化应用程序已成为主流 —— 向 New Relic 报告的 Java 应用中有 70% 是从容器中进行的。
    • Garbage-First (G1) 垃圾收集器仍然是使用 Java 11 或更高版本的用户的最爱,有 65% 的使用率。其他在 Java 8 之后出现的实验性垃圾收集器(ZGC 和 Shenandoah)在生产系统中的使用仍然很少。两者都有生产就绪版本,但在一般处理中仍然可以忽略不计。

    更多详情可查看完整报告:https://newrelic.com/resources/report/2023-state-of-the-java-ecosystem

    
    

  • 代码精简10倍,责任链模式yyds

    快乐分享,Java干货及时送达👇

    目录

    • 背景
    • 什么是责任链
    • 使用场景
    • 结语

    背景

    最近,我让团队内一位成员写了一个导入功能。他使用了责任链模式,代码堆的非常多,bug 也多,没有达到我预期的效果。

    实际上,针对导入功能,我认为模版方法更合适!为此,隔壁团队也拿出我们的案例,进行了集体 code review。

    学好设计模式,且不要为了练习,强行使用!让原本 100 行就能实现的功能,写了 3000 行!对错暂且不论,我们先一起看看责任链设计模式吧!

    什么是责任链

    责任链模式是一种行为设计模式, 允许你将请求沿着处理者链进行发送。收到请求后, 每个处理者均可对请求进行处理, 或将其传递给链上的下个处理者。

    图片

    使用场景

    责任链的使用场景还是比较多的:

    • 多条件流程判断:权限控制

    • ERP 系统流程审批:总经理、人事经理、项目经理

    • Java 过滤器的底层实现 Filter

    如果不使用该设计模式,那么当需求有所改变时,就会使得代码臃肿或者难以维护,例如下面的例子。

    | 反例

    假设现在有一个闯关游戏,进入下一关的条件是上一关的分数要高于 xx:

    • 游戏一共 3 个关卡

    • 进入第二关需要第一关的游戏得分大于等于 80

    • 进入第三关需要第二关的游戏得分大于等于 90

    那么代码可以这样写:

    //第一关  
    public class FirstPassHandler {  
        public int handler(){  
            System.out.println("第一关-->FirstPassHandler");  
            return 80;  
        }  
    }  
      
    //第二关  
    public class SecondPassHandler {  
        public int handler(){  
            System.out.println("第二关-->SecondPassHandler");  
            return 90;  
        }  
    }  
      
      
    //第三关  
    public class ThirdPassHandler {  
        public int handler(){  
            System.out.println("第三关-->ThirdPassHandler,这是最后一关啦");  
            return 95;  
        }  
    }  
      
      
    //客户端  
    public class HandlerClient {  
        public static void main(String[] args) {  
      
            FirstPassHandler firstPassHandler = new FirstPassHandler();//第一关  
            SecondPassHandler secondPassHandler = new SecondPassHandler();//第二关  
            ThirdPassHandler thirdPassHandler = new ThirdPassHandler();//第三关  
      
            int firstScore = firstPassHandler.handler();  
            //第一关的分数大于等于80则进入第二关  
            if(firstScore >= 80){  
                int secondScore = secondPassHandler.handler();  
                //第二关的分数大于等于90则进入第二关  
                if(secondScore >= 90){  
                    thirdPassHandler.handler();  
                }  
            }  
        }  
    }  

    那么如果这个游戏有 100 关,我们的代码很可能就会写成这个样子:

    if(第1关通过){  
        // 第2关 游戏  
        if(第2关通过){  
            // 第3关 游戏  
            if(第3关通过){  
               // 第4关 游戏  
                if(第4关通过){  
                    // 第5关 游戏  
                    if(第5关通过){  
                        // 第6关 游戏  
                        if(第6关通过){  
                            //...  
                        }  
                    }  
                }   
            }  
        }  
    }  

    这种代码不仅冗余,并且当我们要将某两关进行调整时会对代码非常大的改动,这种操作的风险是很高的,因此,该写法非常糟糕。

    | 初步改造

    如何解决这个问题,我们可以通过链表将每一关连接起来,形成责任链的方式,第一关通过后是第二关,第二关通过后是第三关….

    这样客户端就不需要进行多重 if 的判断了:

    public class FirstPassHandler {  
        /**  
         * 第一关的下一关是 第二关  
         */
      
        private SecondPassHandler secondPassHandler;  
      
        public void setSecondPassHandler(SecondPassHandler secondPassHandler) {  
            this.secondPassHandler = secondPassHandler;  
        }  
      
        //本关卡游戏得分  
        private int play(){  
            return 80;  
        }  
      
        public int handler(){  
            System.out.println("第一关-->FirstPassHandler");  
            if(play() >= 80){  
                //分数>=80 并且存在下一关才进入下一关  
                if(this.secondPassHandler != null){  
                    return this.secondPassHandler.handler();  
                }  
            }  
      
            return 80;  
        }  
    }  
      
    public class SecondPassHandler {  
      
        /**  
         * 第二关的下一关是 第三关  
         */
      
        private ThirdPassHandler thirdPassHandler;  
      
        public void setThirdPassHandler(ThirdPassHandler thirdPassHandler) {  
            this.thirdPassHandler = thirdPassHandler;  
        }  
      
        //本关卡游戏得分  
        private int play(){  
            return 90;  
        }  
      
        public int handler(){  
            System.out.println("第二关-->SecondPassHandler");  
      
            if(play() >= 90){  
                //分数>=90 并且存在下一关才进入下一关  
                if(this.thirdPassHandler != null){  
                    return this.thirdPassHandler.handler();  
                }  
            }  
      
            return 90;  
        }  
    }  
      
    public class ThirdPassHandler {  
      
        //本关卡游戏得分  
        private int play(){  
            return 95;  
        }  
      
        /**  
         * 这是最后一关,因此没有下一关  
         */
      
        public int handler(){  
            System.out.println("第三关-->ThirdPassHandler,这是最后一关啦");  
            return play();  
        }  
    }  
      
    public class HandlerClient {  
        public static void main(String[] args) {  
      
            FirstPassHandler firstPassHandler = new FirstPassHandler();//第一关  
            SecondPassHandler secondPassHandler = new SecondPassHandler();//第二关  
            ThirdPassHandler thirdPassHandler = new ThirdPassHandler();//第三关  
      
            firstPassHandler.setSecondPassHandler(secondPassHandler);//第一关的下一关是第二关  
            secondPassHandler.setThirdPassHandler(thirdPassHandler);//第二关的下一关是第三关  
      
            //说明:因为第三关是最后一关,因此没有下一关  
            //开始调用第一关 每一个关卡是否进入下一关卡 在每个关卡中判断  
            firstPassHandler.handler();  
      
        }  
    }  

    | 缺点

    现有模式的缺点:

    • 每个关卡中都有下一关的成员变量并且是不一样的,形成链很不方便

    • 代码的扩展性非常不好

    | 责任链改造

    既然每个关卡中都有下一关的成员变量并且是不一样的,那么我们可以在关卡上抽象出一个父类或者接口,然后每个具体的关卡去继承或者实现。

    有了思路,我们先来简单介绍一下责任链设计模式的基本组成:

    • 抽象处理者(Handler)角色: 定义一个处理请求的接口,包含抽象处理方法和一个后继连接。

    • 具体处理者(Concrete Handler)角色: 实现抽象处理者的处理方法,判断能否处理本次请求,如果可以处理请求则处理,否则将该请求转给它的后继者。

    • 客户类(Client)角色: 创建处理链,并向链头的具体处理者对象提交请求,它不关心处理细节和请求的传递过程。

    图片
    public abstract class AbstractHandler {  
      
        /**  
         * 下一关用当前抽象类来接收  
         */
      
        protected AbstractHandler next;  
      
        public void setNext(AbstractHandler next) {  
            this.next = next;  
        }  
      
        public abstract int handler();  
    }  
      
    public class FirstPassHandler extends AbstractHandler{  
      
        private int play(){  
            return 80;  
        }  
      
        @Override  
        public int handler(){  
            System.out.println("第一关-->FirstPassHandler");  
            int score = play();  
            if(score >= 80){  
                //分数>=80 并且存在下一关才进入下一关  
                if(this.next != null){  
                    return this.next.handler();  
                }  
            }  
            return score;  
        }  
    }  
      
    public class SecondPassHandler extends AbstractHandler{  
      
        private int play(){  
            return 90;  
        }  
      
        public int handler(){  
            System.out.println("第二关-->SecondPassHandler");  
      
            int score = play();  
            if(score >= 90){  
                //分数>=90 并且存在下一关才进入下一关  
                if(this.next != null){  
                    return this.next.handler();  
                }  
            }  
      
            return score;  
        }  
    }  
      
    public class ThirdPassHandler extends AbstractHandler{  
      
        private int play(){  
            return 95;  
        }  
      
        public int handler(){  
            System.out.println("第三关-->ThirdPassHandler");  
            int score = play();  
            if(score >= 95){  
                //分数>=95 并且存在下一关才进入下一关  
                if(this.next != null){  
                    return this.next.handler();  
                }  
            }  
            return score;  
        }  
    }  
      
    public class HandlerClient {  
        public static void main(String[] args) {  
      
            FirstPassHandler firstPassHandler = new FirstPassHandler();//第一关  
            SecondPassHandler secondPassHandler = new SecondPassHandler();//第二关  
            ThirdPassHandler thirdPassHandler = new ThirdPassHandler();//第三关  
      
            // 和上面没有更改的客户端代码相比,只有这里的set方法发生变化,其他都是一样的  
            firstPassHandler.setNext(secondPassHandler);//第一关的下一关是第二关  
            secondPassHandler.setNext(thirdPassHandler);//第二关的下一关是第三关  
      
            //说明:因为第三关是最后一关,因此没有下一关  
      
            //从第一个关卡开始  
            firstPassHandler.handler();  
      
        }  
    }  

    | 责任链工厂改造

    对于上面的请求链,我们也可以把这个关系维护到配置文件中或者一个枚举中。我将使用枚举来教会大家怎么动态的配置请求链并且将每个请求者形成一条调用链。

    图片
    public enum GatewayEnum {  
        // handlerId, 拦截者名称,全限定类名,preHandlerId,nextHandlerId  
        API_HANDLER(new GatewayEntity(1"api接口限流""cn.dgut.design.chain_of_responsibility.GateWay.impl.ApiLimitGatewayHandler"null2)),  
        BLACKLIST_HANDLER(new GatewayEntity(2"黑名单拦截""cn.dgut.design.chain_of_responsibility.GateWay.impl.BlacklistGatewayHandler"13)),  
        SESSION_HANDLER(new GatewayEntity(3"用户会话拦截""cn.dgut.design.chain_of_responsibility.GateWay.impl.SessionGatewayHandler"2null)),  
        ;  
      
        GatewayEntity gatewayEntity;  
      
        public GatewayEntity getGatewayEntity() {  
            return gatewayEntity;  
        }  
      
        GatewayEnum(GatewayEntity gatewayEntity) {  
            this.gatewayEntity = gatewayEntity;  
        }  
    }  
      
    public class GatewayEntity {  
      
        private String name;  
      
        private String conference;  
      
        private Integer handlerId;  
      
        private Integer preHandlerId;  
      
        private Integer nextHandlerId;  
    }  
      
      
    public interface GatewayDao {  
      
        /**  
         * 根据 handlerId 获取配置项  
         * @param handlerId  
         * @return  
         */
      
        GatewayEntity getGatewayEntity(Integer handlerId);  
      
        /**  
         * 获取第一个处理者  
         * @return  
         */
      
        GatewayEntity getFirstGatewayEntity();  
    }  
      
    public class GatewayImpl implements GatewayDao {  
      
        /**  
         * 初始化,将枚举中配置的handler初始化到map中,方便获取  
         */
      
        private static Map gatewayEntityMap = new HashMap();  
      
        static {  
            GatewayEnum[] values = GatewayEnum.values();  
            for (GatewayEnum value : values) {  
                GatewayEntity gatewayEntity = value.getGatewayEntity();  
                gatewayEntityMap.put(gatewayEntity.getHandlerId(), gatewayEntity);  
            }  
        }  
      
        @Override  
        public GatewayEntity getGatewayEntity(Integer handlerId) {  
            return gatewayEntityMap.get(handlerId);  
        }  
      
        @Override  
        public GatewayEntity getFirstGatewayEntity() {  
            for (Map.Entry entry : gatewayEntityMap.entrySet()) {  
                GatewayEntity value = entry.getValue();  
                //  没有上一个handler的就是第一个  
                if (value.getPreHandlerId() == null) {  
                    return value;  
                }  
            }  
            return null;  
        }  
    }  
      
    public class GatewayHandlerEnumFactory {  
      
        private static GatewayDao gatewayDao = new GatewayImpl();  
      
        // 提供静态方法,获取第一个handler  
        public static GatewayHandler getFirstGatewayHandler() {  
      
            GatewayEntity firstGatewayEntity = gatewayDao.getFirstGatewayEntity();  
            GatewayHandler firstGatewayHandler = newGatewayHandler(firstGatewayEntity);  
            if (firstGatewayHandler == null) {  
                return null;  
            }  
      
            GatewayEntity tempGatewayEntity = firstGatewayEntity;  
            Integer nextHandlerId = null;  
            GatewayHandler tempGatewayHandler = firstGatewayHandler;  
            // 迭代遍历所有handler,以及将它们链接起来  
            while ((nextHandlerId = tempGatewayEntity.getNextHandlerId()) != null) {  
                GatewayEntity gatewayEntity = gatewayDao.getGatewayEntity(nextHandlerId);  
                GatewayHandler gatewayHandler = newGatewayHandler(gatewayEntity);  
                tempGatewayHandler.setNext(gatewayHandler);  
                tempGatewayHandler = gatewayHandler;  
                tempGatewayEntity = gatewayEntity;  
            }  
        // 返回第一个handler  
            return firstGatewayHandler;  
        }  
      
        /**  
         * 反射实体化具体的处理者  
         * @param firstGatewayEntity  
         * @return  
         */
      
        private static GatewayHandler newGatewayHandler(GatewayEntity firstGatewayEntity) {  
            // 获取全限定类名  
            String className = firstGatewayEntity.getConference();   
            try {  
                // 根据全限定类名,加载并初始化该类,即会初始化该类的静态段  
                Class> clazz = Class.forName(className);  
                return (GatewayHandler) clazz.newInstance();  
            } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {  
                e.printStackTrace();  
            }  
            return null;  
        }  
      
      
    }  
      
    public class GetewayClient {  
        public static void main(String[] args) {  
            GetewayHandler firstGetewayHandler = GetewayHandlerEnumFactory.getFirstGetewayHandler();  
            firstGetewayHandler.service();  
        }  
    }  

    结语

    设计模式有很多,责任链只是其中的一种,我觉得很有意思,非常值得一学。设计模式确实是一门艺术,仍需努力呀!

    来源:blog.csdn.net/q1472750149/article/

    details/121886327