欢迎光临
我们一直在努力

SpringCloud微服务架构升级总结

mumupudding阅读(2)


一、背景

1.1 应用系统的架构历史

1.2 什么是微服务?

起源:微服务的概念源于 2014 年 3 月 Martin Fowler 所写的一篇文章“Microservices”。文中内容提到:微服务架构是一种架构模式,它提倡将单一应用程序划分成一组小的服务,服务之间互相协调、互相配合,为用户提供最终价值。

通信方式:每个服务运行在其独立的进程中,服务与服务间采用轻量级的通信机制互相沟通(通常是基于 HTTP 的 RESTful API)。

微服务的常规定义:微服务是一种架构风格,一个大型复杂软件应用由一个或多个微服务组成。系统中的各个微服务可被独立部署,各个微服务之间是松耦合的。每个微服务仅关注于完成一件任务。

把原来的一个完整的进程服务,拆分成两个或两个以上的进程服务,且互相之间存在调用关系,与原先单一的进程服务相比,就是“微服务”。(微服务是一个比较级的概念,而不是单一的概念)

1.3 微服务架构的优势

  • 可扩展性:在增加业务功能时,单一应用架构需要在原先架构的代码基础上做比较大的调整,而微服务架构只需要增加新的微服务节点,并调整与之有关联的微服务节点即可。在增加业务响应能力时,单一架构需要进行整体扩容,而微服务架构仅需要扩容响应能力不足的微服务节点。
  • 容错性:在系统发生故障时,单一应用架构需要进行整个系统的修复,涉及到代码的变更和应用的启停,而微服务架构仅仅需要针对有问题的服务进行代码的变更和服务的启停。其他服务可通过重试、熔断等机制实现应用层面的容错。
  • 技术选型灵活:微服务架构下,每个微服务节点可以根据完成需求功能的不同,自由选择最适合的技术栈,即使对单一的微服务节点进行重构,成本也非常低。
  • 开发运维效率更高:每个微服务节点都是一个单一进程,都专注于单一功能,并通过定义良好的接口清晰表述服务边界。由于体积小、复杂度低,每个微服务可由一个小规模团队或者个人完全掌控,易于保持高可维护性和开发效率。

Spring Cloud作为目前最流行的微服务开发框架,不是采用了Spring Cloud框架就实现了微服务架构,具备了微服务架构的优势。正确的理解是使用Spring Cloud框架开发微服务架构的系统,使系统具备微服务架构的优势(Spring Cloud就像工具,还需要“做”的过程)。

1.4 什么是Spring Boot?什么是Spring Cloud?

Spring Boot框架是由Pivotal团队提供的全新框架,其设计目的是用来简化基于Spring应用的初始搭建以及开发过程。SpringBoot框架使用了特定的方式来进行应用系统的配置,从而使开发人员不再需要耗费大量精力去定义模板化的配置文件。

Spring Cloud是一个基于Spring Boot实现的云应用开发工具,它为基于JVM的云应用开发中的配置管理、服务注册,服务发现、断路器、智能路由、微代理、控制总线、全局锁、决策竞选、分布式会话和集群状态管理等操作提供了一种简单的开发方式。

1.5 微服务、Spring Boot、Spring Cloud三者之间的关系

  • 思想:微服务是一种架构的理念,提出了微服务的设计原则,从理论为具体的技术落地提供了指导思想。
  • 脚手架:Spring Boot是一套快速配置脚手架,可以基于Spring Boot快速开发单个微服务。
  • 多个组件的集合:Spring Cloud是一个基于Spring Boot实现的服务治理工具包;Spring Boot专注于快速、方便集成的单个微服务个体;Spring Cloud关注全局的服务治理框架。

二、技术解析

2.1 Everything is jar, Everything is http

Spring Boot通过@SpringBootApplication注解标识为Spring Boot应用程序。所有的应用都通过jar包方式编译,部署和运行。

@SpringBootApplication public class Application {         private static final Logger LOGGER = LoggerFactory.getLogger(Application.class);         public static void main(String[] args) {                 SpringApplication.run(Application.class, args);                 LOGGER.info(”启动成功!");         } }

每个Spring Boot的应用都可以通过内嵌web容器的方式提供http服务,仅仅需要在pom文件中依赖spring-boot-start-web即可,原则上微服务架构希望每个独立节点都提供http服务。

    <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-web</artifactId>    </dependency>

2.2 Spring boot Task 任务启动和定时任务

在Spring Boot需要启动任务时,只要继承CommandLineRunner接口实现其run方法即可。

@SpringBootApplication public class ClientDataListener implements CommandLineRunner    public void run(String... strings) throws Exception {             clientInfoListenerHandler();     }}

在Spring Boot需要执行定时任务时,只需要在定时任务方法上增加@Scheduled(cron = “0 15 0 * * ?”)注解(支持标准cron表达式),并且在服务启动类上增加@EnableScheduling的注解即可。

@SpringBootApplication@EnableSchedulingpublic class Application {         private static final Logger LOGGER = LoggerFactory.getLogger(Application.class);         public static void main(String[] args) {                 SpringApplication.run(Application.class, args);                 LOGGER.info(”启动成功!");         } }
// some class@Scheduled(cron = "0 15 0 * * ?")public void someTimeTask() {    ***}

2.3 Spring boot Actuator 监控

Actuator是spring boot提供的对应用系统自身进行监控的组件,在引入spring-boot-start-web基础上引入spring-boot-starter-actuator即可。

    <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-actuator</artifactId>    </dependency>

2.4 Spring cloud Config 配置中心

在我们实现微服务架构时,每个微服务节点都需要自身的相关配置数据项,当节点众多,维护就变得非常困难,因此需要建立一个中心配置服务。

Spring Cloud Config分为两部分。Spring Cloud Config server作为一个服务进程,Spring Cloud Config File为配置文件存放位置。

2.5 Spring cloud Eureka 服务注册中心

服务注册的概念早在微服务架构之前就出现了,微服务架构更是把原先的单一应用节点拆分成非常多的微服务节点。互相之间的调用关系会非常复杂,Spring Cloud Eureka作为注册中心,所有的微服务都可以将自身注册到Spring Cloud Eureka进行统一的管理和访问(Eureka和Zookeeper不同,在AOP原则中选择了OP,更强调服务的有效性)

2.6 Spring cloud Zuul 服务端智能路由

当我们把所有的服务都注册到Eureka(服务注册中心)以后,就涉及到如何调用的问题。Spring Cloud Zuul是Spring Cloud提供的服务端代理组件,可以看做是网关,Zuul通过Eureka获取到可用的服务,通过映射配置,客户端通过访问Zuul来访问实际需要需要访问的服务。所有的服务通过spring.application.name做标识,

不同IP地址,相同spring.application.name就是一个服务集群。当我们增加一个相同spring.application.name的节点,Zuul通过和Eureka通信获取新增节点的信息实现智能路由,增加该类型服务的响应能力。

2.7 Spring cloud Ribbon 客户端智能路由

与Spring Cloud Zuul的服务端代理相对应,Spring Cloud Ribbon提供了客户端代理。在服务端代理中,客户端并不需要知道最终是哪个微服务节点为之提供服务,而客户端代理获取实质提供服务的节点,并选择一个进行服务调用。Ribbon和Zuul相似,也是通过和Eureka(服务注册中心)进行通信来实现客户端智能路由。

2.8 Spring cloud Sleuth 分布式追踪

2.9 Spring cloud Zipkin 调用链

2.10 Spring cloud Feign http客户端

Spring Cloud Feign是一种声明式、模板化的http客户端。 使用Spring Cloud Feign请求远程服务时能够像调用本地方法一样,让开发者感觉不到这是远程方法(Feign集成了Ribbon做负载均衡)。

把远程服务和本地服务做映射

@FeignClient(name = "rabbitmq-http", url = "${SKYTRAIN_RABBITMQ_HTTP}") public interface TaskService {         @RequestMapping(value = "/api/queues", method = RequestMethod.GET)         public String query(@RequestHeader("Authorization") String token); }

以调用本地服务的方式调用远程服务

@Autowired private TaskService taskService; private String queryRabbitmqStringInfo() {         byte[] credentials = Base64 .encodeBase64((rabbitmqHttpUserName + ":" + rabbitmqHttpPassword).getBytes(StandardCharsets.UTF_8));         String token = "Basic " + new String(credentials, StandardCharsets.UTF_8);         return taskService.query(token); }

2.11 Spring cloud Hystrix 断路器

三、微服务实践

3.1 我们开发的几个微服务组件—应用管理中心

应用管理中心可以对每个已经注册的微服务节点进行停止,编译,打包,部署,启动的完整的上线操作。

3.2 我们开发的几个微服务组件—zookeeper数据查询中心

zookeeper数据查询中心根据zookeeper地址,端口,命令获取zookeeper数据信息。

3.3 我们开发的几个微服务组件—微服务健康检测中心

健康检测中心周期性检查每个微服务的状态,当发现有微服务状态处于DOWN或连接超时时,触发报警。

3.4 我们开发的几个微服务组件—定时任务查询中心

// 在BeanPostProcessor子类中拦截@Componentpublic class SkytrainBeanPostProcessor implements BeanPostProcessor, Ordered {    ***    /**     * Bean 实例化之后进行的处理     */    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {        beanPostProcessor.postProcessAfter(bean, beanName);        return bean;    }    ***}// 拦截后获取定时任务注解***public Object postProcessAfter(Object bean, String beanName) {    Class targetClass = AopUtils.getTargetClass(bean);    Map annotatedMethods = MethodIntrospector.selectMethods(targetClass,            new MethodIntrospector.MetadataLookup() {                 public Set inspect(Method method) {                     Set scheduledMethods = AnnotatedElementUtils.getMergedRepeatableAnnotations(method,                            Scheduled.class, Schedules.class);                    return (!scheduledMethods.isEmpty() ? scheduledMethods : null);                }            });    if (!annotatedMethods.isEmpty()) {        String className = targetClass.getName();        for (Map.Entry entry : annotatedMethods.entrySet()) {            Method method = entry.getKey();            for (Scheduled scheduled : entry.getValue()) {                String key = className + ":" + method.getName();                String value = scheduled.toString();                taskInfos.put(key, value);            }        }    }    return null;}*** // 获取定时任务后注册***public void taskRegister() {    String nodeInfo = ipAddress + ":" + serverPort + ":";    try {        /**         * 定时任务         */        Map infos = taskInfos;        for (Entry item : infos.entrySet()) {            String taskId = nodeInfo + item.getKey();            String taskParameter = item.getValue();            JSONObject info = new JSONObject();            info.put("taskId", taskId);            info.put("taskParameter", taskParameter);            info.put("applicationName", applicationName);            info.put("taskType", "schedule");            LOGGER.info(info.toString());            zooKeeperExecutor.createZKNode(SKYTRAIN_TASK_ZKNODE_PREFIX + taskId, info.toString());        }    }    catch (Exception ex) {        LOGGER.error("", ex);    }}***

3.5 微服务的分类

  • 微服务平台组件
  • 公共服务组件
  • 基础服务组件/业务服务组件

3.6 整体微服务架构图

作者:梁鑫

来源:宜信技术学院

印度版的“大众点评”如何将 Food Feed 业务从 Redis 迁移到 Cassandra

mumupudding阅读(12)

Zomato 是一家食品订购、外卖及餐馆发现平台,被称为印度版的“大众点评”。目前,该公司的业务覆盖全球24个国家(主要是印度,东南亚和中东市场)。本文将介绍该公司的 Food Feed 业务是如何从 Redis 迁移到 Cassandra 的。

Food Feed 是 Zomato 社交场景中不可或缺的一部分,因为它可以让我们的用户参与其中并与朋友的餐厅评论和图片保持同步,甚至可以通过这个获取餐厅提供的优惠和折扣。开始我们选择 Redis 作为消息 Feed 流的存储引擎,因为在当时的用户场景这是最好的选择。但是随着业务的发展,我们需要更高的可用性和负载支持,而 Redis 不能很好的满足这个需求。虽然我们可以通过丢失一些数据来避免系统的中断,但这不是我们想做的事情。为了确保我们的系统具有高可用性,我们不得不放弃 Redis,而选择 Cassandra 作为其替代品。

Cassandra 非常适合这个用例,因为它是分布式的,就有高可用性等。并且 Cassandra 也可以用于存储时间序列数据 – 这实际上就是我们的Feed 流。在做出这一重大改变之前,我们确实有一些 Cassandra 的使用经验,但对于像 Feed 这样重要的东西来说肯定是不够的。我们必须弄清楚如何顺利的从 Redis 过渡到 Cassandra,并像在 Redis 上那样有效地运行 Feed,并且没有停机时间。

我们开始花时间在 Cassandra 上,在前两周深入探索其配置并调整它以满足我们的要求。接下来,在最终确定 Feed 的架构之前,我们明确了一下两个情况:

  • Feed 流信息一般只用于读取而基本上不会修改。使用 Redis 的时候,我们可以同时读取上百个 keys 而不必担心读取延迟,但是对于Cassandra 而言,连接延迟可能是读取请求过程中一个相当重要的部分。
  • schema 需要足够灵活,以便将来允许 Feed 中新类型的数据。鉴于我们不断迭代并致力于丰富产品体验,因此在 Feed 中添加元素和功能几乎是不可避免的。

我们花了几天时间用于收集了我们项目的数据模式以及各种用户案例,然后开始使用2个数据中心,每个数据中心有3个节点。 我们从 Redis 中迁移大概 6000万条记录到 Cassandra 中用于测试其性能。由于是测试阶段,我们只将一部分流量切入到 Cassandra ,并准备了两个版本的代码,分别写入到 Cassandra 和 Redis 。架构图如下

我们监控系统的延迟和其他问题,令人惊讶的是,我们遇到了写入吞吐量的瓶颈问题。 我们知道 Cassandra 的写入能力非常强,但是我们无法实现我们在各种博客文章和文章中阅读的写入吞吐量。 我们知道出了什么问题,但我们不知道是什么。我们从三个节点中获得的最佳结果是每秒1500次写入,这完全不能满足线上的需求,我们不得不在几个小时内回滚并重新评估。

经过几天的排查,我们意识到问题不在于 Cassandra,而在于 Elastic Block Store(EBS)。EBS是安装在每个EC2实例上的网络驱动器,具有10 Gigabits 的共享带宽和网络流量。当在单个EC2实例上的所有用户之间共享时,该带宽成为我们的瓶颈。为了满足这一需求,我们将数据从基于网络的EBS存储移动到同一EC2实例中的磁盘存储。然后我们在每个服务器上逐个部署由 Cassandra 提供支持的新 Food Feed,以便我们控制吞吐量。很高兴的是,这次成功了。

然后我们开始从我们的生产 Redis 服务器迁移数据(我们花了14个小时来迁移所有内容),在迁移过程中我们没有任何故障或额外负载。这就是 Redis 和 Cassandra 的强大功能。今天,我们的 Food Feed 完全运行在 Cassandra 上,我们在没有停机的情况下完成了这项工作。新的架构如下:

总而言之,通过上面这个项目,我们学到了以下几点:

  • 在写入期间避免数据的读取。“读取”吞吐量大致保持不变,而“写入”规模与节点数量成比例;
  • 避免数据的删除。删除意味着压缩(compaction),当它运行时,节点的资源会被占用;
  • 延迟是一个问题。与Redis相比,Cassandra的连接延迟很高,大约是 Redis 的10x-15x。

作者:明惠

原文链接

本文为云栖社区原创内容,未经允许不得转载。

 

DLL文件和LIB文件生成(关于sqlite数据库)

mumupudding阅读(7)

SQLite是一个开源的跨平台的轻型数据库,WINCE本身也有一个自带的数据库SQLCE ,但占用的资源会比较大。最近项目中考虑用到 SQLite,因此特别研究了一下,下面介绍一下具体的移植方法。

一、下载SQLite源码

       去SQLite官网http://www.sqlite.org/download.htm下载最新的source code。我下载的是sqlite-amalgamation-3071401.zip。解压后会得得到四个文件(shell.c、sqlite3.c、sqlite3.h、sqlite3ext.h)。

二、编译生成WINCE下DLL

      1. 在VS2005下新建一个Win32智能设备项目,选择相应的SDK,并选择应用程序类型为DLL。

      2. 将sqlite-amalgamation-3071401.zip解压后的sqlite3.c、sqlite3.h文件拷贝到工程目录下并添加至工程目录。如下图所示。

            

       3.在预处理中增加SQLITE_ENABLE_RTREE和SQLITE_ENABLE_COLUMN_METADATA宏定义。如图所示:

           

        4.编译项目,会提示“error LNK2019: 无法解析的外部符号 localtime_s,该符号在函数 osLocaltime 中被引用”错误,此错误的解决方法是将localtime_s替换成_localtime64_s即可。

        5.编译成功后会发现目录下会生成SQLite.dll文件,但会发现没有lib文件,这样在使用这个DLL时是没法编译的,所以还需要导出.lib文件,解决方法是在SQLite官网上下载sqlite-dll-win32-x86-XXXXXX.zip文件,解压后将目录下的sqlite3.def文件拷贝到DLL工程目录,在项目属性–链接器–输入–模块定义文件中添加sqlite3.def,如下图所示,然后编译即可。

           

这样SQLite的编译工作就大功告成了。

三、WINCE 下SQLite测试

        新建一个SQLite测试程序,测试代码如下:       

 

[cpp] view plaincopy

  1. #include <windows.h>  
  2.   
  3. // sqlite3的回调函数  
  4. int SQLiteQueryResultCallBack( void * para, int n_column, char ** column_value, char ** column_name )  
  5.   
  6. int main(int argc,char* argv[])  
  7. {  
  8.     sqlite3 * db = NULL; //声明sqlite关键结构指针  
  9.     int result;  
  10.     // 打开或创建数据库  
  11.     result = sqlite3_open(“NAND2\\sqlite.db”, &db );  
  12.     if( result != SQLITE_OK )  
  13.     {  
  14.         //数据库打开失败  
  15.         return -1;  
  16.     }  
  17.     char * errmsg = NULL;  
  18.     // 数据库操作代码  
  19. #if 1  
  20.     // 创建一个测试表,表名叫 MyTable,有2个字段: ID 和 name。其中ID是一个自动增加的类型,以后insert时可以不去指定这个字段,它会自己从0开始增加  
  21.     result = sqlite3_exec( db, “create table MyTable( ID integer primary key autoincrement, name nvarchar(32) )”, NULL, NULL, &errmsg );  
  22.     if(result != SQLITE_OK )  
  23.     {  
  24.         printf(“创建表失败,错误码:%d,错误原因:%s\n”, result, errmsg );  
  25.     }  
  26.     // 插入记录  
  27.     result = sqlite3_exec( db, “insert into MyTable( name ) values ( ‘张三’ )”, 0, 0, &errmsg );  
  28.     if(result != SQLITE_OK )  
  29.     {  
  30.         printf(“插入记录失败,错误码:%d,错误原因:%s\n”, result, errmsg );  
  31.     }  
  32.     // 插入记录  
  33.     result = sqlite3_exec( db, “insert into MyTable( name ) values ( ‘李四’ )”, 0, 0, &errmsg );  
  34.     if(result != SQLITE_OK )  
  35.     {  
  36.         printf(“插入记录失败,错误码:%d,错误原因:%s\n”, result, errmsg );  
  37.     }  
  38. #endif  
  39.     // 开始查询数据库  
  40.     result = sqlite3_exec( db, “select * from MyTable”, SQLiteQueryResultCallBack, NULL, &errmsg );  
  41.     // 关闭数据库  
  42.     sqlite3_close( db );  
  43.   
  44.     return 0;  
  45. }  
  46.   
  47. // sqlite3的回调函数  
  48. int SQLiteQueryResultCallBack( void * para, int n_column, char ** column_value, char ** column_name )  
  49. {  
  50.     printf( “******************************\n” );  
  51.     printf(“记录包含 %d 个字段\n”, n_column );  
  52.     for(int i = 0 ; i < n_column; i ++ )  
  53.     {  
  54.         printf( “字段名:%s 字段值:%s\n”, column_name[i], column_value[i] );  
  55.     }  
  56.     printf( “******************************\n” );  
  57.     return 0;  
  58. }  
 
  1. #include <windows.h>

  2.  
  3. // sqlite3的回调函数

  4. int SQLiteQueryResultCallBack( void * para, int n_column, char ** column_value, char ** column_name )

  5.  
  6. int main(int argc,char* argv[])

  7. {

  8. sqlite3 * db = NULL; //声明sqlite关键结构指针

  9. int result;

  10. // 打开或创建数据库

  11. result = sqlite3_open("NAND2\\sqlite.db", &db );

  12. if( result != SQLITE_OK )

  13. {

  14. //数据库打开失败

  15. return -1;

  16. }

  17. char * errmsg = NULL;

  18. // 数据库操作代码

  19. #if 1

  20. // 创建一个测试表,表名叫 MyTable,有2个字段: ID 和 name。其中ID是一个自动增加的类型,以后insert时可以不去指定这个字段,它会自己从0开始增加

  21. result = sqlite3_exec( db, "create table MyTable( ID integer primary key autoincrement, name nvarchar(32) )", NULL, NULL, &errmsg );

  22. if(result != SQLITE_OK )

  23. {

  24. printf("创建表失败,错误码:%d,错误原因:%s\n", result, errmsg );

  25. }

  26. // 插入记录

  27. result = sqlite3_exec( db, "insert into MyTable( name ) values ( '张三' )", 0, 0, &errmsg );

  28. if(result != SQLITE_OK )

  29. {

  30. printf("插入记录失败,错误码:%d,错误原因:%s\n", result, errmsg );

  31. }

  32. // 插入记录

  33. result = sqlite3_exec( db, "insert into MyTable( name ) values ( '李四' )", 0, 0, &errmsg );

  34. if(result != SQLITE_OK )

  35. {

  36. printf("插入记录失败,错误码:%d,错误原因:%s\n", result, errmsg );

  37. }

  38. #endif

  39. // 开始查询数据库

  40. result = sqlite3_exec( db, "select * from MyTable", SQLiteQueryResultCallBack, NULL, &errmsg );

  41. // 关闭数据库

  42. sqlite3_close( db );

  43.  
  44. return 0;

  45. }

  46.  
  47. // sqlite3的回调函数

  48. int SQLiteQueryResultCallBack( void * para, int n_column, char ** column_value, char ** column_name )

  49. {

  50. printf( "******************************\n" );

  51. printf("记录包含 %d 个字段\n", n_column );

  52. for(int i = 0 ; i < n_column; i ++ )

  53. {

  54. printf( "字段名:%s 字段值:%s\n", column_name[i], column_value[i] );

  55. }

  56. printf( "******************************\n" );

  57. return 0;

  58. }

四、SQLite可视化管理工具

 

 

 

        SQLite本身没有可视化管理工具,只提供了一个命令行的管理工具SQLite.exe。有一个第三方的可视化管理工具Sqlite Expert,用着还可以,下载地址: http://www.sqliteexpert.com/download.html

Synchronized 偏向锁、轻量级锁、自旋锁、锁消除

mumupudding阅读(13)

 

 

一、重量级锁

  上篇文章中向大家介绍了Synchronized的用法及其实现的原理。现在我们应该知道,Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。因此,这种依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”。JDK中对Synchronized做的种种优化,其核心都是为了减少这种重量级锁的使用。JDK1.6以后,为了减少获得锁和释放锁所带来的性能消耗,提高性能,引入了“轻量级锁”和“偏向锁”。

 

二、轻量级锁 

  锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。JDK 1.6中默认是开启偏向锁和轻量级锁的,我们也可以通过-XX:-UseBiasedLocking来禁用偏向锁。锁的状态保存在对象的头文件中,以32位的JDK为例:

锁状态

25 bit

4bit

1bit

2bit

23bit

2bit

是否是偏向锁

锁标志位

轻量级锁

指向栈中锁记录的指针

00

重量级锁

指向互斥量(重量级锁)的指针

10

GC标记

11

偏向锁

线程ID

Epoch

对象分代年龄

1

01

无锁

对象的hashCode

对象分代年龄

0

01

  “轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的。但是,首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。在解释轻量级锁的执行过程之前,先明白一点,轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。

1轻量级锁的加锁过程

  (1)在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。这时候线程堆栈与对象头的状态如图2.1所示。

  (2)拷贝对象头中的Mark Word复制到锁记录中。

  (3)拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤(3),否则执行步骤(4)。

  (4)如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如图2.2所示。

  (5)如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。

Synchronized 偏向锁、轻量级锁、自旋锁、锁消除

2、轻量级锁的解锁过程:

  (1)通过CAS操作尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word。

  (2)如果替换成功,整个同步过程就完成了。

  (3)如果替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程。

三、偏向锁

  引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的CAS原子指令的性能消耗)。上面说过,轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能。

1、偏向锁获取过程:

  (1)访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01——确认为可偏向状态。

  (2)如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤(5),否则进入步骤(3)。

  (3)如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行(5);如果竞争失败,执行(4)。

  (4)如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。

  (5)执行同步代码。

2、偏向锁的释放:

  偏向锁的撤销在上述第四步骤中有提到偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。

3、重量级锁、轻量级锁和偏向锁之间转换

Synchronized 偏向锁、轻量级锁、自旋锁、锁消除

四、其他优化 

1适应性自旋(Adaptive Spinning):从轻量级锁获取的流程中我们知道当线程在获取轻量级锁的过程中执行CAS操作失败时,是要通过自旋来获取重量级锁的。问题在于,自旋是需要消耗CPU的,如果一直获取不到锁的话,那该线程就一直处在自旋状态,白白浪费CPU资源。解决这个问题最简单的办法就是指定自旋的次数,例如让其循环10次,如果还没获取到锁就进入阻塞状态。但是JDK采用了更聪明的方式——适应性自旋,简单来说就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。

2、锁粗化(Lock Coarsening):锁粗化的概念应该比较好理解,就是将多次连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成一个范围更大的锁。举个例子:

package com.paddx.test.string;
  
  public class StringBufferTest {
      StringBuffer stringBuffer = new StringBuffer();
  
      public void append(){
          stringBuffer.append("a");
          stringBuffer.append("b");
          stringBuffer.append("c");
     }
 }

这里每次调用stringBuffer.append方法都需要加锁和解锁,如果虚拟机检测到有一系列连串的对同一个对象加锁和解锁操作,就会将其合并成一次范围更大的加锁和解锁操作,即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁。

3、锁消除(Lock Elimination):锁消除即删除不必要的加锁操作。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么可以认为这段代码是线程安全的,不必要加锁。看下面这段程序:

package com.paddx.test.concurrent;
  
  public class SynchronizedTest02 {
  
      public static void main(String[] args) {
          SynchronizedTest02 test02 = new SynchronizedTest02();
          //启动预热
          for (int i = 0; i < 10000; i++) {
              i++;
         }
         long start = System.currentTimeMillis();
         for (int i = 0; i < 100000000; i++) {
             test02.append("abc", "def");
         }
         System.out.println("Time=" + (System.currentTimeMillis() - start));
     }
 
     public void append(String str1, String str2) {
         StringBuffer sb = new StringBuffer();
         sb.append(str1).append(str2);
     }
 }

虽然StringBuffer的append是一个同步方法,但是这段程序中的StringBuffer属于一个局部变量,并且不会从该方法中逃逸出去,所以其实这过程是线程安全的,可以将锁消除。下面是我本地执行的结果:

为了尽量减少其他因素的影响,这里禁用了偏向锁(-XX:-UseBiasedLocking)。通过上面程序,可以看出消除锁以后性能还是有比较大提升的。

五、总结 

  本文重点介绍了JDk中采用轻量级锁和偏向锁等对Synchronized的优化,但是这两种锁也不是完全没缺点的,比如竞争比较激烈的时候,不但无法提升效率,反而会降低效率,因为多了一个锁升级的过程,这个时候就需要通过-XX:-UseBiasedLocking来禁用偏向锁。下面是这几种锁的对比:

优点

缺点

适用场景

偏向锁

加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。

如果线程间存在锁竞争,会带来额外的锁撤销的消耗。

适用于只有一个线程访问同步块场景。

轻量级锁

竞争的线程不会阻塞,提高了程序的响应速度。

如果始终得不到锁竞争的线程使用自旋会消耗CPU。

追求响应时间。

同步块执行速度非常快。

重量级锁

线程竞争不使用自旋,不会消耗CPU。

线程阻塞,响应时间缓慢。

追求吞吐量。

同步块执行速度较长。

 

Linux系统中五款好用的日志分析工具

mumupudding阅读(7)

监控网络活动是一项繁琐的工作,但有充分的理由这样做。例如,它允许你查找和调查工作站和连接到网络的设备及服务器上的可疑登录,同时确定管理员滥用了什么。你还可以跟踪软件安装和数据传输,以实时识别潜在问题,而不是在损坏发生后才进行跟踪。
这些日志还有助于使你的公司遵守适用于在欧盟范围内运营的任何实体的通用数据保护条例(GFPR)。如果你的网站在欧盟可以浏览,那么你就有遵守的该条例的资格。
日志记录,包括跟踪和分析,应该是任何监控基础设置中的一个基本过程。要从灾难中恢复 SQL Server 数据库,需要事务日志文件。此外,通过跟踪日志文件,DevOps 团队和数据库管理员(DBA)可以保持最佳的数据库性能,又或者,在网络攻击的情况下找到未经授权活动的证据。因此,定期监视和分析系统日志非常重要。这是一种重新创建导致出现任何问题的事件链的可靠方式。
现在有很多开源日志跟踪器和分析工具可供使用,这使得为活动日志选择合适的资源比你想象的更容易。自由和开源软件社区提供的日志设计适用于各种站点和操作系统。以下是五个我用过的最好的工具,它们并没有特别的顺序。

Graylog

Graylog 于 2011 年在德国创立,现在作为开源工具或商业解决方案提供。它被设计成一个集中式日志管理系统,接受来自不同服务器或端点的数据流,并允许你快速浏览或分析该信息。
Graylog 在系统管理员中有着良好的声誉,因为它易于扩展。大多数 Web 项目都是从小规模开始的,但它们可能指数级增长。Graylog 可以均衡后端服务网络中的负载,每天可以处理几 TB 的日志数据。
IT 管理员会发现 Graylog 的前端界面易于使用,而且功能强大。Graylog 是围绕仪表板的概念构建的,它允许你选择你认为最有价值的指标或数据源,并快速查看一段时间内的趋势。
当发生安全或性能事件时,IT 管理员希望能够尽可能地根据症状追根溯源。Graylog 的搜索功能使这变得容易。它有内置的容错功能,可运行多线程搜索,因此你可以同时分析多个潜在的威胁。
最好用的开源日志分析工具最好用的开源日志分析工具

Nagios

Nagios 始于 1999 年,最初是由一个开发人员开发的,现在已经发展成为管理日志数据最可靠的开源工具之一。当前版本的 Nagios 可以与运行 Microsoft Windows、Linux 或 Unix 的服务器集成。
它的主要产品是日志服务器,旨在简化数据收集并使系统管理员更容易访问信息。Nagios 日志服务器引擎将实时捕获数据,并将其提供给一个强大的搜索工具。通过内置的设置向导,可以轻松地与新端点或应用程序集成。
Nagios 最常用于需要监控其本地网络安全性的组织。它可以审核一系列与网络相关的事件,并帮助自动分发警报。如果满足特定条件,甚至可以将 Nagios 配置为运行预定义的脚本,从而允许你在人员介入之前解决问题。
作为网络审计的一部分,Nagios 将根据日志数据来源的地理位置过滤日志数据。这意味着你可以使用地图技术构建全面的仪表板,以了解 Web 流量是如何流动的。
最好用的开源日志分析工具最好用的开源日志分析工具

Elastic Stack (ELK Stack)

Elastic Stack,通常称为 ELK Stack,是需要筛选大量数据并理解其日志系统的组织中最受欢迎的开源工具之一(这也是我个人的最爱)。
它的主要产品由三个独立的产品组成:Elasticsearch、Kibana 和 Logstash:
顾名思义, Elasticsearch 旨在帮助用户使用多种查询语言和类型在数据集之中找到匹配项。速度是它最大的优势。它可以扩展成由数百个服务器节点组成的集群,轻松处理 PB 级的数据。
Kibana 是一个可视化工具,与 Elasticsearch 一起工作,允许用户分析他们的数据并构建强大的报告。当你第一次在服务器集群上安装 Kibana 引擎时,你会看到一个显示着统计数据、图表甚至是动画的界面。
ELK Stack 的最后一部分是 Logstash,它作为一个纯粹的服务端管道进入 Elasticsearch 数据库。你可以将 Logstash 与各种编程语言和 API 集成,这样你的网站和移动应用程序中的信息就可以直接提供给强大的 Elastic Stalk 搜索引擎中。
ELK Stack 的一个独特功能是,它允许你监视构建在 WordPress 开源网站上的应用程序。与跟踪管理日志和 PHP 日志的大多数开箱即用的安全审计日志工具相比,ELK Stack 可以筛选 Web 服务器和数据库日志。
糟糕的日志跟踪和数据库管理是导致网站性能不佳的最常见原因之一。没有定期检查、优化和清空数据库日志,不仅会降低站点的运行速度,还可能导致其完全崩溃。因此,ELK Stack 对于每个 WordPress 开发人员的工具包来说都是一个优秀的工具。
最好用的开源日志分析工具最好用的开源日志分析工具

LOGalyze

LOGalyze 是一个位于匈牙利的组织,它为系统管理员和安全专家构建开源工具,以帮助他们管理服务器日志,并将其转换为有用的数据点。其主要产品可供个人或商业用户免费下载。
LOGalyze 被设计成一个巨大的管道,其中多个服务器、应用程序和网络设备可以使用简单对象访问协议(SOAP)方法提供信息。它提供了一个前端界面,管理员可以登录界面来监控数据集并开始分析数据。
在 LOGalyze 的 Web 界面中,你可以运行动态报告,并将其导出到 Excel 文件、PDF 文件或其他格式。这些报告可以基于 LOGalyze 后端管理的多维统计信息。它甚至可以跨服务器或应用程序组合数据字段,借此来帮助你发现性能趋势。
LOGalyze 旨在不到一个小时内完成安装和配置。它具有预先构建的功能,允许它以法律所要求的格式收集审计数据。例如,LOGalyze 可以很容易地运行不同的 HIPAA 报告,以确保你的组织遵守健康法律并保持合规性。
最好用的开源日志分析工具最好用的开源日志分析工具

Fluentd

如果你所在组织的数据源位于许多不同的位置和环境中,那么你的目标应该是尽可能地将它们集中在一起。否则,你将难以监控性能并防范安全威胁。
Fluentd 是一个强大的数据收集解决方案,它是完全开源的。它没有提供完整的前端界面,而是作为一个收集层来帮助组织不同的管道。Fluentd 在被世界上一些最大的公司使用,但是也可以在较小的组织中实施。
Fluentd 最大的好处是它与当今最常用的技术工具兼容。例如,你可以使用 Fluentd 从 Web 服务器(如 Apache)、智能设备传感器和 MongoDB 的动态记录中收集数据。如何处理这些数据完全取决于你。
Fluentd 基于 JSON 数据格式,它可以与由卓越的开发人员创建的 500 多个插件一起使用。这使你可以将日志数据扩展到其他应用程序中,并通过最少的手工操作从中获得更好的分析。
最好用的开源日志分析工具最好用的开源日志分析工具

Java开发中存在这样的代码,反而影响整体整洁和可读性

mumupudding阅读(4)

不完美的库类

不完美的库类(Incomplete Library Class)

当一个类库已经不能满足实际需要时,你就不得不改变这个库(如果这个库是只读的,那就没辙了)。

问题原因

许多编程技术都建立在库类的基础上。库类的作者没用未卜先知的能力,不能因此责怪他们。麻烦的是库往往构造的不够好,而且往往不可能让我们修改其中的类以满足我们的需要。

解决方法

  • 如果你只想修改类库的一两个函数,可以运用 引入外加函数(Introduce Foreign Method)
  • 如果想要添加一大堆额外行为,就得运用 引入本地扩展(Introduce Local Extension) 。

收益

  • 减少代码重复(你不用一言不合就自己动手实现一个库的全部功能,代价太高)

何时忽略

  • 如果扩展库会带来额外的工作量。

重构方法说明

引入外加函数(Introduce Foreign Method)

问题

你需要为提供服务的类增加一个函数,但你无法修改这个类。

class Report {
  //...
  void sendReport() {
    Date nextDay = new Date(previousEnd.getYear(),
      previousEnd.getMonth(), previousEnd.getDate() + 1);
    //...
  }
}

解决

在客户类中建立一个函数,并一个第一个参数形式传入一个服务类实例。

class Report {
  //...
  void sendReport() {
    Date newStart = nextDay(previousEnd);
    //...
  }
  private static Date nextDay(Date arg) {
    return new Date(arg.getYear(), arg.getMonth(), arg.getDate() + 1);
  }
}

引入本地扩展(Introduce Local Extension)

问题

你需要为服务类提供一些额外函数,但你无法修改这个类。

Java开发中存在这样的代码,反而影响整体整洁和可读性

解决

建立一个新类,使它包含这些额外函数,让这个扩展品成为源类的子类或包装类。

Java开发中存在这样的代码,反而影响整体整洁和可读性

中间人

中间人(Middle Man)

如果一个类的作用仅仅是指向另一个类的委托,为什么要存在呢?

Java开发中存在这样的代码,反而影响整体整洁和可读性

问题原因

对象的基本特征之一就是封装:对外部世界隐藏其内部细节。封装往往伴随委托。但是人们可能过度运用委托。比如,你也许会看到一个类的大部分有用工作都委托给了其他类,类本身成了一个空壳,除了委托之外不做任何事情。

解决方法

应该运用 移除中间人(Remove Middle Man),直接和真正负责的对象打交道。

收益

  • 减少笨重的代码。

Java开发中存在这样的代码,反而影响整体整洁和可读性

何时忽略

如果是以下情况,不要删除已创建的中间人:

  • 添加中间人是为了避免类之间依赖关系。
  • 一些设计模式有目的地创建中间人(例如代理模式和装饰器模式)。

重构方法说明

移除中间人(Remove Middle Man)

问题

某个类做了过多的简单委托动作。

Java开发中存在这样的代码,反而影响整体整洁和可读性

解决

让客户直接调用委托类。

Java开发中存在这样的代码,反而影响整体整洁和可读性

依恋情结

依恋情结(Feature Envy)

一个函数访问其它对象的数据比访问自己的数据更多。

Java开发中存在这样的代码,反而影响整体整洁和可读性

问题原因

这种气味可能发生在字段移动到数据类之后。如果是这种情况,你可能想将数据类的操作移动到这个类中。

解决方法

As a basic rule, if things change at the same time, you should keep them in the same place. Usually data and functions that use this data are changed together (although exceptions are possible).

有一个基本原则:同时会发生改变的事情应该被放在同一个地方。通常,数据和使用这些数据的函数是一起改变的。

Java开发中存在这样的代码,反而影响整体整洁和可读性

  • 如果一个函数明显应该被移到另一个地方,可运用 搬移函数(Move Method) 。
  • 如果仅仅是函数的部分代码访问另一个对象的数据,运用 提炼函数(Extract Method) 将这部分代码移到独立的函数中。
  • 如果一个方法使用来自其他几个类的函数,首先确定哪个类包含大多数使用的数据。然后,将该方法与其他数据一起放在此类中。或者,使用 提炼函数(Extract Method) 将方法拆分为几个部分,可以放置在不同类中的不同位置。

收益

  • 减少重复代码(如果数据处理的代码放在中心位置)。
  • 更好的代码组织性(处理数据的函数靠近实际数据)。

Java开发中存在这样的代码,反而影响整体整洁和可读性

何时忽略

  • 有时,行为被有意地与保存数据的类分开。这通常的优点是能够动态地改变行为(见策略设计模式,访问者设计模式和其他模式)。

重构方法说明

搬移函数(Move Method)

问题

你的程序中,有个函数与其所驻类之外的另一个类进行更多交流:调用后者,或被后者调用。

Java开发中存在这样的代码,反而影响整体整洁和可读性

解决

在该函数最常引用的类中建立一个有着类似行为的新函数。将旧函数变成一个单纯的委托函数,或是旧函数完全移除。

Java开发中存在这样的代码,反而影响整体整洁和可读性

提炼函数(Extract Method)

问题

你有一段代码可以组织在一起。

void printOwing() {
  printBanner();

  //print details
  System.out.println("name: " + name);
  System.out.println("amount: " + getOutstanding());
}

解决

移动这段代码到一个新的函数中,使用函数的调用来替代老代码。

void printOwing() {
  printBanner();
  printDetails(getOutstanding());
}

void printDetails(double outstanding) {
  System.out.println("name: " + name);
  System.out.println("amount: " + outstanding);
}

狎昵关系

狎昵关系(Inappropriate Intimacy)

一个类大量使用另一个类的内部字段和方法。

Java开发中存在这样的代码,反而影响整体整洁和可读性

问题原因

类和类之间应该尽量少的感知彼此(减少耦合)。这样的类更容易维护和复用。

解决方法

  • 最简单的解决方法是运用 搬移函数(Move Method) 和 搬移字段(Move Field) 来让类之间斩断羁绊。

 

Java开发中存在这样的代码,反而影响整体整洁和可读性

  • 你也可以看看是否能运用 将双向关联改为单向关联(Change Bidirectional Association to Unidirectional) 让其中一个类对另一个说分手。

  • 如果这两个类实在是情比金坚,难分难舍,可以运用 提炼类(Extract Class) 把二者共同点提炼到一个新类中,让它们产生爱的结晶。或者,可以尝试运用 隐藏委托关系(Hide Delegate) 让另一个类来为它们牵线搭桥。

  • 继承往往造成类之间过分紧密,因为子类对超类的了解总是超过后者的主观愿望,如果你觉得该让这个子类自己闯荡,请运用 以委托取代继承(Replace Inheritance with Delegation) 来让超类和子类分家。

收益

  • 提高代码组织性。
  • 提高代码复用性。

Java开发中存在这样的代码,反而影响整体整洁和可读性

重构方法说明

搬移函数(Move Method)

问题

你的程序中,有个函数与其所驻类之外的另一个类进行更多交流:调用后者,或被后者调用。

Java开发中存在这样的代码,反而影响整体整洁和可读性

解决

在该函数最常引用的类中建立一个有着类似行为的新函数。将旧函数变成一个单纯的委托函数,或是旧函数完全移除。

Java开发中存在这样的代码,反而影响整体整洁和可读性

搬移字段(Move Field)

问题

在你的程序中,某个字段被其所驻类之外的另一个类更多地用到。

Java开发中存在这样的代码,反而影响整体整洁和可读性

解决

在目标类新建一个字段,修改源字段的所有用户,令他们改用新字段。

Java开发中存在这样的代码,反而影响整体整洁和可读性

将双向关联改为单向关联(Change Bidirectional Association to Unidirectional)

问题

两个类之间有双向关联,但其中一个类如今不再需要另一个类的特性。

Java开发中存在这样的代码,反而影响整体整洁和可读性

解决

去除不必要的关联。

Java开发中存在这样的代码,反而影响整体整洁和可读性

提炼类(Extract Class)

问题

某个类做了不止一件事。

Java开发中存在这样的代码,反而影响整体整洁和可读性

解决

建立一个新类,将相关的字段和函数从旧类搬移到新类。

Java开发中存在这样的代码,反而影响整体整洁和可读性

隐藏委托关系(Hide Delegate)

问题

客户通过一个委托类来调用另一个对象。

Java开发中存在这样的代码,反而影响整体整洁和可读性

解决

在服务类上建立客户所需的所有函数,用以隐藏委托关系。

Java开发中存在这样的代码,反而影响整体整洁和可读性

以委托取代继承(Replace Inheritance with Delegation)

问题

某个子类只使用超类接口中的一部分,或是根本不需要继承而来的数据。

Java开发中存在这样的代码,反而影响整体整洁和可读性

解决

在子类中新建一个字段用以保存超类;调整子类函数,令它改而委托超类;然后去掉两者之间的继承关系。

Java开发中存在这样的代码,反而影响整体整洁和可读性

过度耦合的消息链

过度耦合的消息链(Message Chains)

消息链的形式类似于:obj.getA().getB().getC()

Java开发中存在这样的代码,反而影响整体整洁和可读性

问题原因

如果你看到用户向一个对象请求另一个对象,然后再向后者请求另一个对象,然后再请求另一个对象……这就是消息链。实际代码中你看到的可能是一长串 getThis()或一长串临时变量。采取这种方式,意味客户代码将与查找过程中的导航紧密耦合。一旦对象间关系发生任何变化,客户端就不得不做出相应的修改。

解决方法

  • 可以运用 隐藏委托关系(Hide Delegate) 删除一个消息链。

Java开发中存在这样的代码,反而影响整体整洁和可读性

  • 有时更好的选择是:先观察消息链最终得到的对象是用来干什么的。看看能否以 提炼函数(Extract Method)把使用该对象的代码提炼到一个独立函数中,再运用 搬移函数(Move Method) 把这个函数推入消息链。

收益

  • 能减少链中类之间的依赖。
  • 能减少代码量。

Java开发中存在这样的代码,反而影响整体整洁和可读性

何时忽略

  • 过于侵略性的委托可能会使程序员难以理解功能是如何触发的。

重构方法说明

隐藏委托关系(Hide Delegate)

问题

客户通过一个委托类来调用另一个对象。

Java开发中存在这样的代码,反而影响整体整洁和可读性

 

解决

在服务类上建立客户所需的所有函数,用以隐藏委托关系。

Java开发中存在这样的代码,反而影响整体整洁和可读性

提炼函数(Extract Method)

问题

你有一段代码可以组织在一起。

void printOwing() {
  printBanner();

  //print details
  System.out.println("name: " + name);
  System.out.println("amount: " + getOutstanding());
}

解决

移动这段代码到一个新的函数中,使用函数的调用来替代老代码。

void printOwing() {
  printBanner();
  printDetails(getOutstanding());
}

void printDetails(double outstanding) {
  System.out.println("name: " + name);
  System.out.println("amount: " + outstanding);
}

搬移函数(Move Method)

问题

你的程序中,有个函数与其所驻类之外的另一个类进行更多交流:调用后者,或被后者调用。

Java开发中存在这样的代码,反而影响整体整洁和可读性

解决

在该函数最常引用的类中建立一个有着类似行为的新函数。将旧函数变成一个单纯的委托函数,或是旧函数完全移除。

Java开发中存在这样的代码,反而影响整体整洁和可读性

socket协议介绍

mumupudding阅读(5)

  本文主要讲述了Socket协议脚本的基础知识和编写方法,让大家能够在短时间内快速掌握简单的Socket协议脚本的编写方法。
  
  1.socket协议介绍
  
  Socket协议有万能协议之称,很多系统底层都是用的socket协议,用处十分广泛。
  
  1.1 Socket通讯方式说明
  
  与socket通讯有两种方式,一种是建立长连接,建立后不停的发送,接收;另一种是建立短连接,即建立连接发送报文,接收报文关闭连接
  
  1.2 Socket协议发送的buf类型介绍
  
  Send buffer类型分为字符串和xml类型
  
  1.3 Socket协议脚本编写前提:
  
  与项目组沟通,确认是否是socket协议,由项目组提供服务器IP和端口号还有socket协议交易的报文发送及接收报文对,及交易接口文档,了解清楚报文的数据长度,参数化字段,结构,代表什么等,了解清楚后进行socket协议脚本的开发。
  
  1.4、Socket协议脚本函数说明及实例:
  
  1)名称 lrs_create_socket();
  
  创建socket连接,添加IP和端口号,如果创建成功返回值为0,反之则返回为非0数值。(对于长连接,建立socket连接放在vuser_init函数中,短连接放在Action中即可) 实例: lrs_create_socket(“socket0″,”TCP”,”RemoteHost=180.170.150.230:7700″,  LrsLastArg);
  
  2)名称 lrs_send();
  
  发送socket请求消息,取缓冲区buf0的报文并发送。
  
  实例: lrs_send(“socket0″,”buf1”,LrsLastArg);
  
  3)名称 lrs_receive();
  
  接收socket的响应报文,放置buf1中。
  
  实例:lrs_receive(“socket0″,”buf2”,LrsLastArg);
  
  4)名称 lrs_get_last_received_buffer();
  
  获取最后收到的buffer和大小,其中将最后收到的buffer的值赋给RecvBuf变量,将大小赋值给RecvLen。
  
  实例:   lrs_get_last_received_buffer(“socket0”,&recvBuf,&recvLen);
  
  5)名称 lrs_free_buffer();
  
  为防止内存泄露,释放内存空间。
  
  实例:  lrs_free_buffer(recvBuf);
  
  6)名称 lrs_close_socket();
  
  关闭Socket连接,(对于长连接,关闭socket连接应放在vuser_end函数中)
  
  实例:  lrs_close_socket(“socket0”);
  
  其他常用的Socket函数:
  
  lrs_set_send_buffer(“socket0”, sSendPkg, iLenOfPkg );//指定要发送的socket信息
  
  lrs_get_buffer_by_name(“buf0”, sSendPkg, iLenOfPkg);// 获取收到的buffer和大小
  
  lrs_length_send(“socket0″,”buf0″,1,”Size=4″,”Encoding=1”,LrsLastArg);
  
  关联函数:
  
  lrs_save_param_ex(“socket0″,”received”,””,151,7,”ascii”,”response”);//取指定位置字符串保存到变量,以便判断事务是否成功
  
  lrs_save_searched_string();//在指定位置搜索字符串,将出现的字符串报错到参数中
  
  超时函数
  
  lrs_set_connect_timeout();//设置连接超时时间
  
  lrs_set_recv_timeout();//设置服务器响应超时时间
  
  lrs_set_recv_timeout2();//设置接收超时时间,使系统不去检查回收的内容是否一致
  
  2、Socket脚本编写
  
  2.1 简单划分步骤
  
  这种方法是我无意在一片文章中看到的,总体说来,比较简单。就像把大象放进冰箱一样,总共分三步:
  
  第一步:把冰箱门打开
  
  //建立到服务端的连接
  
  rc =    lrs_create_socket(“socket0”, “TCP”, “LocalHost=0”, “RemoteHost=128.64.64.23:8988”, LrsLastArg);
  
  if (rc==0)
  
  lr_output_message(“Socket  was successfully created “);
  
  else
  
  lr_output_message(“An error occurred while creating the socket, Error Code: %d”, rc);
  
  第二步:把大象装进去
  
  lrs_send(“socket0”, “buf0”, LrsLastArg);   //往”socket0″发送”buf0″中的数据
  
  lrs_receive(“socket0”, “buf1”, LrsLastArg);//将”socke0″中返回的数据存放到”buf1″中
  
  第三步:把冰箱门带上
  
  //关闭连接
  
  lrs_close_socket(“socket0”);
  
  2.2 详细划分步骤
  
  ◇变量的声明与定义
  
  ◇ 创建socket连接
  
  ◇ 发送socket请求
  
  ◇ 接收socket响应
  
  ◇ 从返回Buffer 中抽取结果
  
  ◇ 结果判断
  
  ◇ 释放内存
  
  ◇ 断开连接。
  
  2.3 实例脚本
  
  下面我们就是用一个实际项目不同报文格式的脚本进行讲解;
  
  若项目是短连接,且报文不是从文件中读取信息时,vuser_init和vuser_end部分默认即可,主要在Action中开发测试脚本和在data.ws中传输数据到Action的代码中。
  
  Vuser_init.c
  
  #include   “lrs.h”
  
  vuser_init()
  
  {
  
  lrs_startup(257);
  
  return 0;
  
  }
  
  Action.c
  
  #include “lrs.h”
  
  Action()
  
  {
  
  int rc,rv;//保存连接成功返回值
  
  char *recvBuf;//保存接收数据的内容
  
  int recvLen;//保存接收数据的大小
  
  /*对于长连接,建立socket连接放在vuser_init函数中,短连接放在Action中即可*/
  
  rc=lrs_create_socket(“socket0″,”TCP”,”RemoteHost=IP:端口”,  LrsLastArg);
  
  //判断连接是否创建成功
  
  if(rc==0){
  
  lr_output_message(“Socket连接创建成功”);
  
  }
  
  else{
  
  lr_error_message(“Socket连接创建失败!错误码=%d”,rc);
  
  return -1;
  
  }
  
  lr_start_transaction(“XXXX_1234_FCX”);//事务开始
  
  //发送socket请求消息(数据包内容放在data.ws中)
  
  lrs_send(“socket0”, “buf0”, LrsLastArg); //取缓冲区buf0的报文并发送
  
  rv = lrs_receive(“socket0”, “buf1”, LrsLastArg);//接收响应报文
  
  if(rv==0){
  
  lr_output_message(“Socket接收返回消息成功”);
  
  }
  
  else{
  
  lr_error_message(“Socket接收返回消息失败!错误码=%d”,rv);
  
  return -1;
  
  }
  
  //获取最后收到的buffer和大小
  
  lrs_get_last_received_buffer(“socket0”,&recvBuf,&recvLen);
  
  /*设置检查点,验证返回数据是否成功,这个根据各交易具体情况进行判断,以下示例是通过返回报文的长度大于3即为成功*/
  
  if(recvLen>3){
  
  lr_end_transaction(“XXXX_1234_FCX “,PASS);
  
  }
  
  else{
  
  lr_end_transaction(“XXXX_1234_FCX “,FAIL);
  
  lr_error_message(“XXXX_8550_FCX Fail!出错信息:[%s]”, recvBuf);//交易失败时,输出RecvBuf返回信息,用于排查出错原因
  
  }
  
  lrs_free_buffer(recvBuf); //释放recvBuf内存空间,否则会引起内存泄露
  
  /*关闭Socket连接,对于长连接,关闭socket连接应放在vuser_end函数中*/
  
  lrs_close_socket(“socket0”);
  
  return 0;
  
  AnnotationConfigApplicationContext context=new AnnotationConfigApplicationContext(JobService.class);
  
  for (String beanname:context.getBeanDefinitionNames())
  
  {
  
  System.out.println(“——–“+beanname);
  
  }
  
  System.out.println(“context.getBean(JobService.class) = ” + context.getBean(JobService.class));
  
  复制代码
  
  这点代码很简单  初始化bean,然后再来拿bean,我们点进AnnotationConfigApplicationContext来看
  
  复制代码
  
  public AnnotationConfigApplicationContext(Class<?>… annotatedClasses)
  
  {
  
  this();
  
  register(annotatedClasses);
  
  refresh();
  
  }
  
  复制代码
  
  进⼊之后 会调用 this()默认无参构造方法
  
  public AnnotationConfigApplicationContext() {
  
  this.reader = new AnnotatedBeanDefinitionReader(this);
  
  this.scanner = new www.tianjiuyule178.com  ClassPathBeanDefinitionScanner(this);
  
  }
  
  调⽤这个⽆参构造⽅法的同时 他会调用⽗类的构造方法,在调用父类构造⽅方法时 他new了一个对象
  
  public GenericApplicationContext() {
  
  this.beanFactory = new DefaultListableBeanFactory();
  
  }
  
  也就是 DefaultListableBeanFactory,当然 这个就是所谓我们平常所说的 bean工厂,其父类就是 BeanFactory,BeanFactory有很多子类,DefaultListableBeanFactory就是其中一个⼦类。 那么 bean的⽣命周期是围绕那个⽅法呢,就是refresh()⽅法。也就是bean的整个生命周期是围绕refresh() 来进行的
  
  在refresh()我们可以看到
  
  复制代码
  
  public void refresh() throws BeansException, IllegalStateException {
  
  synchronized (this.startupShutdownMonitor)www.dayuzaixianyL.cn {
  
  // 准备好刷新上下文.
  
  prepareRefresh();
  
  // 返回一个Factory 为什么需要返回一个工厂  因为要对工厂进行初始化
  
  ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
  
  // 准备bean工厂,以便在此上下文中使用。
  
  prepareBeanFactory(beanFactory);
  
  try {
  
  // 允许在上下文子类中对bean工厂进行后处理。 在spring5  并未对此接口进行实现
  
  postProcessBeanFactory(beanFactory);
  
  // 在spring的环境中去执行已经被注册的 Factory processors
  
  //设置执行自定义的postProcessBeanFactory和spring内部自己定义的
  
  invokeBeanFactoryPostProcessors(beanFactory);
  
  // 注册postProcessor
  
  registerBeanPostProcessors(beanFactory);
  
  // 初始化此上下文的消息源。
  
  initMessageSource();
  
  // 初始化此上下文的事件多播程序。
  
  initApplicationEventMulticaster();
  
  // 在特定上下文子类中初始化其他特殊bean。
  
  onRefresh();
  
  //检查侦听器bean并注册它们。
  
  registerListeners();
  
  // 实例化所有剩余的(非懒加载)单例。
  
  //new 单例对象
  
  finishBeanFactoryInitialization(beanFactory);
  
  // 最后一步:发布相应的事件
  
  finishRefresh();
  
  }
  
  catch (BeansException ex) {
  
  if (logger.isWarnEnabled()) {
  
  logger.warn(“Exception encountered during context initialization – ” +
  
  ”cancelling refresh attempt: ” + ex);
  
  }
  
  // Destroy already created singletons to avoid dangling resources.
  
  destroyBeans(www.hnxinhe.cn);
  
  // Reset ‘active’ flag.
  
  cancelRefresh(ex);
  
  // Propagate exception to caller.
  
  throw ex;
  
  }
  
  finally {
  
  // Reset common introspection caches in Spring’s core, since we
  
  // might not ever need metadata for singleton beans anymore…
  
  resetCommonCaches();
  
  }
  
  }
  
  }
  
  复制代码
  
  那么这里面最重要就是finishBeanFactoryInitialization(beanFactory);这个方法就是描述 spring的一个bean如何初始化
  
  复制代码
  
  protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
  
  // Initialize conversion service for this context.
  
  if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) &&
  
  beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) {
  
  beanFactory.setConversionService(
  
  beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class));
  
  }
  
  // Register a default embedded value resolver if no bean post-processor
  
  // (such as a PropertyPlaceholderConfigurer bean) registered any before:
  
  // at this point, primarily for resolution in annotation attribute values.
  
  if (!beanFactory.hasEmbeddedValueResolver(www.yunyouuyL.com)) {
  
  beanFactory.addEmbeddedValueResolver(strVal -> getEnvironment(www.chenghgongs.com).resolvePlaceholders(strVal));
  
  }
  
  // Initialize LoadTimeWeaverAware beans early to allow for registering their transformers early.
  
  String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false);
  
  for (String weaverAwareName : weaverAwareNames) {
  
  getBean(weaverAwareName);
  
  }
  
  // Stop using the temporary ClassLoader for type matching.
  
  beanFactory.setTempClassLoader(null);
  
  // Allow for caching all bean definition metadata, not expecting further changes.
  
  beanFactory.freezeConfiguration();
  
  // 实例化所有单例对象
  
  beanFactory.preInstantiateSingletons();
  
  }
  
  复制代码
  
  可以看到前面是一些判断 最重要的就是最后一个方法 beanFactory.preInstantiateSingletons();我们看下preInstantiateSingletons()方法,它是ConfigurableListableBeanFactory这个接口的一个方法 我们直接来看这个接口的实现 是由DefaultListableBeanFactory这个类 来实现
  
  复制代码
  
  @Override
  
  public void preInstantiateSingletons() throws BeansException {
  
  if (logger.isDebugEnabled()) {
  
  logger.debug(“Pre-instantiating singletons in www.baihuiyulegw.com ” + this);
  
  }
  
  // Iterate over a copy to allow for init methods which in turn register new bean definitions.
  
  // While this may not be part of the regular factory bootstrap, it does otherwise work fine.
  
  //所有bean的名字
  
  List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);
  
  // Trigger initialization of all non-lazy singleton beans…
  
  for (String beanName : beanNames) {
  
  RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
  
  if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
  
  if (isFactoryBean(beanName)) {
  
  Object bean = getBean(FACTORY_BEAN_PREFIX + beanName);
  
  if (bean instanceof FactoryBean) {
  
  final FactoryBean<?> factory = (FactoryBean<?>) bean;
  
  boolean isEagerInit;
  
  if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) {
  
  isEagerInit = AccessController.doPrivileged((PrivilegedAction<Boolean>)
  
  ((SmartFactoryBean<www.tianscpt.com>) factory)::isEagerInit,
  
  getAccessControlContext());
  
  }
  
  else {
  
  isEagerInit = (factory instanceof SmartFactoryBean &&
  
  ((SmartFactoryBean<?>) factory).isEagerInit());
  
  }
  
  if (isEagerInit) {
  
  getBean(beanName);
  
  }
  
  }
  
  }
  
  else {
  
  getBean(beanName);
  
  data.ws
  
  1)XML报文格式
  
  ;WSRData 2 1
  
  send  buf0 360
  
  ”<?xmlversion=\”1.0\”encoding=\”GBK\”?>”
  
  ”<TRANINFO>”
  
  ”<HEAD>”
  
  ”<TransCode>S001</TransCode>”
  
  ”<TransDate>20170613</TransDate>”
  
  ”<TransTime>144206</TransTime>”
  
  ”<TransNo>21219603</TransNo>”
  
  ”<Operator>999088</Operator>”
  
  ”<TransInst>70090</TransInst>”
  
  ”</HEAD>”
  
  ”<MSG>”
  
  ”<CustomerID><userID></CustomerID>”//客户编号
  
  ”<Type>3</Type>”//查询类型
  
  ”<BusinessType>01</BusinessType>”//业务类型
  
  ”</MSG>”
  
  ”</TRANINFO>”
  
  recv buf1 300
  
  -1
  
  2)16进制报文格式
  
  ;WSRData 2 1
  
  send  buf0 32
  
  ”\x00\x00\x00\x1c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00″
  
  ” “
  
  ”\x00\x00\x00\x00″
  
  ”PID <tran>”
  
  recv  buf1 197
  
  ”\x00\x00\x00\x1a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00″
  
  ”\x1e\x00\x00\x00\x00″
  
  ”STW -1″
  
  ”\x00\x00\x00\x1e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00″
  
  ”\x1e\x00\x00\x00\x00″
  
  ”STT “
  
  ”\x1f”
  
  -1
  
  其中buf0代表发送的报文的名称,后跟的数字代码发送报文长度,其下放置发送报文;buf1代表接收报文的名称,后跟数字代表接收报文长度,其下放置接收报文。
  
  注意:该协议脚本参数化格式为:<参数名>

使用PriorityQueue实现LFU和LRU

mumupudding阅读(4)

LFU(Least Frequently Used)算法根据数据的历史访问频率来淘汰数据,其核心思想是“如果数据过去被访问多次,那么将来被访问的频率也更高。

然后我先定义一个缓存对象MyCache,这里有一个泛型用来存对象,然后有一个useCount记录被使用次数,接下去是创建时间和最近使用时间。

使用PriorityQueue实现LFU和LRU

使用构造方法来初始化一些值:

使用PriorityQueue实现LFU和LRU

因为使用了Priorityqueue所以要实现Comparable接口,而且更具useCount来比较

使用PriorityQueue实现LFU和LRU

然后我们就可以进入下一步操作啦,定义一个PriorityQueue作为全局变量,这个限制一些最大缓存的数量,还有用一个object用来做同步块,队列中如果没有就加入队列中,这里就先不写set的情况,

使用PriorityQueue实现LFU和LRU

然后根据优先队列的特性来删除哪些不常用的元素,因为根据排序,优先队列会把我们最想要的数据放在头部,所以调用poll方法,就可以实现删除最少使用的那一个,当然这里我可以选择删除固定数量的不常用数据,或者删除一定比例的数据,这个使用中可以用定时器或者分布式任务来清除缓存。

使用PriorityQueue实现LFU和LRU

因为考虑哪些数据相等,我这里重写了hashCode和equals方法,这里只是调用缓存对象的比较,但是对于那些没有重写的缓存对象T,可能就会有问题啦,所以可能有的设计就会用序列化后的数据。

使用PriorityQueue实现LFU和LRU

因为PriorityQueue只是做了一次的排序,所以我需要在用户修改后的数据也要更新顺序,所以我想把原来的数据删了,然后再添加一词,使用次数加1,最近使用时间也更新啦,只是这里我遍历了一边队列,效率比较低,只是说这里先考虑这个最近不常用淘汰,实际过程肯定是查询为主。

使用PriorityQueue实现LFU和LRU

然后我们测试下:

使用PriorityQueue实现LFU和LRU

输出:

使用PriorityQueue实现LFU和LRU

 

LRU(Least recently used)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。

那么我想就是把MyCache里面的排序根据时间来指定就行了,最近最久远的排在队列的前面淘汰掉。

当然这里只是想用PriorityQueue写着玩!

未完待续

https://github.com/woshiyexinjie/algorithm-xin

Java 高并发解决方案(电商的秒杀和抢购)

mumupudding阅读(7)

电商的秒杀和抢购,对我们来说,都不是一个陌生的东西。然而,从技术的角度来说,这对于Web系统是一个巨大的考验。当一个Web系统,在一秒钟内收到数以万计甚至更多请求时,系统的优化和稳定至关重要。这次我们会关注秒杀和抢购的技术实现和优化,同时,从技术层面揭开,为什么我们总是不容易抢到火车票的原因? 

一、大规模并发带来的挑战 

在过去的工作中,我曾经面对过5w每秒的高并发秒杀功能,在这个过程中,整个Web系统遇到了很多的问题和挑战。如果Web系统不做针对性的优化,会轻而易举地陷入到异常状态。我们现在一起来讨论下,优化的思路和方法哈。 

1. 请求接口的合理设计

一个秒杀或者抢购页面,通常分为2个部分,一个是静态的HTML等内容,另一个就是参与秒杀的Web后台请求接口。

通常静态HTML等内容,是通过CDN的部署,一般压力不大,核心瓶颈实际上在后台请求接口上。这个后端接口,必须能够支持高并发请求,同时,非常重要的一点,必须尽可能“快”,在最短的时间里返回用户的请求结果。为了实现尽可能快这一点,接口的后端存储使用内存级别的操作会更好一点。仍然直接面向MySQL之类的存储是不合适的,如果有这种复杂业务的需求,都建议采用异步写入。

当然,也有一些秒杀和抢购采用“滞后反馈”,就是说秒杀当下不知道结果,一段时间后才可以从页面中看到用户是否秒杀成功。但是,这种属于“偷懒”行为,同时给用户的体验也不好,容易被用户认为是“暗箱操作”。

2. 高并发的挑战:一定要“快”

我们通常衡量一个Web系统的吞吐率的指标是QPS(Query Per Second,每秒处理请求数),解决每秒数万次的高并发场景,这个指标非常关键。举个例子,我们假设处理一个业务请求平均响应时间为100ms,同时,系统内有20台Apache的Web服务器,配置MaxClients为500个(表示Apache的最大连接数目)。

那么,我们的Web系统的理论峰值QPS为(理想化的计算方式):

20*500/0.1 = 100000 (10万QPS)

咦?我们的系统似乎很强大,1秒钟可以处理完10万的请求,5w/s的秒杀似乎是“纸老虎”哈。实际情况,当然没有这么理想。在高并发的实际场景下,机器都处于高负载的状态,在这个时候平均响应时间会被大大增加。

就Web服务器而言,Apache打开了越多的连接进程,CPU需要处理的上下文切换也越多,额外增加了CPU的消耗,然后就直接导致平均响应时间增加。因此上述的MaxClient数目,要根据CPU、内存等硬件因素综合考虑,绝对不是越多越好。可以通过Apache自带的abench来测试一下,取一个合适的值。然后,我们选择内存操作级别的存储的Redis,在高并发的状态下,存储的响应时间至关重要。网络带宽虽然也是一个因素,不过,这种请求数据包一般比较小,一般很少成为请求的瓶颈。负载均衡成为系统瓶颈的情况比较少,在这里不做讨论哈。

那么问题来了,假设我们的系统,在5w/s的高并发状态下,平均响应时间从100ms变为250ms(实际情况,甚至更多):

20*500/0.25 = 40000 (4万QPS)

于是,我们的系统剩下了4w的QPS,面对5w每秒的请求,中间相差了1w。

然后,这才是真正的恶梦开始。举个例子,高速路口,1秒钟来5部车,每秒通过5部车,高速路口运作正常。突然,这个路口1秒钟只能通过4部车,车流量仍然依旧,结果必定出现大塞车。(5条车道忽然变成4条车道的感觉)

同理,某一个秒内,20*500个可用连接进程都在满负荷工作中,却仍然有1万个新来请求,没有连接进程可用,系统陷入到异常状态也是预期之内。

其实在正常的非高并发的业务场景中,也有类似的情况出现,某个业务请求接口出现问题,响应时间极慢,将整个Web请求响应时间拉得很长,逐渐将Web服务器的可用连接数占满,其他正常的业务请求,无连接进程可用。

更可怕的问题是,是用户的行为特点,系统越是不可用,用户的点击越频繁,恶性循环最终导致“雪崩”(其中一台Web机器挂了,导致流量分散到其他正常工作的机器上,再导致正常的机器也挂,然后恶性循环),将整个Web系统拖垮。

3. 重启与过载保护

如果系统发生“雪崩”,贸然重启服务,是无法解决问题的。最常见的现象是,启动起来后,立刻挂掉。这个时候,最好在入口层将流量拒绝,然后再将重启。如果是redis/memcache这种服务也挂了,重启的时候需要注意“预热”,并且很可能需要比较长的时间。

秒杀和抢购的场景,流量往往是超乎我们系统的准备和想象的。这个时候,过载保护是必要的。如果检测到系统满负载状态,拒绝请求也是一种保护措施。在前端设置过滤是最简单的方式,但是,这种做法是被用户“千夫所指”的行为。更合适一点的是,将过载保护设置在CGI入口层,快速将客户的直接请求返回。

二、作弊的手段:进攻与防守

秒杀和抢购收到了“海量”的请求,实际上里面的水分是很大的。不少用户,为了“抢“到商品,会使用“刷票工具”等类型的辅助工具,帮助他们发送尽可能多的请求到服务器。还有一部分高级用户,制作强大的自动请求脚本。这种做法的理由也很简单,就是在参与秒杀和抢购的请求中,自己的请求数目占比越多,成功的概率越高。

这些都是属于“作弊的手段”,不过,有“进攻”就有“防守”,这是一场没有硝烟的战斗哈。

1. 同一个账号,一次性发出多个请求

部分用户通过浏览器的插件或者其他工具,在秒杀开始的时间里,以自己的账号,一次发送上百甚至更多的请求。实际上,这样的用户破坏了秒杀和抢购的公平性。

这种请求在某些没有做数据安全处理的系统里,也可能造成另外一种破坏,导致某些判断条件被绕过。例如一个简单的领取逻辑,先判断用户是否有参与记录,如果没有则领取成功,最后写入到参与记录中。这是个非常简单的逻辑,但是,在高并发的场景下,存在深深的漏洞。多个并发请求通过负载均衡服务器,分配到内网的多台Web服务器,它们首先向存储发送查询请求,然后,在某个请求成功写入参与记录的时间差内,其他的请求获查询到的结果都是“没有参与记录”。这里,就存在逻辑判断被绕过的风险。

 

应对方案:

在程序入口处,一个账号只允许接受1个请求,其他请求过滤。不仅解决了同一个账号,发送N个请求的问题,还保证了后续的逻辑流程的安全。实现方案,可以通过Redis这种内存缓存服务,写入一个标志位(只允许1个请求写成功,结合watch的乐观锁的特性),成功写入的则可以继续参加。

或者,自己实现一个服务,将同一个账号的请求放入一个队列中,处理完一个,再处理下一个。

2. 多个账号,一次性发送多个请求

很多公司的账号注册功能,在发展早期几乎是没有限制的,很容易就可以注册很多个账号。因此,也导致了出现了一些特殊的工作室,通过编写自动注册脚本,积累了一大批“僵尸账号”,数量庞大,几万甚至几十万的账号不等,专门做各种刷的行为(这就是微博中的“僵尸粉“的来源)。举个例子,例如微博中有转发抽奖的活动,如果我们使用几万个“僵尸号”去混进去转发,这样就可以大大提升我们中奖的概率。

这种账号,使用在秒杀和抢购里,也是同一个道理。例如,iPhone官网的抢购,火车票黄牛党。

应对方案:

这种场景,可以通过检测指定机器IP请求频率就可以解决,如果发现某个IP请求频率很高,可以给它弹出一个验证码或者直接禁止它的请求:

  1. 弹出验证码,最核心的追求,就是分辨出真实用户。因此,大家可能经常发现,网站弹出的验证码,有些是“鬼神乱舞”的样子,有时让我们根本无法看清。他们这样做的原因,其实也是为了让验证码的图片不被轻易识别,因为强大的“自动脚本”可以通过图片识别里面的字符,然后让脚本自动填写验证码。实际上,有一些非常创新的验证码,效果会比较好,例如给你一个简单问题让你回答,或者让你完成某些简单操作(例如百度贴吧的验证码)。
  2. 直接禁止IP,实际上是有些粗暴的,因为有些真实用户的网络场景恰好是同一出口IP的,可能会有“误伤“。但是这一个做法简单高效,根据实际场景使用可以获得很好的效果。

 

3. 多个账号,不同IP发送不同请求

所谓道高一尺,魔高一丈。有进攻,就会有防守,永不休止。这些“工作室”,发现你对单机IP请求频率有控制之后,他们也针对这种场景,想出了他们的“新进攻方案”,就是不断改变IP。=

有同学会好奇,这些随机IP服务怎么来的。有一些是某些机构自己占据一批独立IP,然后做成一个随机代理IP的服务,有偿提供给这些“工作室”使用。还有一些更为黑暗一点的,就是通过木马黑掉普通用户的电脑,这个木马也不破坏用户电脑的正常运作,只做一件事情,就是转发IP包,普通用户的电脑被变成了IP代理出口。通过这种做法,黑客就拿到了大量的独立IP,然后搭建为随机IP服务,就是为了挣钱。

应对方案:

说实话,这种场景下的请求,和真实用户的行为,已经基本相同了,想做分辨很困难。再做进一步的限制很容易“误伤“真实用户,这个时候,通常只能通过设置业务门槛高来限制这种请求了,或者通过账号行为的”数据挖掘“来提前清理掉它们。

僵尸账号也还是有一些共同特征的,例如账号很可能属于同一个号码段甚至是连号的,活跃度不高,等级低,资料不全等等。根据这些特点,适当设置参与门槛,例如限制参与秒杀的账号等级。通过这些业务手段,也是可以过滤掉一些僵尸号。

4. 火车票的抢购

看到这里,同学们是否明白你为什么抢不到火车票?如果你只是老老实实地去抢票,真的很难。通过多账号的方式,火车票的黄牛将很多车票的名额占据,部分强大的黄牛,在处理验证码方面,更是“技高一筹“。

高级的黄牛刷票时,在识别验证码的时候使用真实的人,中间搭建一个展示验证码图片的中转软件服务,真人浏览图片并填写下真实验证码,返回给中转软件。对于这种方式,验证码的保护限制作用被废除了,目前也没有很好的解决方案。

因为火车票是根据身份证实名制的,这里还有一个火车票的转让操作方式。大致的操作方式,是先用买家的身份证开启一个抢票工具,持续发送请求,黄牛账号选择退票,然后黄牛买家成功通过自己的身份证购票成功。当一列车厢没有票了的时候,是没有很多人盯着看的,况且黄牛们的抢票工具也很强大,即使让我们看见有退票,我们也不一定能抢得过他们哈。 

最终,黄牛顺利将火车票转移到买家的身份证下。

解决方案:

并没有很好的解决方案,唯一可以动心思的也许是对账号数据进行“数据挖掘”,这些黄牛账号也是有一些共同特征的,例如经常抢票和退票,节假日异常活跃等等。将它们分析出来,再做进一步处理和甄别。

三、高并发下的数据安全

我们知道在多线程写入同一个文件的时候,会存现“线程安全”的问题(多个线程同时运行同一段代码,如果每次运行结果和单线程运行的结果是一样的,结果和预期相同,就是线程安全的)。如果是MySQL数据库,可以使用它自带的锁机制很好的解决问题,但是,在大规模并发的场景中,是不推荐使用MySQL的。秒杀和抢购的场景中,还有另外一个问题,就是“超发”,如果在这方面控制不慎,会产生发送过多的情况。我们也曾经听说过,某些电商搞抢购活动,买家成功拍下后,商家却不承认订单有效,拒绝发货。这里的问题,也许并不一定是商家奸诈,而是系统技术层面存在超发风险导致的。

1. 超发的原因

假设某个抢购场景中,我们一共只有100个商品,在最后一刻,我们已经消耗了99个商品,仅剩最后一个。这个时候,系统发来多个并发请求,这批请求读取到的商品余量都是99个,然后都通过了这一个余量判断,最终导致超发。(同文章前面说的场景)

在上面的这个图中,就导致了并发用户B也“抢购成功”,多让一个人获得了商品。这种场景,在高并发的情况下非常容易出现。

2. 悲观锁思路

解决线程安全的思路很多,可以从“悲观锁”的方向开始讨论。

悲观锁,也就是在修改数据的时候,采用锁定状态,排斥外部请求的修改。遇到加锁的状态,就必须等待。

虽然上述的方案的确解决了线程安全的问题,但是,别忘记,我们的场景是“高并发”。也就是说,会很多这样的修改请求,每个请求都需要等待“锁”,某些线程可能永远都没有机会抢到这个“锁”,这种请求就会死在那里。同时,这种请求会很多,瞬间增大系统的平均响应时间,结果是可用连接数被耗尽,系统陷入异常。

3. FIFO队列思路

那好,那么我们稍微修改一下上面的场景,我们直接将请求放入队列中的,采用FIFO(First Input First Output,先进先出),这样的话,我们就不会导致某些请求永远获取不到锁。看到这里,是不是有点强行将多线程变成单线程的感觉哈。

然后,我们现在解决了锁的问题,全部请求采用“先进先出”的队列方式来处理。那么新的问题来了,高并发的场景下,因为请求很多,很可能一瞬间将队列内存“撑爆”,然后系统又陷入到了异常状态。或者设计一个极大的内存队列,也是一种方案,但是,系统处理完一个队列内请求的速度根本无法和疯狂涌入队列中的数目相比。也就是说,队列内的请求会越积累越多,最终Web系统平均响应时候还是会大幅下降,系统还是陷入异常。

4. 乐观锁思路

这个时候,我们就可以讨论一下“乐观锁”的思路了。乐观锁,是相对于“悲观锁”采用更为宽松的加锁机制,大都是采用带版本号(Version)更新。实现就是,这个数据所有请求都有资格去修改,但会获得一个该数据的版本号,只有版本号符合的才能更新成功,其他的返回抢购失败。这样的话,我们就不需要考虑队列的问题,不过,它会增大CPU的计算开销。但是,综合来说,这是一个比较好的解决方案。

5. 缓存服务器 

Redis分布式要保证数据都能能够平均的缓存到每一台机器,首先想到的做法是对数据进行分片,因为Redis是key-value存储的,首先想到的是Hash分片,可能的做法是对key进行哈希运算,得到一个long值对分布式的数量取模会得到一个一个对应数据库的一个映射,没有读取就可以定位到这台数据库

有很多软件和服务都“乐观锁”功能的支持,例如Redis中的watch就是其中之一。通过这个实现,我们保证了数据的安全。

四、小结

互联网正在高速发展,使用互联网服务的用户越多,高并发的场景也变得越来越多。电商秒杀和抢购,是两个比较典型的互联网高并发场景。虽然我们解决问题的具体技术方案可能千差万别,但是遇到的挑战却是相似的,因此解决问题的思路也异曲同工。

个人整理并发解决方案。

     a.应用层面:读写分离、缓存、队列、集群、令牌、系统拆分、隔离、系统升级(可水平扩容方向)。

     b.时间换空间:降低单次请求时间,这样在单位时间内系统并发就会提升。

     c.空间换时间:拉长整体处理业务时间,换取后台系统容量空间。