
来自广州的开发仔一枚,从事互联网金融系统和电子商务系统的技术研发
领课教育致力于打造一个全行业都适用的在线教育系统。除了商业版,团队也积极地推进教育系统的开源项目,希望能给做开发的朋友们带来一些帮助: 开源项目链接:https://gitee.com/roncoocom/roncoo-education 领课教育系统-商业版-最新架构如下 注册中心 早期领课教育系统(以下简称为:系统)使用的就是Eureka,Netflix 出品用于实现服务注册和发现的工具,目前该组件已经进入维护阶段不再更新。现在系统使用了Nacos,阿里巴巴出品一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。 配置中心 早期系统使用的是Spring Cloud Config,无界面管理,需要使用消息队列才能实现配置更新,目前系统使用Nacos既可以当注册中心又可以当配置中心,采用Netty保持TCP长连接实现配置刷新,拥有方便快捷的管理界面。 服务网关 早期系统使用是的Zuul,同样是Netflix 出品,用的是1.x的版本,Zuul 2.x 在底层上有了很大的改变,使用了异步无阻塞式的 API,性能改善明显,不过现在 Spring Cloud 没集成 Zuul 2.x。目前系统使用的是Spring Cloud Gateway,构建于 Spring 5+基于 Spring Boot 2.x 响应式的、非阻塞式的 API,同时它支持 websockets,和 Spring 框架紧密集成。 分布式调度 开源版采用的是传统的调度方案,较为简单,商业版采用了XXL-JOB作为分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。 分布式事务 分布式事务是分布式系统中一个永远绕不过去的话题,也是一个棘手的问题。目前领课教育系统采用了Seata作为解决方案,Seata 是一款Alibaba开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。 限流控制 早期系统使用的是Hystrix,同样是Netflix 出品,但是目前官方声明不再开发新功能。Spring官方不在推荐在后面的版本继续使用,目前领课教育系统采用Sentinle作为解决方案,Sentinel 是面向分布式服务架构的流量控制组件,主要以流量为切入点,从流量控制、熔断降级、系统自适应保护等多个维度来帮助您保障微服务的稳定性。 系统功能体验请登录:https://edu.roncoo.net/experience
Kubernetes是一个开源的,用于管理云平台中多个主机上的容器化的应用,Kubernetes的目标是让部署容器化的应用简单并且高效(powerful),Kubernetes提供了应用部署,规划,更新,维护的一种机制。 Kubernetes一个核心的特点就是能够自主的管理容器来保证云平台中的容器按照用户的期望状态运行着(比如用户想让apache一直运行,用户不需要关心怎么去做,Kubernetes会自动去监控,然后去重启,新建,总之,让apache一直提供服务),管理员可以加载一个微型服务,让规划器来找到合适的位置,同时,Kubernetes也系统提升工具以及人性化方面,让用户能够方便的部署自己的应用(就像canary deployments)。 容器编排调度引擎 —— k8s 的好处 简化应用部署 提高硬件资源利用率 健康检查和自修复 自动扩容缩容 服务发现和负载均衡 通过本教程的学习,你可以掌握K8S的简介、K8S的集群搭建(三种部署方式)、K8S企业应用案例(SpringBoot和K8S的实战、SpringCloud的客户端案例等),胜任企业级的开发工作。 内容详情:https://www.roncoo.com/view/1242797380548493313
互联网技术的崛起使得学习不再局限于教室,旅游、坐车、闲暇等碎片化时间都可用于获取知识。如今的互联网基础已搭建完成,在整个商业大环境进入转型的局势下各行业也都面临转型的挑战。一边是传统企业发展的增长瓶颈,一边是互联网浪潮带来的机会。一个是面向小范围的地区性用户,一个是面向全国甚至全球用户,在强大的互联网链接能力下你会怎么做选择。 在越来越追求效率的今天,用户已经帮企业作出了回答。例如传统线下的教培行业,在线上教学平台和教学工具越来越完善之后,大家就会选择更便捷的方式来获取知识。区别于线下上课在线教育抓住了用户业余学习的痛点,讲师可在业余时间将专业领域的知识整理成视频或在空余时间做直播教学,将业余时间的收益最大化。学员在平台上一次性付费后可随时随地打开学习,将时间利用最大化。 面对新教育趋势企业需要怎样迈出转型这一步,又该如何搭建在线教育平台? 首先线上学习的场景、设备和时间这些都是不固定的,想要模拟线下教学场景必须在满足学员多样化选择的同时结合视频直播支持讲师与学员实时互动的教学体验。因此在线学习系统需支持视频点播和实时直播两大块在线教学功能,搭配在线考试及题库练习,营销分销等。 大体的功能基本都能想到,但是细化到管理、安全性和易用性方面就要到实战环节去体验了。这里提供一个体验环境给大家对系统功能这块做个深入了解【在线教育系统体验环境】 。 介绍完系统功能接下来咱们来讨论下怎么实现,教培行业转型线上要怎样搭建在线教育平台,简单来说就是用什么方式来实现在线教学所需的系统功能。针对大部分中小企业而言最可行的方案有两种:一种是教育系统SaaS服务模式,一种是采购在线教育系统源码。这两种方案相对于自主开发有风险小、周期短等优势且有专业的技术团队做后续支撑。 教育系统SaaS服务 即进驻第三方平台根据自身需求和购买的套餐平台授权相关功能给客户使用,相当于“租用”类似于你在电商平台开个小店铺一样品牌属平台所有。这种模式比较适合人个讲师和小团队,只运营用户不太注重品牌建立且前期投入较小,符合个人的低成本创业需求。 购买教育系统源码 这种方案适合于线下教育机构和企业培训等需要发展自身品牌和注重用户生命周期价值运营的企业。私有化部署和可按需定制功能是采购教育系统源码的两大特点,对于没有技术开发团队的企业来说这种方式无疑是最优选择。通过系统考察-功能需求-定制开发-功能测试-交付上线全流程交给系统供应商管理,企业最后通过验收和协商服务期即可。
Elasticsearch是一个基于Lucene的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web接口。Elasticsearch是用Java语言开发的,并作为Apache许可条款下的开放源码发布,是一种流行的企业级搜索引擎。Elasticsearch用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。 下载https://www.elastic.co/cn/downloads/past-releases#elasticsearch 安装 # tar -zxvf elasticsearch-6.2.2.tar.gz //解压 # mv /opt/tools/elasticsearch-6.2.2 /opt/elasticsearch 说明:移动到/opt/elasticsearch下 配置 # vi /opt/elasticsearch/config/elasticsearch.yml 说明:可以使用默认配置 设置 # vi /usr/lib/systemd/system/elasticsearch.service [Unit] Description=elasticsearch [Service] User=roncoo LimitNOFILE=65536 LimitNPROC=65536 ExecStart=/opt/elasticsearch/bin/elasticsearch [Install] WantedBy=multi-user.target 操作 # systemctl enable elasticsearch //开机启动 # systemctl start elasticsearch //启动 开源项目地址:https://gitee.com/roncoocom/roncoo-education
Redis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。从2010年3月15日起,Redis的开发工作由VMware主持。 Redis 是一个高性能的key-value数据库。 redis的出现,很大程度补偿了memcached这类keyvalue存储的不足,在部 分场合可以对关系数据库起到很好的补充作用。它提供了Python,Ruby,Erlang,PHP客户端,使用很方便。 安装软件 # yum install epel-release -y 说明:安装epel源 # yum install redis -y 说明:安装redis,当前版本为3.2 设置开机启动 # systemctl enable redis 设置配置 # vi /etc/redis.conf bing 0.0.0.0 //允许外网访问,默认是不通外网 daemonize yes requirepass roncoo 说明:默认不能外网访问,需要配置。若配置外网访问,为了安全请设置密码。 操作说明启动:# systemctl start redis查看:# systemctl status redis重启:# systemctl restart redis 目录说明/usr/bin/redis-* 命令/etc/redis* 配置文件/var/log/redis/ 日志目录 文章来源:https://blog.roncoo.com/article/1281402533735550977开源项目地址:https://edu.roncoo.net/
MySQL 是最流行的关系型数据库管理系统,在 WEB 应用方面 MySQL 是最好的 RDBMS(Relational Database Management System:关系数据库管理系统)应用软件之一。这里使用yum安装方式,版本选择:https://repo.mysql.com/ ,推荐使用 MySQL5.7 版本。 安装软件 # yum install https://repo.mysql.com/mysql57-community-release-el7-9.noarch.rpm -y # yum install mysql-community-server -y 说明:安装完成之后,默认已经设置开机启动。 目录说明/var/lib/mysql/ 数据库安装目录/etc/my.cnf 配置文件/var/log/mysqld.log 错误日志文件 配置设置 # vi /etc/my.cnf [mysqld] # 采用utf8mb4编码,解决表情字符的编码与解码问题 character_set_server=utf8mb4 # 需要启用only_full_group_by SQL模式 sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION 初始化 # cat /var/log/mysqld.log | grep password 说明:查看初始化密码,初始化的时候需要用到 # mysql_secure_installation 说明:这里需要输入上一步的初始密码,特别注意新密码要求比较高的密码强度 文章来源:https://blog.roncoo.com/article/1280781211745636354开源项目地址:https://gitee.com/roncoocom/roncoo-education
FastDFS 是用 c 语言编写的一款开源的分布式文件系统,有多种原因的客户端(包括有Java的客户端)。FastDFS 为互联网量身定制, 充分考虑了冗余备份、负载均衡、线性扩容等机制,并注重高可用、高性能等指标,使用 FastDFS 很容易搭建一套高性能的文件服务器集群提供文件上传、下载等服务。 FastDFS 架构包括 Tracker server 和 Storage server。客户端请求 Tracker server 进行文 件上传、下载,通过 Tracker server 调度最终由 Storage server 完成文件上传和下载。Tracker server 作用是负载均衡和调度,通过 Tracker server 在文件上传时可以根据一些 策略找到 Storage server 提供文件上传服务。可以将 tracker 称为追踪服务器或调度服务 器。Storage server 作用是文件存储,客户端上传的文件最终存储在 Storage 服务器上, Storageserver 没有实现自己的文件系统而是利用操作系统 的文件系统来管理文件。可以将 storage 称为存储服务器。 1、准备如下 tar 包 libfastcommon-1.0.43.tar.gz fastdfs-6.06.tar.gz nginx-1.18.0.tar.gz fastdfs-nginx-module-1.22.tar.gz 2、libfastcommon的安装 # tar -zxvf libfastcommon-1.0.43.tar.gz //解压 # cd libfastcommon-1.0.43 // 进入解压目录 # ./make.sh //预编译 # ./make.sh install //安装 3、Fastdfs的安装 # tar -zxvf fastdfs-6.06.tar.gz //解压 # cd /root/download/fastdfs-6.06 // 进入解压目录 # ./make.sh && ./make.sh install 4、Fastdfs的配置 FastDFS由跟踪服务器(Tracker Server)、存储服务器(Storage Server)和客户端(Client)构成。 Tracker Server # cp /etc/fdfs/tracker.conf.sample tracker.conf base_path = /opt/fastdfs/tracker 注意:保证/opt/fastdfs/tracker已经存在,否则启动失败。 # service fdfs_trackerd start // 启动 # chkconfig fdfs_trackerd on // 设置开机启动 Storage Server # cp /et/fdfs/storage.conf.sample storage.conf base_path = /opt/fastdfs/storage store_path0 = /opt/fastdfs/storage0 tracker_server = 192.168.10.27:22122 注意:保证/opt/fastdfs/storage和/opt/fastdfs/storage0已经存在,否则启动失败。 192.168.10.27为内网IP,若要外网调试,可以使用外网IP。 # service fdfs_storaged start // 启动 # chkconfig fdfs_storaged on // 设置开机启动 查看Storage和Tracker是否在通信 # /usr/bin/fdfs_monitor /etc/fdfs/storage.conf 5、Nginx的安装(与fastdfs-nginx-module模块整合) # tar -zxvf nginx-1.18.0.tar.gz # tar -zxvf fastdfs-nginx-module-1.22.tar.gz # cd nginx-1.18.0 # ./configure --add-module=../fastdfs-nginx-module-1.22/src # make && make install 6、配置 # cp fastdfs-nginx-module-1.22/src/mod_fastdfs.conf /etc/fdfs/ # vi /etc/fdfs/mod_fastdfs.conf tracker_server=192.168.10.27:22122 url_have_group_name = true store_path0=/fastdfs/storage # cd fastdfs-6.06/conf/ // 进入fastdfd源码conf目录 # cp http.conf mime.types /etc/fdfs/ // 将http.conf,mime.types两个文件拷贝到/etc/fdfs/目录下 # vi /usr/local/nginx/conf/nginx.conf server { listen 80; server_name localhost; location ~/group([0-9])/M00 { root /opt/fastdfs/storage0/data; ngx_fastdfs_module; } } 文章来源:https://blog.roncoo.com/article/1275251133292867586
入局在线教育,你准备好了吗?没技术沉淀,盲目投入大量资金进行自主开发很容易入坑。除了前期开发投入成本大,产品开发周期长外系统的优化和业务流程的梳理调整都需要耗费大量的人力物力,对于中小企业来说投产比是划不来的。 那么中小企业怎样才能低成本高效率的开展在线教育业务呢?对大部分企业来说购买现成的在线教育系统是最合适的。目前市面现成的在线教育系统有很多,基本上付费购买源码即可快速部署上线。但是在采购的过程中我们怎样去辨别和筛选一套适合自身企业业务需求和稳定的运营系统也是我们工作的重点。 企业自身有内容输出的,可以选择单机构版的独立域名部署系统,与saas服务系统不同的是企业拥有自己的服务器和独立域名,系统提供商将整套系统源码部署在企业自身的服务器上,独立运营自己的品牌。后期企业还可以根据业务需求进行二次开发,迭代升级系统。 如何选择成熟稳定的第三方系统服务商? 领课教育系统: 领课旗下有自主运营的在线教育网站:https://www.roncoo.com 通过输出反馈迭代系统升级,有良好的系统稳定性,更全面的运营功能列表。 系统的开源项目和系统体验可登录官网:https://edu.roncoo.net 系统采用前后端分离模式,具有前端门户和后台运营2个子系统。前端开发语言为Vue.js,核心框架为Nuxt.js,能极大地解决单页面应用的 SEO 的问题,强大的服务端渲染功能。后端开发语言为Java,核心框架为Spring Cloud,有强大社区支持,为目前最流行的技术架构。系统功能介绍 录播功能介绍:讲师把已录制好的视频上传到平台,用户可随时随地登录网站进行学习。同时,拥有课件共享下载、跑马灯、移动端小程序观看、学习进度跟踪等功能。 直播功能介绍:直播小班课功能可以实现从1V1到1V16的全面互动教学,可以实现讲师和学生的实时语音、视频和电子白板的互动教学,支持签到、问卷、答题、评分等教学互动。大班课满足大型线上直播教学场景,支持学员和老师1V1视频语音互动教学,实现万人同时在线教学,讲师可设置助教协助管理直播教学。直播助手适用于软件操作教学或峰会活动等直播场景,支持异地多场景导播切换。 超级SVIP功能: 可独立设置会员特权免费菜单 设置专属会员优惠,叠加会员折上折 更好的维护老客留在及二次转化 博客功能: 用户可在平台上发布图文内容,对文章进行评论、点赞、分享、收藏。 资源专区:上传课件资源或者其他资源到平台供用户下载、也可对资源设置收费,资源需付费之后才能下载。 资讯功能:平台运营人员可以在管理后台发布公司信息、行业动态、产品介绍等信息。 讲师招募:支持讲师在线申请入驻,讲师提交入驻申请后经过管理后台审核通过即可成为平台讲师,成为讲师后即可获得讲师的对应权限,可发布课程,查看收益等。 站点信息:平台运营人员可以在管理后台动态配置网站底部公司简介、使用帮助、ICP备案、客户联系方式、官方微信、友情链接等信息。
从这一节开始,我们就要正式进去数据结构的世界了,那么第一个是什么呢,就是我们的数组。 在我想写数组的时候,我的第一印象是去看它的源码,很可惜,数组的实现太特殊了,找了很久,我没有找到它的源码,带着这样的思考,我就开始了Java中数组的挖掘。Wow,真香! 一、Java中数组的介绍 数组是一种最简单的复合数据类型,它是有序数据的集合,数组中的每个元素具有相同的数据类型,可以用一个统一的数组名和不同的下标来唯一确定数组中的元素。根据数组的维度,可以将其分为一维数组、二维数组和多维数组等。一定要注意,数组只能存放同一种数据类型(Object类型数组除外)。 二、数组是一个引用类型吗? 先给答案,是的,没有任何疑问。 注意,数组也是一种数据类型,它本身是一种引用类型。 数组是一种大小固定的数据结构,对线性表的所有操作都可以通过数组来实现。虽然数组一旦创建之后,它的大小就无法改变了,但是当数组不能再存储线性表中的新元素时,我们可以创建一个新的大的数组来替换当前数组。这样就可以使用数组实现动态的数据结构。 如何验证? 定义一个数组,发现它拥有Object类的所有方法。根据这个例子,其实大家已经看出来了,数组拥有超类Object的所有方法,说明他也是一个类。并且他拥有自己的clone()方法和length属性。 三、如何了解数组的底层实现 既然数组拥有Object的所有方法,那我们是否能查看一下数组的源码,来了解一下数组的实现呢? 可惜,数组太特殊了,他的实现是虚拟机编译的时候动态生成的,所以我们无法直接查看源码,只能通过查看编译后的class的字节码一探究竟。 JVM 中数组对象是一种特殊的对象,虚拟机从数组的元数据中无法确认数组的大小,它的Object Header 比普通对象多了一个word 来存储数组的长度,length 会编译成对应的字节码读取这个field 就可以了。 我分别定义基本数据类型和引用类型来查看一下最终生成的字节码有何区别。 Object[] o = new String[11]; o[0]="1aaa"; int i=o.length; Integer[] a=new Integer[11]; a[0]=100; int j=a.length; int[] b=new int[11]; b[0]=100; int k=b.length; } 对应的字节码为: 2 anewarray #12 <java/lang/String> //anewarray代表对象数组 5 astore_1 6 aload_1 7 iconst_0 8 ldc #25 <1aaa> 10 aastore 11 aload_1 12 arraylength //arraylength代表长度 13 istore_2 14 bipush 11 16 anewarray #26 <java/lang/Integer> //anewarray代表包装类数组 19 astore_3 20 aload_3 21 iconst_0 22 bipush 100 24 invokestatic #27 <java/lang/Integer.valueOf> 27 aastore 28 aload_3 29 arraylength 30 istore 4 32 bipush 11 34 newarray 10 (int) //newarray代表基本数组类型数组 36 astore 5 38 aload 5 40 iconst_0 41 bipush 100 43 iastore 44 aload 5 46 arraylength 47 istore 6 49 return 注意:定义并初始化一个数组后,在内存中分配了两个空间,一个用于存放数组的引用变量,另一个用于存放数组本身。进行程序开发时,要深入底层的运行机制。 看待一个数组时,一定要把数组看成两个部分:一部分是数组引用,也就是在代码中定义的数组引用变量;还有一部分是实际的数组对象,这部分是在对内存里运行的,通常无法直接访问它,只能通过数组引用变量来访问。 四、Array 的 length 域相关 在很多的资料中都写了,Array中有类似public final int length的成员变量。但是在《Java Language Specifications》10.1. Array Types中明确写了,length不是类型的一部分; An array's length is not part of its type. String[] s = new String[2]; System.out.println(s.length); System.out.println(s.getClass().getDeclaredFields().length); try { System.out.println(s.getClass().getDeclaredField("length")); } catch (NoSuchFieldException e) { System.out.println(e.toString()); } } 可以看到length并不是Array的成员变量。 五、Java语言规范关于Array的定义 数组在Java里是一种特殊类型,有别于普通的“类的实例”的对象。10.1. Array Types10.8. Class Objects for Arrays Every array has an associated Class object, shared with all other arrays with the same component type.Although an array type is not a class, the Class object of every array acts as if: 1、The direct superclass of every array type is Object.2、Every array type implements the interfaces Cloneable and java.io.Serializable. 数组类型是由JVM从元素类型合成出来的。10.7. Array Members The members of an array type are all of the following: 1、The public final field length, which contains the number of components of the array. length may be positive or zero. 从Java语言到Class文件,Java源码编译器会识别出对数组类型的length字段的访问,并生成对应的字节码。以OpenJDK8的javac为例:jdk8u/jdk8u/langtools: 84eb51777733 src/share/classes/com/sun/tools/javac/jvm/Gen.java if (sym == syms.lengthVar) { code.emitop0(arraylength); result = items.makeStackItem(syms.intType); } 六、数据应用场景 这种数据结构使用一段连续的空间来存贮元素,所以可以直接通过索引来获取到某个元素,而且可以通过对元素的内容进行排序,然后使用二分法查找,从而提供查找效率。其适合的场合主要是: 1、不会频繁增删元素的场合,因为增删元素都牵涉到元素空间的重新分配,频繁的内存分配操作会大幅降低操作效率。但添加操作时,可以通过预分配足够的空间来优化添加时的效率。 2、属于随机迭代器,可以随机访问任意元素。对于已排序的元素查找起来效率较高。 七、数组总结 在看数组的时候,因为class是动态创建的,所以看了很久,但是根据数组的特性,基本可以认为数组的域和方法,类似于: public T[] clone() { try { return (T[]) super.clone(); } catch (CloneNotSupportedException e) { throw new InternalError(e.getMessage()); } } } 数组可以是一维数组、二维数组或多维数组。 数值数组元素的默认值为 0,而引用元素的默认值为 null。 交错数组是数组的数组,因此,它的元素是引用类型,初始化为 null。交错数组元素的维度和大小可以不同。 数组的索引从 0 开始,如果数组有 n 个元素,那么数组的索引是从 0 到(n-1)。 数组元素可以是任何类型,包括数组类型。 数组类型是从抽象基类 Array 派生的引用类型。 课程扩展阅读:https://www.roncoo.com/view/1160515822987776001
在配置nginx.conf文件的时候,我们很容易发现,有部分配置项是既可以配置在http块,也可以配置在server块,还可以配置在location块中。但是并不是所有的配置项都可以在任意位置进行配置的,根据配置项所起到的作用,nginx对各个配置块所能使用的位置进行了定义。既然一个配置项可以配置在多个配置块中,那么这里就涉及到一个问题就是,在处理请求的时候是以哪一个配置项为准。本文主要讲解nginx是如何实现配置项的合并的。 在前面的文章中,我们讲解了nginx http模块的基本存储结构,在阅读本文之前强烈建议读者朋友先阅读这篇文章。如下是nginx http模块的存储结构示意图: 这里我们不再赘述nginx是如何解析各个配置项,从而形成这样的一个存储结构的。nginx对配置项的合并主要是通过ngx_http_merge_servers()方法进行的,如下是该方法的源码: * @param cf 整个nginx运行的ngx_conf_t对象 * @param cmcf 核心模块对应的配置对象 * @param module 外层遍历时,当前遍历的模块 * @param ctx_index 外层遍历时,当前遍历的模块的配置对应的存储位置 */ static char * ngx_http_merge_servers(ngx_conf_t *cf, ngx_http_core_main_conf_t *cmcf, ngx_http_module_t *module, ngx_uint_t ctx_index) { char *rv; ngx_uint_t s; ngx_http_conf_ctx_t *ctx, saved; ngx_http_core_loc_conf_t *clcf; ngx_http_core_srv_conf_t **cscfp; // 这里获取所有server配置块对应的ngx_http_core_srv_conf_t结构体,每个结构体都对应了配置文件中 // 一个server块的配置 cscfp = cmcf->servers.elts; // 这里的cf->ctx就是解析http配置块得到的ngx_http_conf_ctx_t结构体,需要注意的是,其中只有main_conf // 对应的数组数据有意义,因为http块的数据只存储在main_conf中 ctx = (ngx_http_conf_ctx_t *) cf->ctx; // 这里需要注意的是,在执行saved=*ctx;之后,会将当前*ctx的结构体对象完全复制而得到一个新的,从而存储到 // saved中,也就是说ctx和&saved的值是不一样的。在下面的for循环中会执行 // ctx->srv_conf = cscfp[s]->ctx->srv_conf;语句,这里其实改变 // 的是ctx指针所指向的结构体对象的srv_conf属性的值,而并没有影响到saved->srv_conf属性值, // 因为saved和*ctx是两个各个属性值虽然相同,但是本身并不同的结构体对象。 saved = *ctx; rv = NGX_CONF_OK; for (s = 0; s < cmcf->servers.nelts; s++) { /* merge the server{}s' srv_conf's */ ctx->srv_conf = cscfp[s]->ctx->srv_conf; if (module->merge_srv_conf) { // 这里主要是进行配置的合并,saved.srv_conf[ctx_index]指向的是http块中解析出的SRV类型的配置, // cscfp[s]->ctx->srv_conf[ctx_index]解析的则是当前server块中的配置,合并的一般规则是通过对应 // 模块实现的merge_srv_conf()来进行的,具体将会使用http块中的配置,还是使用server块中的配置, // 或者说合并两者,则是由具体的实现来决定的。这里合并的效果本质上就是对 // cscfp[s]->ctx->srv_conf[ctx_index]中的属性赋值,如果该值不存在,则将http块对应的值写到 // 该属性中 rv = module->merge_srv_conf(cf, saved.srv_conf[ctx_index], cscfp[s]->ctx->srv_conf[ctx_index]); if (rv != NGX_CONF_OK) { goto failed; } } if (module->merge_loc_conf) { ctx->loc_conf = cscfp[s]->ctx->loc_conf; // 需要注意的是,这里合并的主要是http块和当前server块中对应的LOC类型的配置数据,此时并没有开始 // 合并server块下的各个location配置 rv = module->merge_loc_conf(cf, saved.loc_conf[ctx_index], cscfp[s]->ctx->loc_conf[ctx_index]); if (rv != NGX_CONF_OK) { goto failed; } clcf = cscfp[s]->ctx->loc_conf[ngx_http_core_module.ctx_index]; // 这里clcf->locations存储了当前server块下的所有location块的配置,而cscfp[s]->ctx->loc_conf // 则指向的是将http块和server块合并之后的配置数据,从这里就可以看出,当前方法的结果就是合并了 // http块、server块和location块的配置 rv = ngx_http_merge_locations(cf, clcf->locations, cscfp[s]->ctx->loc_conf, module, ctx_index); if (rv != NGX_CONF_OK) { goto failed; } } } failed: *ctx = saved; return rv; } 这里ngx_http_merge_servers()方法主要完成了如下几个工作: 1、调用当前模块的merge_srv_conf()方法将http配置块和server配置块中NGX_HTTP_SRV_CONF_OFFSET类型的配置项进行合并,也即将http配置块结构体中的srv_conf数组与server配置块结构体中的srv_conf数组进行合并,如此就完成了NGX_HTTP_SRV_CONF_OFFSET类型配置项的合并; 2、调用当前模块的merge_loc_conf()方法将http配置块和server配置块中NGX_HTTP_LOC_CONF_OFFSET类型的配置项进行合并,也即将http配置块结构体中的loc_conf数组与server配置块结构体中的loc_conf数组进行合并。不过不同于NGX_HTTP_SRV_CONF_OFFSET类型的配置项,NGX_HTTP_LOC_CONF_OFFSET类型的配置项是还可以配置在location配置块中的,因而还需要将合并的结果与location配置块中的配置项进行合并; 3、调用ngx_http_merge_locations()方法将前一步http配置块与server配置块合并的NGX_HTTP_LOC_CONF_OFFSET类型的配置项继续与location配置块中的配置项进行合并,从而得到最终合并结果。不过这里需要注意的一点是,location配置块中还可以继续配置配置子location配置块,如此不断往复下去,而这里的ngx_http_merge_locations()方法则可以用于递归调用,以便于进行子location的合并。 这里我们继续看ngx_http_merge_locations()方法是如何合并子location的: * 合并location块的配置 * * @param cf 指向当前http块的配置结构体ngx_http_conf_ctx_t * @param locations 当前server块下所有的location块的配置 * @param loc_conf * @param module * @param ctx_index * @return */ static char * ngx_http_merge_locations(ngx_conf_t *cf, ngx_queue_t *locations, void **loc_conf, ngx_http_module_t *module, ngx_uint_t ctx_index) { char *rv; ngx_queue_t *q; ngx_http_conf_ctx_t *ctx, saved; ngx_http_core_loc_conf_t *clcf; ngx_http_location_queue_t *lq; if (locations == NULL) { return NGX_CONF_OK; } ctx = (ngx_http_conf_ctx_t *) cf->ctx; saved = *ctx; for (q = ngx_queue_head(locations); q != ngx_queue_sentinel(locations); q = ngx_queue_next(q)) { lq = (ngx_http_location_queue_t *) q; clcf = lq->exact ? lq->exact : lq->inclusive; ctx->loc_conf = clcf->loc_conf; // 合并location块的配置,这里的loc_conf[ctx_index]存储的是http块与server块配置合并的数据, // 而clcf->loc_conf则是当前server块下的某个location块的数据,这里其实就是将当前loc_conf中的 // 数据合并到clcf->loc_conf中 rv = module->merge_loc_conf(cf, loc_conf[ctx_index], clcf->loc_conf[ctx_index]); if (rv != NGX_CONF_OK) { return rv; } // 由于location块中还可以配置子location块,因而这里会递归的进行合并 rv = ngx_http_merge_locations(cf, clcf->locations, clcf->loc_conf, module, ctx_index); if (rv != NGX_CONF_OK) { return rv; } } *ctx = saved; return NGX_CONF_OK; } 这里对location以及子location的合并过程其实比较简单,就是依次弹出当前配置块(server或者父location)下的各个子location,然后调用当前模块的merge_loc_conf()方法将当前配置块的配置与弹出的子location进行合并。在合并完成后,继续对该子location进行递归的合并,也即将该子location与其子location进行合并。如此不断往复下去,直到所有的location都被合并完成。 nginx的配置项合并是一个比较重要的功能,比如某个既可以在http块中也可以在server块中配置的配置项,如果nginx.conf中仅仅只是配置了http块中的配置,那么所有的server块的配置就都可以继承http块的配置,但如果其中某个server自身也进行了该配置项的配置,那么进行合并的时候该配置项就可以直接使用其自身的配置,而不需要继承http配置块的配置,如此就可以实现非常灵活的功能。 文章来源:https://my.oschina.net/zhangxufeng/blog/3173782 相关内容推荐:https://www.roncoo.com/view/1211250285484212225
这个n到底是多少年?宇宙第一开发IDE Visual Studio的调试功能非常强大,平常工作debug帮助我们解决不少问题。今天分享两个异常捕获的技巧,希望能够帮助解决一些问题。 以下两种情况,我相信大家都会遇到过。 1.没有使用Try-Catch语句,当异常发生的时候,能够自动跳转到异常发生的地方,在使用Try-Catch捕获异常的时候,直接跳转到Catch语句的位置,并不会自动定位到异常代码的位置。 2.使用Try-Catch的时候,多层方法调用时,并不能直接查看到异常代码的位置。 技巧1:自动定位到异常代码位置 针对问题1,我们最想要的结果是,哪里有代码出现错误了,就直接定位到哪儿,异常出在哪行代码上,我一眼就能看得出,这样就能更快地处理问题了。 对于问题1,所出现的这种情况,简单复现一下一个空引用的异常 namespace ExceptionSample { class Program { static void Main(string[] args) { try { Random random = null; Console.WriteLine(random.Next()); } catch (Exception ex) { Console.WriteLine(ex); } Console.ReadLine(); } } } 上面的异常代码NullReferrenceException,Debug模式下,会跳转到catch语句这里。你可能觉得这挺简单的…可实际实际工作中,你的一个方法中仅仅只这一个对象吗?在实际工作中可能不止random一个对象,代码复杂,对象够多,几十个也有,我们就很难定位到异常出错的代码了。StackTrace可以定位到那个函数调用错了,并不能定位到哪一行代码出错了。 为了解决这个行为可以通过在Visual Studio中菜单栏中的调试》窗口》异常设置中去配置。如下图所示:勾选上Common Language Runtime Exceptions下列的异常单选框。有点多,以前的设置有些变化。 现在我们再看之前的代码,使用Try-Catch语句捕获异常的时候,就会直接定位到异常代码的位置了,如下图示: static void Main(string[] args) { try { Random random = null; Random random1 = new Random(); Random random2 = new Random(); Random random3 = new Random(); Console.WriteLine(random1.Next()); Console.WriteLine(random2.Next()); Console.WriteLine(random3.Next()); Console.WriteLine(random.Next()); } catch (Exception ex) { Console.WriteLine(ex); } Console.ReadLine(); } 技巧2:正常的throw 姿势 还是之前的一个方法,我已经将异常设置回复默认了。 static void Main(string[] args) { try { Random random = null; Console.WriteLine(random.Next()); } catch (Exception ex) { System.Diagnostics.Debug.Write(ex); throw ex; } } 我们再输出中可以看到(ps:项目名称用的之前的,不介意哈)错误的代码在16行。可实际工作中的情况并不是这样简单,基本上是A方法调用B方法,B方法调用C方法,代码如下所示: 在Main方法中调用ThrowNullReferrence(),方法ThrowNullReferrence中调用SetNullReferrence()。代码变复杂后,一层嵌套一层。这个时候能正确显示出代码异常的位置吗? static void Main(string[] args) { try { ThrowNullReferrence(); } catch (Exception ex) { System.Diagnostics.Debug.Write(ex); throw ex; } } public static void ThrowNullReferrence() { try { SetNullReferrence(); } catch (Exception ex) { System.Diagnostics.Debug.Write(ex); throw ex; } } public static void SetNullReferrence() { try { Random random = null; Console.WriteLine(random.Next()); } catch(Exception ex) { System.Diagnostics.Debug.Write(ex); throw ex; } } 我们可以通过下图看到:System.NullReferenceException: 未将对象引用设置到对象的实例。 在 ExceptionSample.Program.SetNullReferrence() 位置 D:Learn延迟加载LinqLayzLoadLinqLayzLoadProgram.cs:行号 39System.NullReferenceException: 未将对象引用设置到对象的实例。 在 ExceptionSample.Program.SetNullReferrence() 位置 D:Learn延迟加载LinqLayzLoadLinqLayzLoadProgram.cs:行号 44 在 ExceptionSample.Program.ThrowNullReferrence() 位置 D:Learn延迟加载LinqLayzLoadLinqLayzLoadProgram.cs:行号 27System.NullReferenceException: 未将对象引用设置到对象的实例。 在 ExceptionSample.Program.ThrowNullReferrence() 位置 D:Learn延迟加载LinqLayzLoadLinqLayzLoadProgram.cs:行号 32 在 ExceptionSample.Program.Main(String[] args) 位置 D:Learn延迟加载LinqLayzLoadLinqLayzLoadProgram.cs:行号 15 错误代码的位置在39行,以上出现异常的地方都是throw的位置。原因呢?catch捕获完后,如果要向上抛出,应该重新实例化一个新的异常对象,再向上抛出,这个最外层方法catch到的才是完整的异常,当然也包括完整的堆栈信息,这样才能定位到异常代码的位置。 要使用 throw new Exception改造后的例子如图,精准定位到39行的空引用异常Console.WriteLine(random.Next());结语分享之前看到的一个老程序员的经验之谈:“多coding,少debug”,回到标题为什么说"使用Vistual Studio n年",这个n到底指的是多少年。我的意思是可能有些东西,即使使用多年,可能不知道这两个技巧。 文章来源:https://blog.csdn.net/kebi007/article/details/103439933更多技术内容:https://www.roncoo.com
在微服务中,rest服务互相调用是很普遍的,我们该如何优雅地调用,其实在Spring框架使用RestTemplate类可以优雅地进行rest服务互相调用,它简化了与http服务的通信方式,统一了RESTful的标准,封装了http链接,操作使用简便,还可以自定义RestTemplate所需的模式。其中: 1.RestTemplate默认使用HttpMessageConverter实例将HTTP消息转换成POJO或者从POJO转换成HTTP消息。默认情况下会注册主mime类型的转换器,但也可以通过setMessageConverters注册自定义转换器。 2.RestTemplate使用了默认的DefaultResponseErrorHandler,对40X Bad Request或50X internal异常error等错误信息捕捉。 3.RestTemplate还可以使用拦截器interceptor,进行对请求链接跟踪,以及统一head的设置。 其中,RestTemplate还定义了很多的REST资源交互的方法,其中的大多数都对应于HTTP的方法,如下: RestTemplate源码 1.1 默认调用链路restTemplate进行API调用时,默认调用链: ###########1.使用createRequest创建请求######## resttemplate->execute()->doExecute() HttpAccessor->createRequest() //获取拦截器Interceptor,InterceptingClientHttpRequestFactory,SimpleClientHttpRequestFactory InterceptingHttpAccessor->getRequestFactory() //获取默认的SimpleBufferingClientHttpRequest SimpleClientHttpRequestFactory->createRequest() #######2.获取响应response进行处理########### AbstractClientHttpRequest->execute()->executeInternal() AbstractBufferingClientHttpRequest->executeInternal() ###########3.异常处理##################### resttemplate->handleResponse() ##########4.响应消息体封装为java对象####### HttpMessageConverterExtractor->extractData() 1.2 restTemplate->doExecute() 在默认调用链中,restTemplate 进行API调用都会调用 doExecute 方法,此方法主要可以进行如下步骤: 1)使用createRequest创建请求,获取响应2)判断响应是否异常,处理异常3)将响应消息体封装为java对象 [@Nullable](https://my.oschina.net/u/2896689) protected <T> T doExecute(URI url, [@Nullable](https://my.oschina.net/u/2896689) HttpMethod method, [@Nullable](https://my.oschina.net/u/2896689) RequestCallback requestCallback, [@Nullable](https://my.oschina.net/u/2896689) ResponseExtractor<T> responseExtractor) throws RestClientException { Assert.notNull(url, "URI is required"); Assert.notNull(method, "HttpMethod is required"); ClientHttpResponse response = null; try { //使用createRequest创建请求 ClientHttpRequest request = createRequest(url, method); if (requestCallback != null) { requestCallback.doWithRequest(request); } //获取响应response进行处理 response = request.execute(); //异常处理 handleResponse(url, method, response); //响应消息体封装为java对象 return (responseExtractor != null ? responseExtractor.extractData(response) : null); }catch (IOException ex) { String resource = url.toString(); String query = url.getRawQuery(); resource = (query != null ? resource.substring(0, resource.indexOf('?')) : resource); throw new ResourceAccessException("I/O error on " + method.name() + " request for \"" + resource + "\": " + ex.getMessage(), ex); }finally { if (response != null) { response.close(); } } } 1.3 InterceptingHttpAccessor->getRequestFactory() 在默认调用链中,InterceptingHttpAccessor的getRequestFactory()方法中,如果没有设置interceptor拦截器,就返回默认的SimpleClientHttpRequestFactory,反之,返回InterceptingClientHttpRequestFactory的requestFactory,可以通过resttemplate.setInterceptors设置自定义拦截器interceptor。 //Return the request factory that this accessor uses for obtaining client request handles. public ClientHttpRequestFactory getRequestFactory() { //获取拦截器interceptor(自定义的) List<ClientHttpRequestInterceptor> interceptors = getInterceptors(); if (!CollectionUtils.isEmpty(interceptors)) { ClientHttpRequestFactory factory = this.interceptingRequestFactory; if (factory == null) { factory = new InterceptingClientHttpRequestFactory(super.getRequestFactory(), interceptors); this.interceptingRequestFactory = factory; } return factory; } else { return super.getRequestFactory(); } } 然后再调用SimpleClientHttpRequestFactory的createRequest创建连接: [@Override](https://my.oschina.net/u/1162528) public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException { HttpURLConnection connection = openConnection(uri.toURL(), this.proxy); prepareConnection(connection, httpMethod.name()); if (this.bufferRequestBody) { return new SimpleBufferingClientHttpRequest(connection, this.outputStreaming); } else { return new SimpleStreamingClientHttpRequest(connection, this.chunkSize, this.outputStreaming); } } 1.4 resttemplate->handleResponse()在默认调用链中,resttemplate的handleResponse,响应处理,包括异常处理,而且异常处理可以通过调用setErrorHandler方法设置自定义的ErrorHandler,实现对请求响应异常的判别和处理。自定义的ErrorHandler需实现ResponseErrorHandler接口,同时Spring boot也提供了默认实现DefaultResponseErrorHandler,因此也可以通过继承该类来实现自己的ErrorHandler。 DefaultResponseErrorHandler默认对40X Bad Request或50X internal异常error等错误信息捕捉。如果想捕捉服务本身抛出的异常信息,需要通过自行实现RestTemplate的ErrorHandler。 ResponseErrorHandler errorHandler = getErrorHandler(); //判断响应是否有异常 boolean hasError = errorHandler.hasError(response); if (logger.isDebugEnabled()) { try { int code = response.getRawStatusCode(); HttpStatus status = HttpStatus.resolve(code); logger.debug("Response " + (status != null ? status : code)); }catch (IOException ex) { // ignore } } //有异常进行异常处理 if (hasError) { errorHandler.handleError(url, method, response); } } 1.5 HttpMessageConverterExtractor->extractData()在默认调用链中, HttpMessageConverterExtractor的extractData中进行响应消息体封装为java对象,就需要使用message转换器,可以通过追加的方式增加自定义的messageConverter:先获取现有的messageConverter,再将自定义的messageConverter添加进去。 根据restTemplate的setMessageConverters的源码可得,使用追加的方式可防止原有的messageConverter丢失,源码: public void setMessageConverters(List<HttpMessageConverter<?>> messageConverters) { //检验 validateConverters(messageConverters); // Take getMessageConverters() List as-is when passed in here if (this.messageConverters != messageConverters) { //先清除原有的messageConverter this.messageConverters.clear(); //后加载重新定义的messageConverter this.messageConverters.addAll(messageConverters); } } HttpMessageConverterExtractor的extractData源码: MessageBodyClientHttpResponseWrapper responseWrapper = new MessageBodyClientHttpResponseWrapper(response); if (!responseWrapper.hasMessageBody() || responseWrapper.hasEmptyMessageBody()) { return null; } //获取到response的ContentType类型 MediaType contentType = getContentType(responseWrapper); try { //依次循环messageConverter进行判断是否符合转换条件,进行转换java对象 for (HttpMessageConverter<?> messageConverter : this.messageConverters) { //会根据设置的返回类型responseType和contentType参数进行匹配,选择合适的MessageConverter if (messageConverter instanceof GenericHttpMessageConverter) { GenericHttpMessageConverter<?> genericMessageConverter = (GenericHttpMessageConverter<?>) messageConverter; if (genericMessageConverter.canRead(this.responseType, null, contentType)) { if (logger.isDebugEnabled()) { ResolvableType resolvableType = ResolvableType.forType(this.responseType); logger.debug("Reading to [" + resolvableType + "]"); } return (T) genericMessageConverter.read(this.responseType, null, responseWrapper); } } if (this.responseClass != null) { if (messageConverter.canRead(this.responseClass, contentType)) { if (logger.isDebugEnabled()) { String className = this.responseClass.getName(); logger.debug("Reading to [" + className + "] as \"" + contentType + "\""); } return (T) messageConverter.read((Class) this.responseClass, responseWrapper); } } } } ..... } 1.6 contentType与messageConverter之间的关系在HttpMessageConverterExtractor的extractData方法中看出,会根据contentType与responseClass选择messageConverter是否可读、消息转换。关系如下: springboot集成RestTemplate 根据上述源码的分析学习,可以轻松,简单地在项目进行对RestTemplate进行优雅地使用,比如增加自定义的异常处理、MessageConverter以及拦截器interceptor。本文使用示例demo,详情请查看接下来的内容。 2.1. 导入依赖:(RestTemplate集成在Web Start中) <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.2.0.RELEASE</version> </dependency> <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.10</version> <scope>provided</scope> </dependency> 2.2. RestTemplat配置:1.使用ClientHttpRequestFactory属性配置RestTemplat参数,比如ConnectTimeout,ReadTimeout;2.增加自定义的interceptor拦截器和异常处理;3.追加message转换器;4.配置自定义的异常处理. @Configuration public class RestTemplateConfig { @Value("${resttemplate.connection.timeout}") private int restTemplateConnectionTimeout; @Value("${resttemplate.read.timeout}") private int restTemplateReadTimeout; @Bean //@LoadBalanced public RestTemplate restTemplate( ClientHttpRequestFactory simleClientHttpRequestFactory) { RestTemplate restTemplate = new RestTemplate(); //配置自定义的message转换器 List<HttpMessageConverter<?>> messageConverters = restTemplate.getMessageConverters(); messageConverters.add(new CustomMappingJackson2HttpMessageConverter()); restTemplate.setMessageConverters(messageConverters); //配置自定义的interceptor拦截器 List<ClientHttpRequestInterceptor> interceptors=new ArrayList<ClientHttpRequestInterceptor>(); interceptors.add(new HeadClientHttpRequestInterceptor()); interceptors.add(new TrackLogClientHttpRequestInterceptor()); restTemplate.setInterceptors(interceptors); //配置自定义的异常处理 restTemplate.setErrorHandler(new CustomResponseErrorHandler()); restTemplate.setRequestFactory(simleClientHttpRequestFactory); return restTemplate; } @Bean public ClientHttpRequestFactory simleClientHttpRequestFactory(){ SimpleClientHttpRequestFactory reqFactory= new SimpleClientHttpRequestFactory(); reqFactory.setConnectTimeout(restTemplateConnectionTimeout); reqFactory.setReadTimeout(restTemplateReadTimeout); return reqFactory; } } 2.3. 组件(自定义异常处理、interceptor拦截器、message转化器) 自定义interceptor拦截器,实现ClientHttpRequestInterceptor接口 1.自定义TrackLogClientHttpRequestInterceptor,记录resttemplate的request和response信息,可进行追踪分析; 2.自定义HeadClientHttpRequestInterceptor,设置请求头的参数。API发送各种请求,很多请求都需要用到相似或者相同的Http Header。如果在每次请求之前都把Header填入HttpEntity/RequestEntity,这样的代码会显得十分冗余,可以在拦截器统一设置。 TrackLogClientHttpRequestInterceptor: /** * @Auther: ccww * @Date: 2019/10/25 22:48,记录resttemplate访问信息 * @Description: 记录resttemplate访问信息 */ @Slf4j public class TrackLogClientHttpRequestInterceptor implements ClientHttpRequestInterceptor { public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { trackRequest(request,body); ClientHttpResponse httpResponse = execution.execute(request, body); trackResponse(httpResponse); return httpResponse; } private void trackResponse(ClientHttpResponse httpResponse)throws IOException { log.info("============================response begin=========================================="); log.info("Status code : {}", httpResponse.getStatusCode()); log.info("Status text : {}", httpResponse.getStatusText()); log.info("Headers : {}", httpResponse.getHeaders()); log.info("=======================response end================================================="); } private void trackRequest(HttpRequest request, byte[] body)throws UnsupportedEncodingException { log.info("======= request begin ========"); log.info("uri : {}", request.getURI()); log.info("method : {}", request.getMethod()); log.info("headers : {}", request.getHeaders()); log.info("request body : {}", new String(body, "UTF-8")); log.info("======= request end ========"); } } HeadClientHttpRequestInterceptor: @Slf4j public class HeadClientHttpRequestInterceptor implements ClientHttpRequestInterceptor { public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] bytes, ClientHttpRequestExecution clientHttpRequestExecution) throws IOException { log.info("#####head handle########"); HttpHeaders headers = httpRequest.getHeaders(); headers.add("Accept", "application/json"); headers.add("Accept-Encoding", "gzip"); headers.add("Content-Encoding", "UTF-8"); headers.add("Content-Type", "application/json; charset=UTF-8"); ClientHttpResponse response = clientHttpRequestExecution.execute(httpRequest, bytes); HttpHeaders headersResponse = response.getHeaders(); headersResponse.add("Accept", "application/json"); return response; } } 自定义异常处理,可继承DefaultResponseErrorHandler或者实现ResponseErrorHandler接口: 1.实现自定义ErrorHandler的思路是根据响应消息体进行相应的异常处理策略,对于其他异常情况由父类DefaultResponseErrorHandler来进行处理。 2.自定义CustomResponseErrorHandler进行30x异常处理 CustomResponseErrorHandler: /** * @Auther: Ccww * @Date: 2019/10/28 17:00 * @Description: 30X的异常处理 */ @Slf4j public class CustomResponseErrorHandler extends DefaultResponseErrorHandler { @Override public boolean hasError(ClientHttpResponse response) throws IOException { HttpStatus statusCode = response.getStatusCode(); if(statusCode.is3xxRedirection()){ return true; } return super.hasError(response); } @Override public void handleError(ClientHttpResponse response) throws IOException { HttpStatus statusCode = response.getStatusCode(); if(statusCode.is3xxRedirection()){ log.info("########30X错误,需要重定向!##########"); return; } super.handleError(response); } } 自定义message转化器 /** * @Auther: Ccww * @Date: 2019/10/29 21:15 * @Description: 将Content-Type:"text/html"转换为Map类型格式 */ public class CustomMappingJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter { public CustomMappingJackson2HttpMessageConverter() { List<MediaType> mediaTypes = new ArrayList<MediaType>(); mediaTypes.add(MediaType.TEXT_PLAIN); mediaTypes.add(MediaType.TEXT_HTML); //加入text/html类型的支持 setSupportedMediaTypes(mediaTypes);// tag6 } } 文章来源:https://my.oschina.net/ccwwlx/blog/3129031
摘要:本篇博文是“Java秒杀系统实战系列文章”的第十篇,本篇博文我们将采用RabbitMQ的死信队列的方式处理“用户秒杀成功生成订单后,却迟迟没有支付”的情况,一起来见识一下RabbitMQ死信队列在实际业务环境下的强大之处! 内容:对于消息中间件RabbitMQ,Debug其实在前面的篇章中已经简单分享介绍过了,在这里就不再赘述了!在本文我们将采用RabbitMQ的死信队列实现这样的业务需求:“用户在秒杀成功并成功创建一笔订单记录后,理论上应该是执行去支付的操作,但是却存在着一种情况是用户迟迟不肯去支付~至于原因,不得而知!” 对于这种场景,各位小伙伴可以在一些商城平台体验一下,即挑选完商品,加入购物车后,点击去结算,这个时候会有个倒计时,提醒你需要在指定的时间内完成付款,否则订单将失效! 对于这种业务逻辑的处理,传统的做法是采用“定时器的方式”,定时轮询获取已经超过指定时间的订单,然后执行一系列的处理措施(比如再争取给用户发送短信,提醒超过多长时间订单就要失效了等等。。。),在这个秒杀系统中,我们将借助RabbitMQ死信队列这一组件,对该订单执行“失效”的措施! “死信队列”,顾明思议,是可以延时、延迟一定的时间再处理消息的一种特殊队列,它相对于“普通的队列”而言,可以实现“进入死信队列的消息不立即处理,而是可以等待一定的时间再进行处理”的功能!而普通的队列则不行,即进入队列后的消息会立即被对应的消费者监听消费,如下图所示为普通队列的基本消息模型: 而对于“死信队列”,它的构成以及使用相对而言比较复杂一点,在正常情况,死信队列由三大核心组件组成:死信交换机+死信路由+TTL(消息存活时间~非必需的),而死信队列又可以由“面向生产者的基本交换机+基本路由”绑定而成,故而生产者首先是将消息发送至“基本交换机+基本路由”所绑定而成的消息模型中,即间接性地进入到死信队列中,当过了TTL,消息将“挂掉”,从而进入下一个中转站,即“面下那个消费者的死信交换机+死信路由”所绑定而成的消息模型中。如下图所示: 下面,我们以实际的代码来构建死信队列的消息模型,并将此消息模型应用到秒杀系统的上述功能模块中。 (1)首先,需要在RabbitmqConfig配置类创建死信队列的消息模型,其完整的源代码如下所示: //构建秒杀成功之后-订单超时未支付的死信队列消息模型 @Bean public Queue successKillDeadQueue(){ Map<String, Object> argsMap= Maps.newHashMap(); argsMap.put("x-dead-letter-exchange",env.getProperty("mq.kill.item.success.kill.dead.exchange")); argsMap.put("x-dead-letter-routing-key",env.getProperty("mq.kill.item.success.kill.dead.routing.key")); return new Queue(env.getProperty("mq.kill.item.success.kill.dead.queue"),true,false,false,argsMap); } //基本交换机 @Bean public TopicExchange successKillDeadProdExchange(){ return new TopicExchange(env.getProperty("mq.kill.item.success.kill.dead.prod.exchange"),true,false); } //创建基本交换机+基本路由 -> 死信队列 的绑定 @Bean public Binding successKillDeadProdBinding(){ return BindingBuilder.bind(successKillDeadQueue()).to(successKillDeadProdExchange()).with(env.getProperty("mq.kill.item.success.kill.dead.prod.routing.key")); } //真正的队列 @Bean public Queue successKillRealQueue(){ return new Queue(env.getProperty("mq.kill.item.success.kill.dead.real.queue"),true); } //死信交换机 @Bean public TopicExchange successKillDeadExchange(){ return new TopicExchange(env.getProperty("mq.kill.item.success.kill.dead.exchange"),true,false); } //死信交换机+死信路由->真正队列 的绑定 @Bean public Binding successKillDeadBinding(){ return BindingBuilder.bind(successKillRealQueue()).to(successKillDeadExchange()).with(env.getProperty("mq.kill.item.success.kill.dead.routing.key")); } 其中,环境变量对象实例env读取的变量是配置在application.properties配置文件中的,取值如下所示: #订单超时未支付自动失效-死信队列消息模型 mq.kill.item.success.kill.dead.queue=${mq.env}.kill.item.success.kill.dead.queue mq.kill.item.success.kill.dead.exchange=${mq.env}.kill.item.success.kill.dead.exchange mq.kill.item.success.kill.dead.routing.key=${mq.env}.kill.item.success.kill.dead.routing.key mq.kill.item.success.kill.dead.real.queue=${mq.env}.kill.item.success.kill.dead.real.queue mq.kill.item.success.kill.dead.prod.exchange=${mq.env}.kill.item.success.kill.dead.prod.exchange mq.kill.item.success.kill.dead.prod.routing.key=${mq.env}.kill.item.success.kill.dead.prod.routing.key #单位为ms mq.kill.item.success.kill.expire=20000 (2)成功创建了消息模型之后,紧接着,我们需要在通用的RabbitMQ发送消息服务类RabbitSenderService中开发“发送消息入死信队列”的功能,在该功能方法中,我们指定了消息的存活时间TTL,取值为配置的变量:mq.kill.item.success.kill.expire 的值,即20s;其完整的源代码如下所示: //秒杀成功后生成抢购订单-发送信息入死信队列,等待着一定时间失效超时未支付的订单 public void sendKillSuccessOrderExpireMsg(final String orderCode){ try { if (StringUtils.isNotBlank(orderCode)){ KillSuccessUserInfo info=itemKillSuccessMapper.selectByCode(orderCode); if (info!=null){ rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter()); rabbitTemplate.setExchange(env.getProperty("mq.kill.item.success.kill.dead.prod.exchange")); rabbitTemplate.setRoutingKey(env.getProperty("mq.kill.item.success.kill.dead.prod.routing.key")); rabbitTemplate.convertAndSend(info, new MessagePostProcessor() { @Override public Message postProcessMessage(Message message) throws AmqpException { MessageProperties mp=message.getMessageProperties(); mp.setDeliveryMode(MessageDeliveryMode.PERSISTENT); mp.setHeader(AbstractJavaTypeMapper.DEFAULT_CONTENT_CLASSID_FIELD_NAME,KillSuccessUserInfo.class); //TODO:动态设置TTL(为了测试方便,暂且设置20s) mp.setExpiration(env.getProperty("mq.kill.item.success.kill.expire")); return message; } }); } } }catch (Exception e){ log.error("秒杀成功后生成抢购订单-发送信息入死信队列,等待着一定时间失效超时未支付的订单-发生异常,消息为:{}",orderCode,e.fillInStackTrace()); } } 从该“发送消息入死信队列”的代码中,我们可以看到,消息首先是先入到“基本交换机+基本路由”所绑定的死信队列的消息模型中的!当消息到了TTL,自然会从死信队列中出来(即“解脱了”),然后进入下一个中转站,即:“死信交换机+死信路由” 所绑定而成的真正队列的消息模型中,最终真正被消费者监听消费! 此时,可以将整个项目、系统运行在外置的tomcat服务器中,然后打开RabbitMQ后端控制台应用,找到该死信队列,可以看到该死信队列的详细信息,如下图所示: (3)最后,是需要在RabbitMQ通用的消息监听服务类RabbitReceiverService 中监听“真正队列”中的消息并进行处理:在这里我们是对该订单进行失效处理(前提是还没付款的情况下!),其完整的源代码如下所示: //用户秒杀成功后超时未支付-监听者 @RabbitListener(queues = {"${mq.kill.item.success.kill.dead.real.queue}"},containerFactory = "singleListenerContainer") public void consumeExpireOrder(KillSuccessUserInfo info){ try { log.info("用户秒杀成功后超时未支付-监听者-接收消息:{}",info); if (info!=null){ ItemKillSuccess entity=itemKillSuccessMapper.selectByPrimaryKey(info.getCode()); if (entity!=null && entity.getStatus().intValue()==0){ itemKillSuccessMapper.expireOrder(info.getCode()); } } }catch (Exception e){ log.error("用户秒杀成功后超时未支付-监听者-发生异常:",e.fillInStackTrace()); } } 其中,失效更新订单的记录的操作由 itemKillSuccessMapper.expireOrder(info.getCode()); 来实现,其对应的动态Sql的写法如下所示: <!--失效更新订单信息--> <update id="expireOrder"> UPDATE item_kill_success SET status = -1 WHERE code = #{code} AND status = 0 </update> (4)至此,关于RabbitMQ死信队列消息模型的代码实战已经完毕了!最后我只需要在“用户秒杀成功创建订单的那一刻,发送消息入死信队列”的地方调用即可,其调用代码如下所示: /** * 通用的方法-记录用户秒杀成功后生成的订单-并进行异步邮件消息的通知 * @param kill * @param userId * @throws Exception */ private void commonRecordKillSuccessInfo(ItemKill kill, Integer userId) throws Exception{ //TODO:记录抢购成功后生成的秒杀订单记录 ItemKillSuccess entity=new ItemKillSuccess(); String orderNo=String.valueOf(snowFlake.nextId()); //entity.setCode(RandomUtil.generateOrderCode()); //传统时间戳+N位随机数 entity.setCode(orderNo); //雪花算法 entity.setItemId(kill.getItemId()); entity.setKillId(kill.getId()); entity.setUserId(userId.toString()); entity.setStatus(SysConstant.OrderStatus.SuccessNotPayed.getCode().byteValue()); entity.setCreateTime(DateTime.now().toDate()); //TODO:学以致用,举一反三 -> 仿照单例模式的双重检验锁写法 if (itemKillSuccessMapper.countByKillUserId(kill.getId(),userId) <= 0){ int res=itemKillSuccessMapper.insertSelective(entity); if (res>0){ //TODO:进行异步邮件消息的通知=rabbitmq+mail rabbitSenderService.sendKillSuccessEmailMsg(orderNo); //TODO:入死信队列,用于 “失效” 超过指定的TTL时间时仍然未支付的订单 rabbitSenderService.sendKillSuccessOrderExpireMsg(orderNo); } } } 最后,是进行自测:点击“抢购”按钮,用户秒杀成功后,会发送一条消息入死信队列(这一点可以在RabbitMQ后端控制台中可以看到一条正Ready好的消息),等待20s,即可看到消息转移到真正的队列,并被真正的消费者监听消费,如下所示: 好了,关于“RabbitMQ死信队列”的介绍以及应用实战本文就暂且介绍到这里了,此种方式可以很灵活对“超时未支付的订单”,进行很好的处理,而且整个过程是“自动、自然”的,而无需人为去手动点击按钮触发了!当然啦,万事万物都并非十全十美的,死信队列也是如此,在一篇文章中我们将介绍此种方式的瑕疵之处,并采用相应的解决方案进行处理! 补充: 1、由于相应的博客的更新可能并不会很快,故而如果有想要快速入门以及实战整套系统的,可以参考阅读: Java商城秒杀系统的设计与实战视频教程(SpringBoot版) 2、目前,这一秒杀系统的整体构建与代码实战已经全部完成了,完整的源代码数据库地址可以来这里下载:https://gitee.com/steadyjack/SpringBoot-SecondKill
摘要:本篇博文是“Java秒杀系统实战系列文章”的第九篇,在这篇文章中我们将继续完善秒杀系统中的核心处理逻辑,即“用户秒杀~抢单”的业务逻辑!本文我们将基于JavaMail服务,开发一个通用的发送邮件服务,用于发送邮件通知消息,并与上一篇章中已经实现的RabbitMQ异步发送消息的逻辑进行整合,彻底实现“用户秒杀成功后,异步发送邮件通知消息给到用户邮箱,告知用户尽快进行付款”的功能! 内容:对于发送邮件服务,相信各位小伙伴并不陌生,本篇博文我们将开发一个通用的发送邮件服务,用于“用户秒杀成功之后异步发送邮件消息给到用户”。 (1)同样的道理,首先我们需要加入发送邮件服务的依赖,其依赖的版本号跟SpringBoot的版本号一直,为1.5.7.RELEASE,如下所示: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> <version>${spring-boot.version}</version> </dependency> 紧接着,我们需要在application.properties配置文件中加入“发送邮件服务”所需要的额外的支持配置信息: #发送邮件配置 spring.mail.host=smtp.qq.com spring.mail.username=1974544863@qq.com spring.mail.password=cmtvsjvhonkjdaje spring.mail.properties.mail.smtp.auth=true spring.mail.properties.mail.smtp.starttls.enable=true spring.mail.properties.mail.smtp.starttls.required=true mail.send.from=1974544863@qq.com mail.kill.item.success.subject=商品抢购成功 mail.kill.item.success.content=您好,您已成功抢购到商品: <strong style="color: red">%s</strong> ,复制该链接并在浏览器采用新的页面打开,即可查看抢购详情:${system.domain.url}/kill/record/detail/%s,并请您在1个小时内完成订单的支付,超时将失效该订单哦!祝你生活愉快! 在本秒杀系统中,发送邮件的服务我们是采用 QQ邮箱 作为主邮箱账号,相应的SMTP服务器也是采用QQ邮箱的!其中,spring.mail.password 指的是在QQ邮箱后台开通POP3/SMTP服务 时腾讯官方给的“密钥”(授权码),在这里,Debug就贡献了上面那个密钥~授权码 给各位使用了,而真正在企业生产环境中,其实是需要去申请一个主邮箱账号的,至于如何申请,在这里就不赘述了! (2)接下来,我们就可以大显伸手一番了!我们在MailService中开发了两种发送邮件的功能,一种发送简单文本的功能(即纯文字的、很死板、高冷风的那种),另一种是发送带HTML标签的花哨文本的功能(即带样式的、比较俏皮的、有温度的那种),如下所示: //通用的发送邮件服务 @Service @EnableAsync public class MailService { private static final Logger log= LoggerFactory.getLogger(MailService.class); @Autowired private JavaMailSender mailSender; @Autowired private Environment env; //发送简单文本文件 @Async public void sendSimpleEmail(final MailDto dto){ try { SimpleMailMessage message=new SimpleMailMessage(); message.setFrom(env.getProperty("mail.send.from")); message.setTo(dto.getTos()); message.setSubject(dto.getSubject()); message.setText(dto.getContent()); mailSender.send(message); log.info("发送简单文本文件-发送成功!"); }catch (Exception e){ log.error("发送简单文本文件-发生异常: ",e.fillInStackTrace()); } } //发送花哨邮件 @Async public void sendHTMLMail(final MailDto dto){ try { MimeMessage message=mailSender.createMimeMessage(); MimeMessageHelper messageHelper=new MimeMessageHelper(message,true,"utf-8"); messageHelper.setFrom(env.getProperty("mail.send.from")); messageHelper.setTo(dto.getTos()); messageHelper.setSubject(dto.getSubject()); messageHelper.setText(dto.getContent(),true); mailSender.send(message); log.info("发送花哨邮件-发送成功!"); }catch (Exception e){ log.error("发送花哨邮件-发生异常: ",e.fillInStackTrace()); } } } 其中,MailDto类主要统一封装了在发送邮件时所需要的字段信息,比如接收人、邮件标题、邮件内容等等(提现了面向对象的重要特性)!其源代码如下所示: /**统一封装了在发送邮件时所需要的字段信息 * @Author:debug (SteadyJack) * @Date: 2019/6/22 10:11 **/ @Data @ToString @AllArgsConstructor @NoArgsConstructor public class MailDto implements Serializable{ //邮件主题 private String subject; //邮件内容 private String content; //接收人 private String[] tos; } (3)最后是在“RabbitMQ通用的消息接收服务类” RabbitReceiverService 的接收消息逻辑中整合进“发送邮件服务”的逻辑,如下所示: @Autowired private MailService mailService; @Autowired private Environment env; /** * 秒杀异步邮件通知-接收消息 */ @RabbitListener(queues = {"${mq.kill.item.success.email.queue}"},containerFactory = "singleListenerContainer") public void consumeEmailMsg(KillSuccessUserInfo info){ try { log.info("秒杀异步邮件通知-接收消息:{}",info); //TODO:真正的发送邮件.... //简单文本 //MailDto dto=new MailDto(env.getProperty("mail.kill.item.success.subject"),"这是测试内容",new String[]{info.getEmail()}); //mailService.sendSimpleEmail(dto); //花哨文本 final String content=String.format(env.getProperty("mail.kill.item.success.content"),info.getItemName(),info.getCode()); MailDto dto=new MailDto(env.getProperty("mail.kill.item.success.subject"),content,new String[]{info.getEmail()}); mailService.sendHTMLMail(dto); }catch (Exception e){ log.error("秒杀异步邮件通知-接收消息-发生异常:",e.fillInStackTrace()); } } (4)至此,关于通用的发送邮件服务的代码实战,我们就介绍到这里了,接下来我们进入测试环节。点击“抢购”,如果用户秒杀成功,系统后端会在数据库录入一笔秒杀成功后的订单,同时user表中“邮箱字段值”对应的邮箱会受到一封邮件,如下图所示: 好了,欢乐的撸码时光总是短暂的,本篇文章我们就介绍到这里了!下篇博文我们将继续我们的“秒杀系统实战”之旅! 补充: 1、由于相应的博客的更新可能并不会很快,故而如果有想要快速入门以及实战整套系统的,可以阅读: Java商城秒杀系统的设计与实战视频教程(SpringBoot版) 2、目前,这一秒杀系统的整体构建与代码实战已经全部完成了,完整的源代码数据库地址可以来这里下载:https://gitee.com/steadyjack/SpringBoot-SecondKill
课程简介:本课程讲解的是一个真正意义上的、企业级的项目实战,主要介绍了企业级应用系统中后端应用权限的管理,其中主要涵盖了六大核心业务模块、十几张数据库表。 其中的核心业务模块主要包括用户模块、部门模块、岗位模块、角色模块、菜单模块和系统日志模块;与此同时,Debug还亲自撸了额外的附属模块,包括字典管理模块、商品分类模块以及考勤管理模块等等,主要是为了更好地巩固相应的技术栈以及企业应用系统业务模块的开发流程! 核心技术栈列表: 值得介绍的是,本课程在技术栈层面涵盖了前端和后端的大部分常用技术,包括Spring Boot、Spring MVC、Mybatis、Mybatis-Plus、Shiro(身份认证与资源授权跟会话等等)、Spring AOP、防止XSS攻击、防止SQL注入攻击、过滤器Filter、验证码Kaptcha、热部署插件Devtools、POI、Vue、LayUI、ElementUI、JQuery、HTML、Bootstrap、Freemarker、一键打包部署运行工具Wagon等等,如下图所示: 课程内容与收益: (1)学习完本课程之后,各位小伙伴将可以掌握企业应用系统权限管理平台的设计思想、流程,并掌握如何去构建一套最基本的、可付诸企业应用的权限系统; (2)从这一权限管理平台中,学会如何去对核心的业务模块进行拆分、设计、关联以及代码实战,并掌握如何基于现有的核心业务模块,快速开发项目中需要的其他业务模块,即套路以及规律的学习。 (3)学习掌握 任意一个业务模块 的前后端开发流程,学会如何从前端撸到后端再到数据库,最终交付出一个完整的功能模块; (4)掌握如何去搭建、重构一些通用的核心处理服务(比如通用的Service、处理工具类等等)、可复用的前端组件;同时,也可以掌握如何实现从需求分析 -> 数据库设计 -> 前后端与数据库开发 -> 一键打包上线部署运行 等“一条龙”的开发流程。 核心内容介绍:本课程是一门具有很强实践性质的“项目实战”课程,即“企业应用员工角色权限管理平台”,主要介绍了当前企业级应用系统中员工、部门、岗位、角色、权限、菜单以及其他实体模块的管理;其中,还重点讲解了如何基于Shiro的资源授权实现员工-角色-操作权限、员工-角色-数据权限的管理;在课程的最后,还介绍了如何实现一键打包上传部署运行项目等等。如下图所示为本权限管理平台的数据库设计图: 以下为项目整体的运行效果截图: 值得一提的是,在本课程中,Debug也向各位小伙伴介绍了如何在企业级应用系统业务模块的开发中,前端到后端再到数据库,最后再到服务器的上线部署运行等流程,如下图所示: 详细解说请阅读:https://www.roncoo.com/view/1159028108916490242
摘要: 本篇博文是“Java秒杀系统实战系列文章”的第八篇,在这篇文章中我们将整合消息中间件RabbitMQ,包括添加依赖、加入配置信息以及自定义注入相关操作组件,比如RabbitTemplate等等,最终初步实现消息的发送和接收,并在下一篇章将其与邮件服务整合,实现“用户秒杀成功发送邮件通知消息”的功能! 内容: 对于消息中间件RabbitMQ,想必各位小伙伴没有用过、也该有听过,它是一款目前市面上应用相当广泛的消息中间件,可以实现消息异步通信、业务服务模块解耦、接口限流、消息分发等功能,在微服务、分布式系统架构中可以说是充当着一名了不起的角色!(详细的介绍,Debug在这里就不赘述了,各位小伙伴可以上官网看看其更多的介绍及其典型的应用场景)! 在本篇博文中,我们将使用RabbitMQ充当消息发送的组件,将它与后面篇章介绍的“邮件服务”结合实现“用户秒杀成功后异步发送邮件通知消息,告知用户秒杀已经成功!”,下面我们一起进入代码实战吧。 (1)要使用RabbitMQ,前提得在本地开发环境或者服务器安装RabbitMQ服务,如下图所示为Debug在本地安装RabbitMQ服务成功后访问其后端控制台应用的首页:之后我们开始将其与SpringBoot进行整合。首先需要加入其依赖,其版本号跟SpringBoot的版本一致,版本号为1.5.7.RELEASE: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> <version>${spring-boot.version}</version> </dependency> 然后需要在配置文件application.properties中加入RabbitMQ服务相关的配置,比如其服务所在的Host、端口Port等等: #rabbitmq spring.rabbitmq.virtual-host=/ spring.rabbitmq.host=127.0.0.1 spring.rabbitmq.port=5672 spring.rabbitmq.username=guest spring.rabbitmq.password=guest spring.rabbitmq.listener.simple.concurrency=5 spring.rabbitmq.listener.simple.max-concurrency=15 spring.rabbitmq.listener.simple.prefetch=10 (2)紧接着,我们借助SpringBoot天然具有的一些特性,自动注入RabbitMQ一些组件的配置,包括其“单一实例消费者”配置、“多实例消费者”配置以及用于发送消息的操作组件实例“RabbitTemplate”的配置: //通用化 Rabbitmq 配置 @Configuration public class RabbitmqConfig { private final static Logger log = LoggerFactory.getLogger(RabbitmqConfig.class); @Autowired private Environment env; @Autowired private CachingConnectionFactory connectionFactory; @Autowired private SimpleRabbitListenerContainerFactoryConfigurer factoryConfigurer; //单一消费者 @Bean(name = "singleListenerContainer") public SimpleRabbitListenerContainerFactory listenerContainer(){ SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); factory.setConnectionFactory(connectionFactory); factory.setMessageConverter(new Jackson2JsonMessageConverter()); factory.setConcurrentConsumers(1); factory.setMaxConcurrentConsumers(1); factory.setPrefetchCount(1); factory.setTxSize(1); return factory; } //多个消费者 @Bean(name = "multiListenerContainer") public SimpleRabbitListenerContainerFactory multiListenerContainer(){ SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); factoryConfigurer.configure(factory,connectionFactory); factory.setMessageConverter(new Jackson2JsonMessageConverter()); //确认消费模式-NONE factory.setAcknowledgeMode(AcknowledgeMode.NONE); factory.setConcurrentConsumers(env.getProperty("spring.rabbitmq.listener.simple.concurrency",int.class)); factory.setMaxConcurrentConsumers(env.getProperty("spring.rabbitmq.listener.simple.max-concurrency",int.class)); factory.setPrefetchCount(env.getProperty("spring.rabbitmq.listener.simple.prefetch",int.class)); return factory; } @Bean public RabbitTemplate rabbitTemplate(){ connectionFactory.setPublisherConfirms(true); connectionFactory.setPublisherReturns(true); RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory); rabbitTemplate.setMandatory(true); rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() { @Override public void confirm(CorrelationData correlationData, boolean ack, String cause) { log.info("消息发送成功:correlationData({}),ack({}),cause({})",correlationData,ack,cause); } }); rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() { @Override public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) { log.warn("消息丢失:exchange({}),route({}),replyCode({}),replyText({}),message:{}",exchange,routingKey,replyCode,replyText,message); } }); return rabbitTemplate; } } 在RabbitMQ的消息发送组件RabbitTemplate的配置中,我们还特意加入了“消息发送确认”、“消息丢失回调”的输出配置,即当消息正确进入到队列后,即代表消息发送成功;当消息找不到对应的队列(在某种程度上,其实也就是找不到交换机和路由)时,会输出消息丢失。 (3)完了之后,我们准备开始使用RabbitMQ实现消息的发送和接收。首先,我们需要在RabbitmqConfig配置类中创建队列、交换机、路由以及绑定等Bean组件,如下所示: //构建异步发送邮箱通知的消息模型 @Bean public Queue successEmailQueue(){ return new Queue(env.getProperty("mq.kill.item.success.email.queue"),true); } @Bean public TopicExchange successEmailExchange(){ return new TopicExchange(env.getProperty("mq.kill.item.success.email.exchange"),true,false); } @Bean public Binding successEmailBinding(){ return BindingBuilder.bind(successEmailQueue()).to(successEmailExchange()).with(env.getProperty("mq.kill.item.success.email.routing.key")); } 其中,环境变量实例env读取的那些属性我们是配置在application.properties文件中的,如下所示: mq.env=test #秒杀成功异步发送邮件的消息模型 mq.kill.item.success.email.queue=${mq.env}.kill.item.success.email.queue mq.kill.item.success.email.exchange=${mq.env}.kill.item.success.email.exchange mq.kill.item.success.email.routing.key=${mq.env}.kill.item.success.email.routing.key 紧接着,我们需要在通用的消息发送服务类 RabbitSenderService 中写一段发送消息的方法,该方法用于接收“订单编号”参数,然后在数据库中查询其对应的详细订单记录,将该记录充当“消息”并发送至RabbitMQ的队列中,等待被监听消费: /** * RabbitMQ通用的消息发送服务 * @Author:debug (SteadyJack) * @Date: 2019/6/21 21:47 **/ @Service public class RabbitSenderService { public static final Logger log= LoggerFactory.getLogger(RabbitSenderService.class); @Autowired private RabbitTemplate rabbitTemplate; @Autowired private Environment env; @Autowired private ItemKillSuccessMapper itemKillSuccessMapper; //秒杀成功异步发送邮件通知消息 public void sendKillSuccessEmailMsg(String orderNo){ log.info("秒杀成功异步发送邮件通知消息-准备发送消息:{}",orderNo); try { if (StringUtils.isNotBlank(orderNo)){ KillSuccessUserInfo info=itemKillSuccessMapper.selectByCode(orderNo); if (info!=null){ //TODO:rabbitmq发送消息的逻辑 rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter()); rabbitTemplate.setExchange(env.getProperty("mq.kill.item.success.email.exchange")); rabbitTemplate.setRoutingKey(env.getProperty("mq.kill.item.success.email.routing.key")); //TODO:将info充当消息发送至队列 rabbitTemplate.convertAndSend(info, new MessagePostProcessor() { @Override public Message postProcessMessage(Message message) throws AmqpException { MessageProperties messageProperties=message.getMessageProperties(); messageProperties.setDeliveryMode(MessageDeliveryMode.PERSISTENT); messageProperties.setHeader(AbstractJavaTypeMapper.DEFAULT_CONTENT_CLASSID_FIELD_NAME,KillSuccessUserInfo.class); return message; } }); } } }catch (Exception e){ log.error("秒杀成功异步发送邮件通知消息-发生异常,消息为:{}",orderNo,e.fillInStackTrace()); } } } (4)最后,是在通用的消息接收服务类RabbitReceiverService中实现消息的接收,其完整的源代码如下所示: /** * RabbitMQ通用的消息接收服务 * @Author:debug (SteadyJack) * @Date: 2019/6/21 21:47 **/ @Service public class RabbitReceiverService { public static final Logger log= LoggerFactory.getLogger(RabbitReceiverService.class); @Autowired private MailService mailService; @Autowired private Environment env; @Autowired private ItemKillSuccessMapper itemKillSuccessMapper; //秒杀异步邮件通知-接收消息 @RabbitListener(queues = {"${mq.kill.item.success.email.queue}"},containerFactory = "singleListenerContainer") public void consumeEmailMsg(KillSuccessUserInfo info){ try { log.info("秒杀异步邮件通知-接收消息:{}",info); //到时候这里将整合邮件服务发送邮件通知消息的逻辑 }catch (Exception e){ log.error("秒杀异步邮件通知-接收消息-发生异常:",e.fillInStackTrace()); } } } 至此,关于SpringBoot整合消息中间件RabbitMQ的代码实战,本篇文章就介绍到这里了。 最后一点,我们需要进行测试,即用户在界面发起“抢购”的请求操作之后,如果能秒杀成功,则RabbitMQ会发送、接收一条消息,如下所示:好了,关于RabbitMQ的使用,本文到此就暂且告一段落了,在下一篇文章中我们将把它与邮件服务进行整合,实现“用户秒杀成功后异步发送邮件通知消息给到用户邮箱”的功能!除此之外,我们还将在后面的篇章介绍“如何使用RabbitMQ的死信队列,处理用户下单成功后却超时未支付的订单~在那里我们将采取失效的操作”。 补充: 1、由于相应的博客的更新可能并不会很快,故而如果有想要快速入门以及实战整套系统的,可以阅读: Java商城秒杀系统的设计与实战视频教程(SpringBoot版) 2、目前,这一秒杀系统的整体构建与代码实战已经全部完成了,完整的源代码数据库地址可以来这里下载:https://gitee.com/steadyjack/SpringBoot-SecondKill
摘要: 本篇博文是“Java秒杀系统实战系列文章”的第七篇,在本博文中我们将重点介绍 “在高并发,如秒杀的业务场景下如何生成全局唯一、趋势递增的订单编号”,我们将介绍两种方法,一种是传统的采用随机数生成的方式,另外一种是采用当前比较流行的“分布式唯一ID生成算法-雪花算法”来实现。 内容: 在上一篇博文,我们完成了商品秒杀业务逻辑的代码实战,在该代码中,我们还实现了“当用户秒杀成功后,需要在数据库表中为其生成一笔秒杀成功的订单记录”的功能,其对应的代码如下所示: private void commonRecordKillSuccessInfo(ItemKill kill, Integer userId) throws Exception{ //TODO:记录抢购成功后生成的秒杀订单记录 ItemKillSuccess entity=new ItemKillSuccess(); //此处为订单编号的生成逻辑 String orderNo=String.valueOf(snowFlake.nextId()); //entity.setCode(RandomUtil.generateOrderCode()); //传统时间戳+N位随机数 entity.setCode(orderNo); //雪花算法 entity.setItemId(kill.getItemId()); entity.setKillId(kill.getId()); entity.setUserId(userId.toString()); entity.setStatus(SysConstant.OrderStatus.SuccessNotPayed.getCode().byteValue()); entity.setCreateTime(DateTime.now().toDate()); //TODO:学以致用,举一反三 -> 仿照单例模式的双重检验锁写法 if (itemKillSuccessMapper.countByKillUserId(kill.getId(),userId) <= 0){ int res=itemKillSuccessMapper.insertSelective(entity); //其他逻辑省略 } } 在该实现逻辑中,其核心要点在于“在高并发的环境下,如何高效的生成订单编号”,那么如何才算是高效呢?Debug认为应该满足以下两点: (1)保证订单编号的生成逻辑要快、稳定,减少时延 (2)要保证生成的订单编号全局唯一、不重复、趋势递增、有时序性 下面,我们采用两种方式来生成“订单编号”,并自己写一个多线程的程序模拟生成的订单编号是否满足条件。 值得一提的是,为了能直观的观察多线程并发生成的订单编号是否具有唯一性、趋势递增,在这里Debug借助了一张数据库表 random_code 来存储生成的订单编号,其DDL如下所示: CREATE TABLE `random_code` ( `id` int(11) NOT NULL AUTO_INCREMENT, `code` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `idx_code` (`code`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 从该数据库表数据结构定义语句中可以看出,我们设定了 订单编号字段code 为唯一!所以如果高并发多线程生成的订单编号出现重复,那么在插入数据库表的时候必然会出现错误 下面,首先开始我们的第一种方式吧:基于随机数的方式生成订单编号 (1)首先是建立一个Thread类,其run方法的执行逻辑为生成订单编号,并将生成的订单编号插入数据库表中,其代码如下所示: /** * 随机数生成的方式-Thread * @Author:debug (SteadyJack) * @Date: 2019/7/11 10:30 **/ public class CodeGenerateThread implements Runnable{ private RandomCodeMapper randomCodeMapper; public CodeGenerateThread(RandomCodeMapper randomCodeMapper) { this.randomCodeMapper = randomCodeMapper; } @Override public void run() { //生成订单编号并插入数据库 RandomCode entity=new RandomCode(); entity.setCode(RandomUtil.generateOrderCode()); randomCodeMapper.insertSelective(entity); } } 其中,RandomUtil.generateOrderCode()的生成逻辑是借助ThreadLocalRandom来实现的,其完整的源代码如下所示: /** * 随机数生成util * @Author:debug (SteadyJack) * @Date: 2019/6/20 21:05 **/ public class RandomUtil { private static final SimpleDateFormat dateFormatOne=new SimpleDateFormat("yyyyMMddHHmmssSS"); private static final ThreadLocalRandom random=ThreadLocalRandom.current(); //生成订单编号-方式一 public static String generateOrderCode(){ //TODO:时间戳+N为随机数流水号 return dateFormatOne.format(DateTime.now().toDate()) + generateNumber(4); } //N为随机数流水号 public static String generateNumber(final int num){ StringBuffer sb=new StringBuffer(); for (int i=1;i<=num;i++){ sb.append(random.nextInt(9)); } return sb.toString(); } } (2)紧接着是在 BaseController控制器 中开发一个请求方法,目的正是用来模拟前端高并发触发产生多线程并生成订单编号的逻辑,在这里我们暂且用1000个线程进行模拟,其源代码如下所示: @Autowired private RandomCodeMapper randomCodeMapper; //测试在高并发下多线程生成订单编号-传统的随机数生成方法 @RequestMapping(value = "/code/generate/thread",method = RequestMethod.GET) public BaseResponse codeThread(){ BaseResponse response=new BaseResponse(StatusCode.Success); try { ExecutorService executorService=Executors.newFixedThreadPool(10); for (int i=0;i<1000;i++){ executorService.execute(new CodeGenerateThread(randomCodeMapper)); } }catch (Exception e){ response=new BaseResponse(StatusCode.Fail.getCode(),e.getMessage()); } return response; } (3)完了之后,就可以将整个项目、系统运行在外置的tomcat中了,然后打开postman,发起一个Http的Get请求,请求链接为:http://127.0.0.1:8092/kill/base/code/generate/thread ,仔细观察控制台的输出信息,会看一些令自己躁动不安的东西:竟然会出现“重复生成了重复的订单编号”!而且,打开数据库表进行观察,会发现“他娘的1000个线程生成订单编号,竟然只有900多个记录”,这就说明了这么多个线程在执行生成订单编号的逻辑期间出现了“重复的订单编号”!如下图所示:因此,此种基于随机数生成唯一ID或者订单编号的方式,我们是可以Pass掉了(当然啦,在并发量不是很高的情况下,这种方式还是阔以使用的,因为简单而且易于理解啊!) 鉴于此种“基于随机数生成”的方式在高并发的场景下并不符合我们的要求,接下来,我们将介绍另外一种比较流行的、典型的方式,即“分布式唯一ID生成算法-雪花算法”来实现。 对于“雪花算法”的介绍,各位小伙伴可以参考Github上的这一链接,我觉得讲得还是挺清晰的:https://github.com/souyunku/SnowFlake ,详细的Debug在这里就不赘述了,下面截取了部分概述:SnowFlake算法在分布式的环境下,之所以能高效率的生成唯一的ID,我觉得其中很重要的一点在于其底层的实现是通过“位运算”来实现的,简单来讲,就是直接跟机器打交道!其底层数据的存储结构(64位)如下图所示:下面,我们就直接基于雪花算法来生成秒杀系统中需要的订单编号吧! (1)同样的道理,我们首先定义一个Thread类,其run方法的实现逻辑是借助雪花算法生成订单编号并将其插入到数据库中。 /** 基于雪花算法生成全局唯一的订单编号并插入数据库表中 * @Author:debug (SteadyJack) * @Date: 2019/7/11 10:30 **/ public class CodeGenerateSnowThread implements Runnable{ private static final SnowFlake SNOW_FLAKE=new SnowFlake(2,3); private RandomCodeMapper randomCodeMapper; public CodeGenerateSnowThread(RandomCodeMapper randomCodeMapper) { this.randomCodeMapper = randomCodeMapper; } @Override public void run() { RandomCode entity=new RandomCode(); //采用雪花算法生成订单编号 entity.setCode(String.valueOf(SNOW_FLAKE.nextId())); randomCodeMapper.insertSelective(entity); } } 其中,SNOW_FLAKE.nextId() 的方法正是采用雪花算法生成全局唯一的订单编号的逻辑,其完整的源代码如下所示: /** * 雪花算法 * @author: zhonglinsen * @date: 2019/5/20 */ public class SnowFlake { //起始的时间戳 private final static long START_STAMP = 1480166465631L; //每一部分占用的位数 private final static long SEQUENCE_BIT = 12; //序列号占用的位数 private final static long MACHINE_BIT = 5; //机器标识占用的位数 private final static long DATA_CENTER_BIT = 5;//数据中心占用的位数 //每一部分的最大值 private final static long MAX_DATA_CENTER_NUM = -1L ^ (-1L << DATA_CENTER_BIT); private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT); private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT); //每一部分向左的位移 private final static long MACHINE_LEFT = SEQUENCE_BIT; private final static long DATA_CENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT; private final static long TIMESTAMP_LEFT = DATA_CENTER_LEFT + DATA_CENTER_BIT; private long dataCenterId; //数据中心 private long machineId; //机器标识 private long sequence = 0L; //序列号 private long lastStamp = -1L;//上一次时间戳 public SnowFlake(long dataCenterId, long machineId) { if (dataCenterId > MAX_DATA_CENTER_NUM || dataCenterId < 0) { throw new IllegalArgumentException("dataCenterId can't be greater than MAX_DATA_CENTER_NUM or less than 0"); } if (machineId > MAX_MACHINE_NUM || machineId < 0) { throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0"); } this.dataCenterId = dataCenterId; this.machineId = machineId; } //产生下一个ID public synchronized long nextId() { long currStamp = getNewStamp(); if (currStamp < lastStamp) { throw new RuntimeException("Clock moved backwards. Refusing to generate id"); } if (currStamp == lastStamp) { //相同毫秒内,序列号自增 sequence = (sequence + 1) & MAX_SEQUENCE; //同一毫秒的序列数已经达到最大 if (sequence == 0L) { currStamp = getNextMill(); } } else { //不同毫秒内,序列号置为0 sequence = 0L; } lastStamp = currStamp; return (currStamp - START_STAMP) << TIMESTAMP_LEFT //时间戳部分 | dataCenterId << DATA_CENTER_LEFT //数据中心部分 | machineId << MACHINE_LEFT //机器标识部分 | sequence; //序列号部分 } private long getNextMill() { long mill = getNewStamp(); while (mill <= lastStamp) { mill = getNewStamp(); } return mill; } private long getNewStamp() { return System.currentTimeMillis(); } } (2)紧接着,我们在BaseController中开发一个请求方法,用于模拟前端触发高并发产生多线程抢单的场景。 /** * 测试在高并发下多线程生成订单编号-雪花算法 * @return */ @RequestMapping(value = "/code/generate/thread/snow",method = RequestMethod.GET) public BaseResponse codeThreadSnowFlake(){ BaseResponse response=new BaseResponse(StatusCode.Success); try { ExecutorService executorService=Executors.newFixedThreadPool(10); for (int i=0;i<1000;i++){ executorService.execute(new CodeGenerateSnowThread(randomCodeMapper)); } }catch (Exception e){ response=new BaseResponse(StatusCode.Fail.getCode(),e.getMessage()); } return response; } (3)完了之后,我们采用Postman发起一个Http的Get请求,其请求链接如下所示:http://127.0.0.1:8092/kill/base/code/generate/thread/snow ,观察控制台的输出信息,可以看到“一片安然的景象”,再观察数据库表的记录,可以发现,1000个线程成功触发生成了1000个对应的订单编号,如下图所示:除此之外,各位小伙伴还可以将线程数从1000调整为10000、100000甚至1000000,然后观察控制台的输出信息以及数据库表的记录等等。 Debug亲测了1w跟10w的场景下是木有问题的,100w的线程数的测试就交给各位小伙伴去试试了(时间比较长,要有心理准备哦!)至此,我们就可以将雪花算法生成全局唯一的订单编号的逻辑应用到我们的“秒杀处理逻辑”中,即其代码(在KillService的commonRecordKillSuccessInfo方法中)如下所示: ItemKillSuccess entity=new ItemKillSuccess(); String orderNo=String.valueOf(snowFlake.nextId());//雪花算法 entity.setCode(orderNo); //其他代码省略 补充:1、由于相应的博客的更新可能并不会很快,故而如果有想要快速入门以及实战整套系统的,可以阅读: Java商城秒杀系统的设计与实战视频教程(SpringBoot版) 2、目前,这一秒杀系统的整体构建与代码实战已经全部完成了,完整的源代码数据库地址可以来这里下载:https://gitee.com/steadyjack/SpringBoot-SecondKill
摘要: 本篇博文是“Java秒杀系统实战系列文章”的第六篇,本篇博文我们将进入整个秒杀系统核心功能模块的代码开发,即“商品秒杀”功能模块的代码实战。 内容: “商品秒杀”功能模块是建立在“商品详情”功能模块的基础之上,对于这一功能模块而言,其主要的核心流程在于:前端发起抢购请求,该请求将携带着一些请求数据:待秒杀Id跟当前用户Id等数据;后端接口在接收到请求之后,将执行一系列的判断与秒杀处理逻辑,最终将处理结果返回给到前端。 其中,后端接口的这一系列判断与秒杀处理逻辑还是挺复杂的,Debug将其绘制成了如下的流程图: 从该业务流程图中可以看出,后端接口在接收前端用户的秒杀请求时,其核心处理逻辑为: (1)首先判断当前用户是否已经抢购过该商品了,如果否,则代表用户没有抢购过该商品,可以进入下一步的处理逻辑 (2)判断该商品可抢的剩余数量,即库存是否充足(即是否大于0),如果是,则进入下一步的处理逻辑 (3)扣减库存,并更新数据库的中对应抢购记录的库存(一般是减一操作),判断更新库存的数据库操作是否成功了,如果是,则创建用户秒杀成功的订单,并异步发送短信或者邮件通知信息通知用户 (4)以上的操作逻辑如果有任何一步是不满足条件的,则直接结束整个秒杀的流程,即秒杀失败! 接下来,我们仍然基于MVC的开发模式,采用代码实战实现这一功能模块! (1)首先是在KillController 控制器开发接收“前端用户秒杀请求”的功能方法,其中,该方法需要接收前端请求过来的“待秒杀Id”,而当前用户的Id可以通过上一篇博文介绍的Shiro 的会话模块Session进行获取! 其源代码如下所示: @Autowired private IKillService killService; @Autowired private ItemKillSuccessMapper itemKillSuccessMapper; /*** * 商品秒杀核心业务逻辑 */ @RequestMapping(value = prefix+"/execute",method = RequestMethod.POST,consumes = MediaType.APPLICATION_JSON_UTF8_VALUE) @ResponseBody public BaseResponse execute(@RequestBody @Validated KillDto dto, BindingResult result, HttpSession session){ if (result.hasErrors() || dto.getKillId()<=0){ return new BaseResponse(StatusCode.InvalidParams); } //获取当前登录用户的信息 Object uId=session.getAttribute("uid"); if (uId==null){ return new BaseResponse(StatusCode.UserNotLogin); } Integer userId= (Integer)uId ; BaseResponse response=new BaseResponse(StatusCode.Success); try { Boolean res=killService.killItem(dto.getKillId(),userId); if (!res){ return new BaseResponse(StatusCode.Fail.getCode(),"哈哈~商品已抢购完毕或者不在抢购时间段哦!"); } }catch (Exception e){ response=new BaseResponse(StatusCode.Fail.getCode(),e.getMessage()); } return response; } 其中,KillDto对象主要封装了“待秒杀Id”等字段信息,其主要用于接收前端过来的用户秒杀请求信息,源代码如下所示: @Data @ToString public class KillDto implements Serializable{ @NotNull private Integer killId; private Integer userId; //在整合shiro之后,userId字段可以不需要了!因为通过session进行获取了 } (2)紧接着是开发 killService.killItem(dto.getKillId(),userId) 的功能,该功能对应的代码的编写逻辑可以参见本文刚开始介绍时的流程图!其完整源代码如下所示: @Autowired private ItemKillSuccessMapper itemKillSuccessMapper; @Autowired private ItemKillMapper itemKillMapper; @Autowired private RabbitSenderService rabbitSenderService; //商品秒杀核心业务逻辑的处理 @Override public Boolean killItem(Integer killId, Integer userId) throws Exception { Boolean result=false; //TODO:判断当前用户是否已经抢购过当前商品 if (itemKillSuccessMapper.countByKillUserId(killId,userId) <= 0){ //TODO:查询待秒杀商品详情 ItemKill itemKill=itemKillMapper.selectById(killId); //TODO:判断是否可以被秒杀canKill=1? if (itemKill!=null && 1==itemKill.getCanKill() ){ //TODO:扣减库存-减一 int res=itemKillMapper.updateKillItem(killId); //TODO:扣减是否成功?是-生成秒杀成功的订单,同时通知用户秒杀成功的消息 if (res>0){ commonRecordKillSuccessInfo(itemKill,userId); result=true; } } }else{ throw new Exception("您已经抢购过该商品了!"); } return result; } 其中,itemKillMapper.selectById(killId); 表示用于获取待秒杀商品的详情信息,这在前面的篇章中已经介绍过了;而 itemKillMapper.updateKillItem(killId); 主要用于扣减库存(在这里是减1操作),其对应的动态Sql如下所示: <!--抢购商品,剩余数量减一--> <update id="updateKillItem"> UPDATE item_kill SET total = total - 1 WHERE id = #{killId} </update> (3)值得一提的是,在上面 KillService执行killItem功能方法时,还开发了一个通用的方法:用户秒杀成功后创建秒杀订单、并异步发送通知消息给到用户秒杀成功的信息!该方法为 commonRecordKillSuccessInfo(itemKill,userId); 其完整的源代码如下所示: /** * 通用的方法-用户秒杀成功后创建订单-并进行异步邮件消息的通知 * @param kill * @param userId * @throws Exception */ private void commonRecordKillSuccessInfo(ItemKill kill, Integer userId) throws Exception{ //TODO:记录抢购成功后生成的秒杀订单记录 ItemKillSuccess entity=new ItemKillSuccess(); String orderNo=String.valueOf(snowFlake.nextId()); //entity.setCode(RandomUtil.generateOrderCode()); //传统时间戳+N位随机数 entity.setCode(orderNo); //雪花算法 entity.setItemId(kill.getItemId()); entity.setKillId(kill.getId()); entity.setUserId(userId.toString()); entity.setStatus(SysConstant.OrderStatus.SuccessNotPayed.getCode().byteValue()); entity.setCreateTime(DateTime.now().toDate()); //TODO:学以致用,举一反三 -> 仿照单例模式的双重检验锁写法 if (itemKillSuccessMapper.countByKillUserId(kill.getId(),userId) <= 0){ int res=itemKillSuccessMapper.insertSelective(entity); if (res>0){ //TODO:进行异步邮件消息的通知=rabbitmq+mail rabbitSenderService.sendKillSuccessEmailMsg(orderNo); //TODO:入死信队列,用于 “失效” 超过指定的TTL时间时仍然未支付的订单 rabbitSenderService.sendKillSuccessOrderExpireMsg(orderNo); } } } 该方法涉及的功能模块稍微比较多,即主要包含了“分布式唯一ID-雪花算法的应用”、“整合RabbitMQ异步发送通知消息给用户”、“基于JavaMail开发发送邮件的功能”、“死信队列失效超时未支付的订单”等等,这些功能模块将在后面的小节一步一步展开进行介绍! (4)最后是需要在前端页面info.jsp开发“提交用户秒杀请求”的功能,其部分核心源代码如下所示: 其中,提交的数据是采用application/json的格式提交的,即json的格式!并采用POST的请求方法进行交互! (5)将整个系统、项目采用外置的tomcat运行起来,观察控制台的输出信息,如果没有报错信息,则代表整体的实战代码没有语法级别的错误!点击“详情”按钮,登录成功后,进入“待秒杀商品的的详情”,可以查看当前待秒杀商品的详情信息;点击“抢购”按钮,即可进入“秒杀”环节,后端经过一系列的逻辑处理之后,将处理的结果返回给到前端,如下图所示: 与此同时,当前用户的邮箱中将收到一条“秒杀成功”的邮件信息,表示当前用户已经成功秒杀抢到当前商品了,如下图所示: 除此之外,在数据库表item_kill_success中也将会生成一笔“秒杀成功的订单记录”,如下图所示: 当然,对于“邮件的通知”和“秒杀成功生成的订单的订单编号”的功能,我们将在后面的篇章进行分享介绍,在本节我们主要是分享介绍了秒杀系统中用户的“秒杀/抢购请求”功能! 补充: 1、由于相应的博客的更新可能并不会很快,故而如果有想要快速入门以及实战整套系统的,可以阅读: Java商城秒杀系统的设计与实战视频教程(SpringBoot版) 2、目前,这一秒杀系统的整体构建与代码实战已经全部完成了,完整的源代码数据库地址可以来这里下载:https://gitee.com/steadyjack/SpringBoot-SecondKill
摘要: 本篇博文是“Java秒杀系统实战系列文章”的第五篇,在本篇博文中,我们将整合权限认证-授权框架Shiro,实现用户的登陆认证功能,主要用于:要求用户在抢购商品或者秒杀商品时,限制用户进行登陆!并对于特定的url(比如抢购请求对应的url)进行过滤(即当用户访问指定的url时,需要要求用户进行登陆)。 内容: 对于Shiro,相信各位小伙伴应该听说过,甚至应该也使用过!简单而言,它是一个很好用的用户身份认证、权限授权框架,可以实现用户登录认证,权限、资源授权、会话管理等功能,在本秒杀系统中,我们将主要采用该框架实现对用户身份的认证和用户的登录功能。 值得一提的是,本篇博文介绍的“Shiro实现用户登录认证”功能模块涉及到的数据库表为用户信息表user,下面进入代码实战环节。 <!--shiro权限控制--> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>${shiro.version}</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>${shiro.version}</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-web</artifactId> <version>${shiro.version}</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>${shiro.version}</version> </dependency> (2)紧接着是在UserController控制器中开发用户前往登录、用户登录以及用户退出登录的请求对应的功能方法,其完整的源代码如下所示: @Autowired private Environment env; //跳到登录页 @RequestMapping(value = {"/to/login","/unauth"}) public String toLogin(){ return "login"; } //登录认证 @RequestMapping(value = "/login",method = RequestMethod.POST) public String login(@RequestParam String userName, @RequestParam String password, ModelMap modelMap){ String errorMsg=""; try { if (!SecurityUtils.getSubject().isAuthenticated()){ String newPsd=new Md5Hash(password,env.getProperty("shiro.encrypt.password.salt")).toString(); UsernamePasswordToken token=new UsernamePasswordToken(userName,newPsd); SecurityUtils.getSubject().login(token); } }catch (UnknownAccountException e){ errorMsg=e.getMessage(); modelMap.addAttribute("userName",userName); }catch (DisabledAccountException e){ errorMsg=e.getMessage(); modelMap.addAttribute("userName",userName); }catch (IncorrectCredentialsException e){ errorMsg=e.getMessage(); modelMap.addAttribute("userName",userName); }catch (Exception e){ errorMsg="用户登录异常,请联系管理员!"; e.printStackTrace(); } if (StringUtils.isBlank(errorMsg)){ return "redirect:/index"; }else{ modelMap.addAttribute("errorMsg",errorMsg); return "login"; } } //退出登录 @RequestMapping(value = "/logout") public String logout(){ SecurityUtils.getSubject().logout(); return "login"; } 其中,在匹配用户的密码时,我们在这里采用的Md5Hash的方法,即MD5加密的方式进行匹配(因为数据库的user表中用户的密码字段存储的正是采用MD5加密后的加密串) 前端页面login.jsp的内容比较简单,只需要用户输入最基本的用户名和密码即可,如下图所示为该页面的部分核心源代码:当前端提交“用户登录”请求时,将以“提交表单”的形式将用户名、密码提交到后端UserController控制器对应的登录方法中,该方法首先会进行最基本的参数判断与校验,校验通过之后,会调用Shiro内置的组件SecurityUtils.getSubject().login()方法执行登录操作,其中的登录操作将主要在 “自定义的Realm的doGetAuthenticationInfo方法”中执行。 (3)接下来是基于Shiro的AuthorizingRealm,开发自定义的Realm,并实现其中的用户登录认证方法,即doGetAuthenticationInfo()方法。其完整的源代码如下所示: * 用户自定义的realm-用于shiro的认证、授权 * @Author:debug (SteadyJack) * @Date: 2019/7/2 17:55 **/ public class CustomRealm extends AuthorizingRealm{ private static final Logger log= LoggerFactory.getLogger(CustomRealm.class); private static final Long sessionKeyTimeOut=3600_000L; @Autowired private UserMapper userMapper; //授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { return null; } //认证-登录 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { UsernamePasswordToken token= (UsernamePasswordToken) authenticationToken; String userName=token.getUsername(); String password=String.valueOf(token.getPassword()); log.info("当前登录的用户名={} 密码={} ",userName,password); User user=userMapper.selectByUserName(userName); if (user==null){ throw new UnknownAccountException("用户名不存在!"); } if (!Objects.equals(1,user.getIsActive().intValue())){ throw new DisabledAccountException("当前用户已被禁用!"); } if (!user.getPassword().equals(password)){ throw new IncorrectCredentialsException("用户名密码不匹配!"); } SimpleAuthenticationInfo info=new SimpleAuthenticationInfo(user.getUserName(),password,getName()); setSession("uid",user.getId()); return info; } /** * 将key与对应的value塞入shiro的session中-最终交给HttpSession进行管理(如果是分布式session配置,那么就是交给redis管理) * @param key * @param value */ private void setSession(String key,Object value){ Session session=SecurityUtils.getSubject().getSession(); if (session!=null){ session.setAttribute(key,value); session.setTimeout(sessionKeyTimeOut); } } } 其中,userMapper.selectByUserName(userName);主要用于根据userName查询用户实体信息,其对应的动态Sql的写法如下所示: <!--根据用户名查询--> <select id="selectByUserName" resultType="com.debug.kill.model.entity.User"> SELECT <include refid="Base_Column_List"/> FROM user WHERE user_name = #{userName} </select> 值得一提的是,当用户登录成功时(即用户名和密码的取值跟数据库的user表相匹配),我们会借助Shiro的Session会话机制将当前用户的信息存储至服务器会话中,并缓存一定时间!(在这里是3600s,即1个小时)! (4)最后是我们需要实现“用户在访问待秒杀商品详情或者抢购商品或者任何需要进行拦截的业务请求时,如何自动检测用户是否处于登录的状态?如果已经登录,则直接进入业务请求对应的方法逻辑,否则,需要前往用户登录页要求用户进行登录”。 基于这样的需求,我们需要借助Shiro的组件ShiroFilterFactoryBean 实现“用户是否登录”的判断,以及借助FilterChainDefinitionMap拦截一些需要授权的链接URL,其完整的源代码如下所示: /** * shiro的通用化配置 * @Author:debug (SteadyJack) * @Date: 2019/7/2 17:54 **/ @Configuration public class ShiroConfig { @Bean public CustomRealm customRealm(){ return new CustomRealm(); } @Bean public SecurityManager securityManager(){ DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager(); securityManager.setRealm(customRealm()); securityManager.setRememberMeManager(null); return securityManager; } @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(){ ShiroFilterFactoryBean bean=new ShiroFilterFactoryBean(); bean.setSecurityManager(securityManager()); bean.setLoginUrl("/to/login"); bean.setUnauthorizedUrl("/unauth"); //对于一些授权的链接URL进行拦截 Map<String, String> filterChainDefinitionMap=new HashMap<>(); filterChainDefinitionMap.put("/to/login","anon"); filterChainDefinitionMap.put("/**","anon"); filterChainDefinitionMap.put("/kill/execute","authc"); filterChainDefinitionMap.put("/item/detail/*","authc"); bean.setFilterChainDefinitionMap(filterChainDefinitionMap); return bean; } } 从上述该源代码中可以看出,Shiro的ShiroFilterFactoryBean组件将会对 URL=/kill/execute 和 URL=/item/detail/* 的链接进行拦截,即当用户访问这些URL时,系统会要求当前的用户进行登录(前提是用户还没登录的情况下!如果已经登录,则直接略过,进入实际的业务模块!) 除此之外,Shiro的ShiroFilterFactoryBean组件还设定了 “前往登录页”和“用户没授权/没登录的前提下的调整页”的链接,分别是 /to/login 和 /unauth! (5)至此,整合Shiro框架实现用户的登录认证的前后端代码实战已经完毕了,将项目/系统运行在外置的tomcat服务器中,打开浏览器即可访问进入“待秒杀商品的列表页”,点击“详情”,此时,由于用户还没登陆,故而将跳转至用户登录页,如下图所示:输入用户名:debug,密码:123456,点击“登录”按钮,即可登录成功,并成功进入“详情页”,如下图所示: 登录成功之后,再回到刚刚上一个列表页,即“待秒杀商品的列表页”,再次点击“详情”按钮,此时会直接进入“待秒杀商品的详情页”,而不会跳转至“用户登录页”,而且用户的登录态将会持续1个小时!(这是借助Shiro的Session的来实现的)。 补充:1、由于相应的博客的更新可能并不会很快,故而如果有想要快速入门以及实战整套系统的可参考: Java商城秒杀系统的设计与实战视频教程(SpringBoot版) 2、目前,这一秒杀系统的整体构建与代码实战已经全部完成了,完整的源代码数据库地址可以来这里下载:https://gitee.com/steadyjack/SpringBoot-SecondKill 记得Fork跟Star啊!!!
摘要: 本篇博文是“Java秒杀系统实战系列文章”的第四篇,从这篇文章开始我们将进入该秒杀系统相关业务模块的代码实战!本篇博文将首先从最简单的业务模块入手,即如何实现“获取待秒杀商品的列表以及查看待秒杀的商品详情”功能! 内容: 对于“待秒杀商品列表及其详情的展示”这一功能,我们将采用目前比较流行的mvc开发模式来实现!值得一提的是,这一功能模块涉及的主要数据库表为“商品信息表item”、“待秒杀商品信息item_kill”。 一、“待秒杀商品列表”代码实战 (1)首先是在 ItemController控制器中开发“获取待秒杀商品列表”的请求方法,其源代码如下所示: @RequestMapping(value = {"/","/index",prefix+"/list",prefix+"/index.html"},method = RequestMethod.GET) public String list(ModelMap modelMap){ try { //获取待秒杀商品列表 List<ItemKill> list=itemService.getKillItems(); modelMap.put("list",list); log.info("获取待秒杀商品列表-数据:{}",list); }catch (Exception e){ log.error("获取待秒杀商品列表-发生异常:",e.fillInStackTrace()); return "redirect:/base/error"; } return "list"; } 控制器的这一方法在获取到待秒杀商品的列表信息后,将通过modelMap的形式将数据列表返回给到前端的页面list.jsp中进行渲染!其中,itemService.getKillItems() 主要用于获取待秒杀商品的列表信息,其源代码如下所示: @Autowired private ItemKillMapper itemKillMapper; //获取待秒杀商品列表 @Override public List<ItemKill> getKillItems() throws Exception { return itemKillMapper.selectAll(); } (2)紧接着是开发 itemKillMapper.selectAll() 方法,其主要是基于Mybatis在配置文件中写动态Sql,该Sql的作用在于“获取待秒杀商品的列表”,其源代码如下所示: <!--查询待秒杀的活动商品列表--> <select id="selectAll" resultType="com.debug.kill.model.entity.ItemKill"> SELECT a.*, b.name AS itemName, ( CASE WHEN (now() BETWEEN a.start_time AND a.end_time AND a.total > 0) THEN 1 ELSE 0 END ) AS canKill FROM item_kill AS a LEFT JOIN item AS b ON b.id = a.item_id WHERE a.is_active = 1 </select> 在这里的Sql,Debug是采用了Left Join左关联查询的方式获取列表信息,目的是为了获取“商品信息表”中的商品信息,如“商品名称”等等。 值得一提的是,在这里Debug还使用了一个小技巧,即采用一个字段 canKill 来表示当前“待秒杀的商品”是否可以被秒杀/被抢购!其判断的标准为: 当待秒杀的商品的剩余数量/库存,即 total 字段的取值大于0时,并且 “当前的服务器时间now()处于待秒杀商品的抢购开始时间 和 抢购结束时间的范围内”时,canKill的取值将为1,即代表可以被抢购或者被秒杀。否则canKill的取值将为0。 (3)至此,“待秒杀商品列表”这一功能模块的后端代码开发已经完成了!前端发起请求后,请求将首先到达controller,通过请求路径url映射到某个方法进行调用,controller的方法首先会进行最基本的数据校验,然后通过调用service提供的接口获取真正的业务数据,最后是在service中执行真正的dao层层面的数据查询或者数据操作逻辑,最终完成整个业务流的操作。 (4)接下来是开发一个页面list.jsp用于展示“待秒杀商品列表的信息”,下面展示了该页面的部分核心源码,如下图所示:从该代码中可以看出,当canKill字段取值为1时,将可以点击“详情”进行查看;否则,将会提示相应的信息!即“判断是否可以秒杀”的逻辑Debug是将其放在了后端来实现! (5)至此,“获取待秒杀商品列表”这一功能模块的前后端代码实战已经完毕了,点击运行整个项目,将整个系统运行在外置的tomcat服务器中,观察控制台的输出信息,如果没有报错,这说明整个系统的代码在语法级别层面是木有问题的。如下图所示为整个秒杀系统、项目在运行起来之后的首页:虽然不是很美观,但是Debug觉得还是凑合着用吧 哈哈!! 二、“待秒杀商品详情”代码实战 (1)接下来是点击“详情”,查看“待秒杀商品的详情信息”,对于这个功能模块,其实还是比较简单的,其核心主要是根据“主键”进行查询。 同样的道理,首先需要在 ItemController控制器中开发接收前端请求的功能方法,其源代码如下所示: /** * 获取待秒杀商品的详情 * @return */ @RequestMapping(value = prefix+"/detail/{id}",method = RequestMethod.GET) public String detail(@PathVariable Integer id,ModelMap modelMap){ if (id==null || id<=0){ return "redirect:/base/error"; } try { ItemKill detail=itemService.getKillDetail(id); modelMap.put("detail",detail); }catch (Exception e){ log.error("获取待秒杀商品的详情-发生异常:id={}",id,e.fillInStackTrace()); return "redirect:/base/error"; } return "info"; } 该控制器的方法在获取到待秒杀商品的详情后,将通过modelMap把详情信息塞回info.jsp前端页面中进行渲染展示! (2)紧接着是itemService.getKillDetail(id) 的开发,即用于获取“待秒杀商品的详情”,其源代码如下所示: /** * 获取待秒杀商品详情 */ @Override public ItemKill getKillDetail(Integer id) throws Exception { ItemKill entity=itemKillMapper.selectById(id); if (entity==null){ throw new Exception("获取秒杀详情-待秒杀商品记录不存在"); } return entity; } 其中,itemKillMapper.selectById(id); 主要是基于Mybatis在配置文件中写动态Sql,该Sql的主要功能为根据主键查询待秒杀商品的详情,其源代码如下所示: <!--获取秒杀详情--> <select id="selectById" resultType="com.debug.kill.model.entity.ItemKill"> SELECT a.*, b.name AS itemName, ( CASE WHEN (now() BETWEEN a.start_time AND a.end_time AND a.total > 0) THEN 1 ELSE 0 END ) AS canKill FROM item_kill AS a LEFT JOIN item AS b ON b.id = a.item_id WHERE a.is_active = 1 AND a.id= #{id} </select> 从该Sql中不难看出,其实就是在“获取待秒杀商品列表”的Sql中加入“主键的精准查询”! (3)最后是在页面info.jsp渲染展示该详情信息,如下图所示为该页面的部分核心源代码:从该页面的部分核心源代码中可以看出,为了避免有人“跳过页面的请求,直接恶意刷后端接口”,在该页面仍然再次进行了一次判断(在后面执行“抢购/秒杀”请求时,后端接口还会再次进行判断的,所有这些都是为了安全考虑!) (4)至此,关于“待秒杀商品的详情展示”的功能的前后端代码实战已经完成了!再次将整个系统/项目运行在外置的tomcat服务器中,点击列表页中的“详情”按钮,可以看到待秒杀商品的详情信息,如下图所示:至此,本文所要分享介绍的内容已经完成了,即主要分享介绍了“获取待秒杀商品的列表”和“查看待秒杀商品的详情”功能! 补充: 1、目前,这一秒杀系统的整体构建与代码实战已经全部完成了,完整的源代码数据库地址可以来这里下载:https://gitee.com/steadyjack/SpringBoot-SecondKill 记得Fork跟Star啊!!! 2、由于相应的博客的更新可能并不会很快,故而如果有想要快速入门以及实战整套系统的,可以参考: Java商城秒杀系统的设计与实战视频教程(SpringBoot版)
摘要: 本篇博文是“Java秒杀系统实战系列文章”的第三篇,本篇博文将主要介绍秒杀系统的整体业务流程,并根据相应的业务流程进行数据库设计,最终采用Mybatis逆向工程生成相应的实体类Entity、操作Sql的接口Mapper以及写动态Sql的配置文件Mapper.xml。 内容: 对于该秒杀系统的整体业务流程,相信机灵的小伙伴在看完第二篇博文的时候,就已经知道个大概了!因为在提供的源码数据库下载的链接中,Debug已经跟各位小伙伴介绍了该秒杀系统整体的业务流程,而且还以视频形式给各位小伙伴进行了展示!该源码数据库的下载链接如下:https://gitee.com/steadyjack/SpringBoot-SecondKill 在本篇博文中Debug将继续花一点篇幅介绍介绍! 一图以概之,如下图所示为该秒杀系统整体的业务流程: 从该业务流程图中,可以看出,后端接口在接收前端的秒杀请求时,其核心处理逻辑为: (1)首先判断当前用户是否已经抢购过该商品了,如果否,则代表用户没有抢购过该商品,可以进入下一步的处理逻辑 (2)判断该商品可抢的剩余数量,即库存是否充足(即是否大于0),如果是,则进入下一步的处理逻辑 (3)扣减库存,并更新数据库的中对应抢购记录的库存(一般是减一操作),判断更新库存的数据库操作是否成功了,如果是,则创建用户秒杀成功的订单,并异步发送短信或者邮件通知信息通知用户 (4)以上的操作逻辑如果有任何一步是不满足条件的,则直接结束整个秒杀的流程,即秒杀失败! 如下图所示为后端处理“秒杀请求”时的核心处理逻辑: 综合这两个业务流程,下面进入“秒杀系统”的数据库设计环节,其中,主要包含以下几个表:商品信息表item、待秒杀信息表item_kill、秒杀成功记录表item_kill_success以及用户信息表user;当然,在实际的大型网站中,其所包含的数据库表远远不止于此!本系统暂且浓缩出其中核心的几张表! 如下图所示为该“秒杀系统”的数据库设计模型:紧接着,是采用Mybatis的逆向工程生成这几个数据库表对应的实体类Entity、操作Sql的接口Mapper以及写动态Sql的配置文件Mapper.xml。如下图所示: 下面,贴出其中一个实体类以及相对应的Mapper接口和Mapper.xml代码,其他的,各位小伙伴可以点击链接:https://gitee.com/steadyjack/SpringBoot-SecondKill 前往下载查看!首先是实体类ItemKill的源代码: import lombok.Data; import java.util.Date; @Data public class ItemKill { private Integer id; private Integer itemId; private Integer total; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8") private Date startTime; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8") private Date endTime; private Byte isActive; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8") private Date createTime; private String itemName; //采用服务器时间控制是否可以进行抢购 private Integer canKill; } 然后是ItemKillMapper接口的源代码: import com.debug.kill.model.entity.ItemKill; import org.apache.ibatis.annotations.Param; import java.util.List; public interface ItemKillMapper { List<ItemKill> selectAll(); ItemKill selectById(@Param("id") Integer id); int updateKillItem(@Param("killId") Integer killId); ItemKill selectByIdV2(@Param("id") Integer id); int updateKillItemV2(@Param("killId") Integer killId); } 最后是ItemKillMapper.xml配置文件的源代码: <!--查询待秒杀的活动商品列表--> <select id="selectAll" resultType="com.debug.kill.model.entity.ItemKill"> SELECT a.*, b.name AS itemName, ( CASE WHEN (now() BETWEEN a.start_time AND a.end_time AND a.total > 0) THEN 1 ELSE 0 END ) AS canKill FROM item_kill AS a LEFT JOIN item AS b ON b.id = a.item_id WHERE a.is_active = 1 </select> <!--获取秒杀详情--> <select id="selectById" resultType="com.debug.kill.model.entity.ItemKill"> SELECT a.*, b.name AS itemName, ( CASE WHEN (now() BETWEEN a.start_time AND a.end_time AND a.total > 0) THEN 1 ELSE 0 END ) AS canKill FROM item_kill AS a LEFT JOIN item AS b ON b.id = a.item_id WHERE a.is_active = 1 AND a.id= #{id} </select> <!--抢购商品,剩余数量减一--> <update id="updateKillItem"> UPDATE item_kill SET total = total - 1 WHERE id = #{killId} </update> <!--获取秒杀详情V2--> <select id="selectByIdV2" resultType="com.debug.kill.model.entity.ItemKill"> SELECT a.*, b.name AS itemName, (CASE WHEN (now() BETWEEN a.start_time AND a.end_time) THEN 1 ELSE 0 END) AS canKill FROM item_kill AS a LEFT JOIN item AS b ON b.id = a.item_id WHERE a.is_active = 1 AND a.id =#{id} AND a.total>0 </select> <!--抢购商品,剩余数量减一--> <update id="updateKillItemV2"> UPDATE item_kill SET total = total - 1 WHERE id = #{killId} AND total>0 </update> </mapper> 值得注意的是,上面实体类ItemKill、ItemKillMapper接口的相应方法及其对应的动态Sql的含义,各位小伙伴可以暂且忽略,在后面介绍到相应的业务实战时将会再次进行重点介绍。 至此,关于“秒杀系统”整体的业务流程、后端接口的核心处理逻辑以及Mybatis逆向工程的应用等就介绍到这里了。下一节将进入实际的代码实战环节! 补充1、由于相应的博客的更新可能并不会很快,故而如果有想要快速入门以及实战整套系统的,可以参考视频教程:link 2、目前,这一秒杀系统的整体构建与代码实战已经全部完成了,完整的源代码数据库地址可以来这里下载:https://gitee.com/steadyjack/SpringBoot-SecondKill 记得Fork跟Star啊!!!
摘要:本篇博文是“Java秒杀系统实战系列文章”的第二篇,主要分享介绍如何采用IDEA,基于SpringBoot+SpringMVC+Mybatis+分布式中间件构建一个多模块的项目,即“秒杀系统”!。 内容:传统的基于IDEA构建SpringBoot的项目,是直接借助Spring Initializr插件进行构建,但是这种方式在大部分情况下,只能充当“单模块”的项目,并不能很好的做到“分工明确、职责清晰”的分层原则! 故而为了能更好的管理项目代码以及尽量做到“模块如名”,快速定位给定的类文件或者其他文件的位置,下面我们将基于IDEA、借助Maven构建多模块的项目,其中,其构建的思路如下图所示: 详细的构建过程在本文就不赘述了!文末有提供源码的地址以及构建过程的视频教程!下面重点介绍一下跟“Java秒杀系统”相关的构建步骤。 (1)如下图所示为最终构建成功的项目的整体目录结构: 从该目录结构中可以看出,该项目为一个“聚合型项目”,其中,model模块依赖api模块,server模块依赖model模块,层层依赖!最终在server模块实现“大汇总”,即server模块为整个项目的核心关键所在,像什么“配置文件”、“入口启动类”啥的都在这个模块中! 而且,各个模块的职责是不一样的,分工也很明确,就像model模块,一般人看了就知道这里放的东西应该是跟mybatis或者跟数据库mysql相关的类文件与配置文件等等。 构建好相应的模块之后,就需要往相应的模块添加依赖,即只需要在pom.xml中加入相应的依赖即可,在这里就不贴出来了! (2)在这里主要贴一下server模块入口启动类MainApplication的代码,如下所示: @SpringBootApplication @ImportResource(value = {"classpath:spring/spring-jdbc.xml"}) @MapperScan(basePackages = "com.debug.kill.model.mapper") @EnableScheduling public class MainApplication extends SpringBootServletInitializer{ @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { return builder.sources(MainApplication.class); } public static void main(String[] args) { SpringApplication.run(MainApplication.class,args); } } 其中,该启动类将加载配置文件spring-jdbc.xml(数据库链接信息的配置文件)! 构建完成之后,可以将整个项目采用外置的Tomcat跑起来,运行过程中,观察控制台Console的输出信息,如果没有报错信息,则代表整个项目的搭建是没有问题的!如果出现了问题,建议自己先研究一番并尝试去解决掉!如果仍旧不能解决,可以加文末提供的联系方式进行解决! (4)除此之外,为了让整个项目在前后端分离开发的情况下,前后端的接口交互更加规范(比如响应信息的规范等等),在这里我们采用了通用的一个状态码枚举类StatusCode 跟 一个通用的响应结果类BaseResponse,用于后端在返回响应信息给到前端时进行统一封装。 状态码枚举类StatusCode的源代码如下所示: public enum StatusCode { Success(0,"成功"), Fail(-1,"失败"), InvalidParams(201,"非法的参数!"), UserNotLogin(202,"用户没登录"), ; private Integer code; //状态码code private String msg; //状态码描述信息msg StatusCode(Integer code, String msg) { this.code = code; this.msg = msg; } public Integer getCode() { return code; } public void setCode(Integer code) { this.code = code; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } } 响应结果类BaseResponse的源代码如下所示: public class BaseResponse<T> { private Integer code; //状态码code private String msg; //状态码对应的描述信息msg private T data; //响应数据 public BaseResponse(Integer code, String msg) { this.code = code; this.msg = msg; } public BaseResponse(StatusCode statusCode) { this.code = statusCode.getCode(); this.msg = statusCode.getMsg(); } public BaseResponse(Integer code, String msg, T data) { this.code = code; this.msg = msg; this.data = data; } public Integer getCode() { return code; } public void setCode(Integer code) { this.code = code; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public T getData() { return data; } public void setData(T data) { this.data = data; } } 在后面使用的过程中,大家会发现,这个BaseResponse和StatusCode的结合使用会带来很大的方便,而且,大家仔细观察,会发现这种模式跟“HTTP的响应模型”很像! (5)最后,为了测试整个项目的可用性以及BaseResponse和StatusCode的使用,下面建立一个BaseController控制器,并在其中开发一个简单的请求方法,如下所示: @Controller @RequestMapping("base") public class BaseController { private static final Logger log= LoggerFactory.getLogger(BaseController.class); /** * 标准请求-响应数据格式 */ @RequestMapping(value = "/response",method = RequestMethod.GET) @ResponseBody public BaseResponse response(String name){ BaseResponse response=new BaseResponse(StatusCode.Success); if (StringUtils.isBlank(name)){ name="这是welcome!"; } response.setData(name); return response; } } 6)将整个运行起来,如果控制台没有相应的报错信息,则打开Postman,并发起相应的请求:http://localhost:8092/kill/base/response?name=Java秒杀系统 (端口跟上下文是自己设置的!),可以观察响应信息,如下所示: (7)除此之外,这个Java秒杀系统项目还支持前端发起请求时,后端协助进行页面的跳转,其中本项目使用的模板引擎为Jsp,跳转后的页面位于/WEB-INF/views/目录下(这主要是通过在application.properties文件配置实现的)。 如下代码为在BaseController开发一个跳转到welcome页面的方法,其代码如下所示: /** * 跳转页面-跳转成功携带 name 参数到 welcome页面中 * @param name * @param modelMap * @return */ @GetMapping("/welcome") public String welcome(String name, ModelMap modelMap){ if (StringUtils.isBlank(name)){ name="这是welcome!"; } modelMap.put("name",name); return "welcome"; } 8)打开浏览器,访问:http://localhost:8092/kill/base/welcome?name=Java秒杀系统 即可跳转到相应的页面! 至此,关于“Java秒杀系统”多模块项目的构建已经完成了!值得一提的是,这一多模块项目可以适用于其他任何SpringBoot业务的应用场景,可以将其作为一个奠基项目来使用。 补充1、由于相应的博客的更新可能并不会很快,故而如果有想要快速入门以及实战整套系统的,可以参考教程:https://www.roncoo.com/view/1146338929757712386 2、目前,这一秒杀系统的整体构建与代码实战已经全部完成了,完整的源代码数据库地址可以来这里下载:https://gitee.com/steadyjack/SpringBoot-SecondKill 记得Fork跟Star啊!!!
稳定性技术策略什么是稳定性对于大型微服务系统,在错综复杂的服务逻辑各种交互情景下,面对各种未知的条件变化,整体系统依旧能够正常平稳的提供服务,这便是稳定性。 影响稳定性的因素系统稳定性影响因素非常多,举例来说: 服务间的依赖:某个服务 BUG 造成其他依赖服务的不可用; 业务逻辑变更:业务逻辑不断迭代演变,新老服务的不兼容; 访问流量激增:流量突然增加,比如我们进行促销活动期间,导致服务压力过大 ,达到服务能力上限,从而导致服务崩溃; 机器老化异常:任何机器和人一样,都有生老病死,随着长时间的运行,也会有磨损,因此某个机器故障,也是可能的异常。 还有其他各方面的因素,在这里就不进行穷尽了,大家可以思考一下,还有那些经典的影响因素。 稳定性的衡量标准稳定性衡量标准一般用 N 个 9 来衡量。如表格所示:比如说某个系统网站全年稳定性达到 4 个 9 ,意味着全年服务不可用的时间小于等于 53 分钟。 稳定性技术策略稳定性的技术策略,是我们本文介绍的重点,从大的方面来说,稳定性技术策略包含:监控,冗余,限流,降级,回滚,重试。 在这里重点介绍监控相关的内容。 监控 监控是指对整个系统服务进行实时的监控操作,准确反馈系统运行状态,能够做到及时发现异常故障,记录详细日志与数据,提高故障发现,定位,解决的效率。从而提高系统服务整体稳定性。 监控是保证稳定性最基础工作。我们将重点介绍监控需要从几个方面考虑? 有哪些监控方向? 监控可以分为以下几类来进行: 流量监控 流量监控包括:PV、 UV、 IP,热门页面,用户响应时间。 这些基本的流量指标就不在这里向大家详细说明了,如果有不太清楚的同学可以自己搜索一下。 在流量监控这块,我们需要注意的是:流量毛刺。流量毛刺往往代表了系统某个风险点或者异常情况的发生。 一个正常业务的流量趋势具备周期一致性特征。比如说,一个业务每天的流量峰值一般在中午 12:00 和下午 18:00,那么这种峰值在没有特殊情况出现的前提下,应该会遵循该峰值时间规律。 那么流量毛刺是啥呢? 如下图所示:从图中左侧部分可以看到,8 点钟有流量的突增,这时候我们需要确认为什么会有流量的突增。是业务的正常表现,还是有其他的异常流量进入。 从图中右侧部分可以看到,每条曲线代表了每一天的流量统计,红色曲线相对于其他几天的流量曲线在凌晨 3:00 和早上 8:00 的时候有明显的流量毛刺,这时候我们就需要确认是什么因素造成流量数据的突变。 通过对于流量毛刺的观察,能够让我们及时了解业务中的风险,及时做好预警与准备。 业务监控 业务监控是根据业务属性来定义监控指标,可以用于判定整体业务的运转是否正常。不同业务类型监控的指标肯定有所不同,比如电商场景、物流场景、游戏场景是完全不同的监控方向。 我们拿一个电商交易业务系统来举例,看看有哪些监控指标?大家可以参考着思考自己目前负责的业务指标。举例来说针对一个电商交易系统我们可以监控的业务指标有: 用户下单监控:秒/分/时 用户下单统计 ; 用户支付监控:秒/分/时 用户支付统计; 用户退款情况:秒/分/时 用户退款统计; 商品库存状态:库存消耗/剩余统计; 业务 GMV 监控:业务整体销售额统计; 商家在线状态:监控商家在线状态; 促销运营监控:监控促销金额与数量。 在业务监控中,还需要重点关注业务转化漏斗概念。流量漏斗可以看出业务转化率以及用户的访问深度。它是业务健康程度的监控,也是部分需求效果的衡量标准。如下图所示: 机器监控 机器监控需要关注的内容,应该是后台研发比较熟悉的部分。 主要监控方向包含下面几个部分:当然在 linux 系统中,也有各种常用指令,来进行该类数据的收集:top、free、ping、iostat、netstat。 对于 Java 系统来说,需要进行 JVM 层面的监控内容,比如说:gc 的情况,线程创建销毁情况,full gc 情况,内存不同模块使用情况等。 JVM 同样也提供了指令集,方便我们进行信息的查找:jstat、jps、stack、jmap、jhat 等。 日志记录 在日志的打印过程中,我们需要注意日志的打印规范,日志打印内容应该包含对于问题排查有益的信息,并且日志格式清晰,可解析性高。日志一般是打印到服务器磁盘上面,但是由于单机磁盘能力有限,并且对于大型分布式系统来说需要整体收集不同服务器模块的日志,进行统一分析,提高问题排查效率。这时候就需要一个集中式的日志中心。 日志中心的核心能力一般包含:获取日志、存储日志、展示日志、分析日志、报警服务。 相对比较简单的实现方式可以采用ELK快速搭建自己的日志中心。我们利用 kafka 将日志信息,进行异步广播,然后进行日志的解析,存储到 ES 检索系统中,利用 kibana 来进行日志的检索、查看等。进一步提升日志的有效管理。 总结 上面是监控相关内用,希望对大家有所帮助。 更多关于大型网站稳定性建设的内容,请参考: https://www.roncoo.com/view/1070493514408189953
领课教育系统 拥有完善的录播功能、直播功能、题库功能、资源功能、社区功能、营销功能 开源版 产品体验 该教育系统是由广州市领课网络科技有限公司自主研发,各行业都适用的在线教育系统。拥有完善的录播功能、直播功能、题库功能、资源功能、社区功能、营销功能。 可以作为专业的在线教育系统独立运营使用。 也可以作为通用的在线教育系统,根据自身需求可进行快速定制。(系统本身就是一个通用的系统,可以高度定制属于自己的在线教育系统) 课程录播功能介绍提供专业的学习体验,绝不仅是视频浏览;多功能、多模式授课 课程直播功能介绍专享直播“高清无延迟”,课可生成回放,无次数限制 系统优势微服务架构(技术先进)、容器化部署(运维高效)、高可用集群(系统稳定) 系统技术架构 成功案例 :IT 成功案例 :K12
目录 连接 连接池产生原因 连接池实现原理 小结 一、连接 什么是连接?连接,代表上游对下游的通信或会话。比如客户端连接服务器、服务器连接数据存储等 连接其通信的基本步骤,很类似 HTTP 操作: 1、上游对下游建立一个连接(客户端与服务器需要建立连接。比如点击某个超级链接)2、上游通过连接,发送请求(建立连接后,客户端发送请求给服务器)3、上游通过连接,收到响应(服务器接到请求后,响应其响应信息)4、上游关闭连接,释放连接资源(客户端接收服务器所返回的信息通过浏览器显示在用户的显示屏上,然后客户机与服务器断开连接) file 再深入点,HTTP 持久连接是什么?HTTP 持久连接是指用同一个 HTTP 底层的 TCP 连接来发送/接收多个 HTTP 请求/响应。扩展点,只需要在头部设置: Connection: Keep-Alive 为什么要有持久连接?每次都是从建立连接开始也可以达到结果,并且最后是关闭连接释放资源。这就是引出连接池产生原因。 二、连接池产生原因 先看一下常见的 mysql-connector-java 包驱动下面 ConnectionImpl 源码: trackConnection() execSQL() commit() close() 对 MySQL 多半是进行连接(connection),增删改查并提交(execSQL、commit),关闭连接(close)操作,然后实现业务相关逻辑。其操作也很清晰: 1、建立连接2、发送请求(数据的 CRUD 操作)3、关闭连接 但,为啥会需要有连接池?其实在业务量流量不大,并发量也不大的情况下,连接临时建立完全可以。但并发量起来,达到百级、千级,其中建立连接、关闭连接的操作会造成性能瓶颈,所以得考虑连接池来优化上述 1 和 3 操作: 1、取出连接(业务服务启动时,初始化若干个连接,放在连接存储中)2、发送请求(当有请求,从连接存储中中取出)3、放回连接(执行完毕,连接放回连接存储中) 这里对连接存储的数据结构,并维护连接,就是连接池。 三、连接池实现原理 连接池原理,可以具体看下阿里巴巴 Druid 包的 DruidDataSource 源码: DruidConnectionHolder[] connections; createConnection() getConnection() recycle() 连接池实现原理也不难,DruidDataSource 即德鲁伊连接池,可以核心设计接口: 1、createConnection:服务启动 init ,会创建一批指定数量的连接放入 connections 数组2、getConnection:这样每次请求,不会新建一个连接。而是从 DruidConnectionHolder[] connections 数组中取出一个连接3、recycle:每次请求结束后,不是关闭连接,而是回收连接到 connections 数组 其中有个重入锁 ReetrantLock,具体作用如下: 获取一个连接,锁住 返回该连接,使用连接 使用完毕,回收连接,并释放锁 四、小结 核心连接池也就这么点东西,具体还需要考虑其他点如下: 连接池连接设计遵守 LRU 策略,性能的关键点是连接是否 LRU 方式重用。LRU 资料:https://yq.aliyun.com/articles/70456 通过 Hash 去连接,实现串行化 可以自动扩容连接数 连接数过多,可以自动关闭连接,释放资源 等等 文章来源:https://my.oschina.net/jeffli1993/blog/3029248
一、龙果开源支付系统 架构全新升级 https://gitee.com/roncoocom/roncoo-payhttps://github.com/roncoo/roncoo-pay 1、项目框架更新,从spring 3.X直接升级到Spring Boot 2.X版本(注意运行需要JDK1.8+) 2、添加微信服务商小微商户进件功能,(在运营后台->交易管理->进件记录管理) 3、把原来本地添加支付宝SDK改为从中央库直接拉取(阿里已经把SDK维护到Maven中央库中) 龙果支付系统是国内首款开源的互联网支付系统,其核心目标是汇聚所有主流支付渠道,打造一款轻量、便捷、易用,且集支付、资金对账、资金清结算于一体的支付系统,满足互联网业务系统的收款和业务资金管理需求。 主要特点:具备支付系统通用的支付、对账、清算、资金账户管理、支付订单管理等功能; 目前已接通“支付宝即时到账”和“微信扫码支付”通道; 支持直连和间连两种支付模式,任君选择; 通过支付网关,业务系统可以轻松实现统一支付接入; 搭配运营后台,支付数据的监控和管理可以兼得; 配套完善的系统使用文档,可轻松嵌入任何需要支付的场景; 龙果支付系统产品技术团队是一支拥有多年第三方支付系统设计研发经验的团队,会为龙果支付系统持续提供商业级的免费开源技术服务支持。 二、分布式在线教育开源系统roncoo-education 1.0.0 正式发布https://www.oschina.net/news/104692/roncoo-education-1-0-0 项目介绍该开源项目是基于领课团队多年的在线教育系统开发和运营经验所创建的开源产品,致力于打造一个全行业都适用的在线教育系统。 功能介绍 权限管理功能,多角色多用户自定义配置 系统配置功能,自定义进行站点配置及第三方参数配置 讲师管理功能,讲师申请入驻,后台具有审核功能 课程管理功能,讲师管理自有课程,后台具有审核功能 用户登录功能,同一时间只允许同一个账号在同一个地方登录,防止账号共享 广告管理功能,后台自定义广告设置,增加营销效果 支付功能,系统无缝集成了龙果支付 技术选型 系统架构图 项目结构 ├─roncoo-education -----------------------------父项目,公共依赖 │ │ │ ├─roncoo-education-course -------------------课程模块,包括订单模块 │ │ │ │ │ ├─roncoo-education-course-common ---------共用工程 │ │ │ │ │ ├─roncoo-education-course-feign ----------接口工程,供其他工程模块使用 │ │ │ │ │ └─roncoo-education-course-service --------服务工程,其他接口服务 │ │ │ ├─roncoo-education-crontab-plan -------------定时任务,处理过期订单和统计等 │ │ │ ├─roncoo-education-gateway-api --------------网关工程 │ │ │ ├─roncoo-education-server-admin -------------监控中心 │ │ │ ├─roncoo-education-server-config ------------配置中心 │ │ │ ├─roncoo-education-server-eureka ------------注册中心 │ │ │ ├─roncoo-education-system -------------------系统基础工程 │ │ │ │ │ ├─roncoo-education-system-common ---------共用工程 │ │ │ │ │ ├─roncoo-education-system-feign ----------接口工程,供其他工程模块使用 │ │ │ │ │ └─roncoo-education-system-service --------服务工程,其他接口服务 │ │ │ ├─roncoo-education-user ---------------------用户工程 │ │ │ │ │ ├─roncoo-education-user-common -----------共用工程 │ │ │ │ │ ├─roncoo-education-user-feign ------------接口工程,供其他工程模块使用 │ │ │ │ │ └─roncoo-education-user-service ----------服务工程,其他接口服务 │ │ │ ├─roncoo-education-web-boss -----------------管理后台工程 │ │ │ ├─doc │ │ │ │ │ ├─images --------------------------------项目演示截图 │ │ │ │ │ ├─lombok.jar ----------------------------Eclipse使用,放到Eclipse的根目录即可 │ │ │
项目介绍 领课教育是基于领课团队多年的在线教育开发和运营经验的产品,打造一个全行业都适用的在线教育系统。 配置工程 roncoo-education-config: 码云地址 | Github地址 https://gitee.com/roncoocom/roncoo-education-config https://github.com/roncoo/roncoo-education-config 功能介绍 权限管理功能,多角色多用户自定义配置 系统配置功能,自定义进行站点配置及第三方参数配置 讲师管理功能,讲师申请入驻,后台具有审核功能 课程管理功能,讲师管理自有课程,后台具有审核功能 用户登录功能,同一时间只允许同一个账号在同一个地方登录,防止账号共享 广告管理功能,后台自定义广告设置,增加营销效果 支付功能,系统无缝集成了龙果支付 技术选型 流程图说明 系统架构图 课程播放流程 播放鉴权流程 课程下单流程 下单回调流程 项目结构 ├─roncoo-education -----------------------------父项目,公共依赖 │ │ │ ├─roncoo-education-course -------------------课程模块,包括订单模块 │ │ │ │ │ ├─roncoo-education-course-common ---------共用工程 │ │ │ │ │ ├─roncoo-education-course-feign ----------接口工程,供其他工程模块使用 │ │ │ │ │ └─roncoo-education-course-service --------服务工程,其他接口服务 │ │ │ ├─roncoo-education-crontab-plan -------------定时任务,处理过期订单和统计等 │ │ │ ├─roncoo-education-gateway-api --------------网关工程 │ │ │ ├─roncoo-education-server-admin -------------监控中心 │ │ │ ├─roncoo-education-server-config ------------配置中心 │ │ │ ├─roncoo-education-server-eureka ------------注册中心 │ │ │ ├─roncoo-education-system -------------------系统基础工程 │ │ │ │ │ ├─roncoo-education-system-common ---------共用工程 │ │ │ │ │ ├─roncoo-education-system-feign ----------接口工程,供其他工程模块使用 │ │ │ │ │ └─roncoo-education-system-service --------服务工程,其他接口服务 │ │ │ ├─roncoo-education-user ---------------------用户工程 │ │ │ │ │ ├─roncoo-education-user-common -----------共用工程 │ │ │ │ │ ├─roncoo-education-user-feign ------------接口工程,供其他工程模块使用 │ │ │ │ │ └─roncoo-education-user-service ----------服务工程,其他接口服务 │ │ │ ├─roncoo-education-web-boss -----------------管理后台工程 │ │ │ ├─doc │ │ │ │ │ ├─images --------------------------------项目演示截图 │ │ │ │ │ ├─lombok.jar ----------------------------Eclipse使用,放到Eclipse的根目录即可 │ │ │ └──└──└─*.sql----------------------------------项目SQL脚本:带有demo数据 加速maven构建 在maven的settings.xml 文件里配置mirrors的子节点,添加如下mirror <mirror> <id>nexus-aliyun</id> <mirrorOf>*</mirrorOf> <name>Nexus aliyun</name> <url>http://maven.aliyun.com/nexus/content/groups/public</url> </mirror> Lombok使用 Lombok是一个可以通过简单的注解形式来帮助我们简化消除一些必须有但显得很臃肿的Java代码的工具,通过使用对应的注解,可以在编译源码的时候生成对应的方法。 官方地址:https://projectlombok.org/ 1. Eclipse使用方法 把lombok.jar放入Eclipse的根目录,在eclipse.ini配置文件的最后加上 -javaagent:lombok.jar 2. IntelliJ IDEA使用方法 安装插件,如图所示 项目推荐 roncoo-recharge:码云地址 | Github地址 roncoo-jui-springboot:码云地址 | Github地址
如果RabbitMQ集群只有一个broker节点,那么该节点的失效将导致整个服务临时性的不可用,并且可能会导致message的丢失(尤其是在非持久化message存储于非持久化queue中的时候)。可以将所有message都设置为持久化,并且使用持久化的queue,但是这样仍然无法避免由于缓存导致的问题:因为message在发送之后和被写入磁盘并执行fsync之间存在一个虽然短暂但是会产生问题的时间窗。通过publisher的confirm机制能够确保客户端知道哪些message已经存入磁盘,尽管如此,一般不希望遇到因单点故障导致服务不可用。 如果RabbitMQ集群是由多个broker节点构成的,那么从服务的整体可用性上来讲,该集群对于单点失效是有弹性的,但是同时也需要注意:尽管exchange和binding能够在单点失效问题上幸免于难,但是queue和其上持有的message却不行,这是因为queue及其内容仅仅存储于单个节点之上,所以一个节点的失效表现为其对应的queue不可用。 举例说明一下,如果一个MQ集群由三个节点组成(MQ集群节点的模式也是有讲究的,一般三个节点会有一个RAM,两个DISK),exchange、bindings 等元数据会在三个节点之间同步,但queue上的消息是不会同步的,且不特殊设置的情况下,Queue只会在一个节点存在。可能有的同学会提另一个问题,我从三个MQ几点的监控面板,都可以看到这个Queue?这个是对的,这是由于Queue的元数据也是在三个节点之间同步,但Queue的实际存储只会在一个节点。我们发送消息到指定Queue,其实是发送消息到指定节点下的Queue。如下图所示,消息发送至队列testQueue,无论发送者通过哪个MQ节点执行发送,其最终的执行都会是在MQ03节点执行消息的存储。说到这儿,可能有的小伙伴就要问了?说好的,RabbitMQ集群提供高可用性呢。 分析一下,RabbitMQ集群搭建完成后,如果不进行任何高可用配置,会有哪些问题呢? 单点故障会导致消息丢失:如果MQ03节点故障,那么MQ03 中的消息就会丢失 无法最大化的利用MQ提供,提升执行效率:既然每次发送到队列testQueue的消息都会在MQ03节点存储,那么何必搭建集群。 引入RabbitMQ的镜像队列机制,将queue镜像到cluster中其他的节点之上。在该实现下,如果集群中的一个节点失效了,queue能自动地切换到镜像中的另一个节点以保证服务的可用性。在通常的用法中,针对每一个镜像队列都包含一个master和多个slave,分别对应于不同的节点。slave会准确地按照master执行命令的顺序进行命令执行,故slave与master上维护的状态应该是相同的。除了publish外所有动作都只会向master发送,然后由master将命令执行的结果广播给slave们,故看似从镜像队列中的消费操作实际上是在master上执行的。 一旦完成了选中的slave被提升为master的动作,发送到镜像队列的message将不会再丢失:publish到镜像队列的所有消息总是被直接publish到master和所有的slave之上。这样一旦master失效了,message仍然可以继续发送到其他slave上。 简单来说,镜像队列机制就是将队列在三个节点之间设置主从关系,消息会在三个节点之间进行自动同步,且如果其中一个节点不可用,并不会导致消息丢失或服务不可用的情况,提升MQ集群的整体高可用性。 先来看下设置镜像队列后的效果: 镜像队列会出现+2标识。 1.设置队列为镜像队列:How 两种方式: 通过监控面板设置 通过命令设置 rabbitmqctl set_policy [-p Vhost] Name Pattern Definition [Priority] -p Vhost: 可选参数,针对指定vhost下的queue进行设置 Name: policy的名称 Pattern: queue的匹配模式(正则表达式) Definition:镜像定义,包括三个部分ha-mode, ha-params, ha-sync-mode ha-mode:指明镜像队列的模式,有效值为 all/exactly/nodes all:表示在集群中所有的节点上进行镜像 exactly:表示在指定个数的节点上进行镜像,节点的个数由ha-params指定 nodes:表示在指定的节点上进行镜像,节点名称通过ha-params指定 ha-params:ha-mode模式需要用到的参数 ha-sync-mode:进行队列中消息的同步方式,有效值为automatic和manual priority:可选参数,policy的优先级 请注意一个事实,镜像配置的pattern 采用的是正则表达式匹配,也就是说会匹配一组。 RabbitMQ集群节点失效,MQ处理策略: 如果某个slave失效了,系统处理做些记录外几乎啥都不做:master依旧是master,客户端不需要采取任何行动,或者被通知slave失效。 如果master失效了,那么slave中的一个必须被选中为master。被选中作为新的master的slave通常是最老的那个,因为最老的slave与前任master之间的同步状态应该是最好的。然而,特殊情况下,如果存在没有任何一个slave与master完全同步的情况,那么前任master中未被同步的消息将会丢失。 镜像队列消息的同步: 将新节点加入已存在的镜像队列时,默认情况下ha-sync-mode=manual,镜像队列中的消息不会主动同步到新节点,除非显式调用同步命令。当调用同步命令后,队列开始阻塞,无法对其进行操作,直到同步完毕。当ha-sync-mode=automatic时,新加入节点时会默认同步已知的镜像队列。由于同步过程的限制,所以不建议在生产的active队列(有生产消费消息)中操作。 rabbitmqctl list_queues name slave_pids synchronised_slave_pids 查看那些slaves已经完成同步 rabbitmqctl sync_queue name 手动的方式同步一个queue rabbitmqctl cancel_sync_queue name 取消某个queue的同步功能 以上针对消息同步的命令,均可以通过监控界面来进行操作,最终也是通过这些操作命令执行。 说明: 镜像队列不是负载均衡,镜像队列无法提升消息的传输效率,或者更进一步说,由于镜像队列会在不同节点之间进行同步,会消耗消息的传输效率。 对exclusive队列设置镜像并不会有任何作用,因为exclusive队列是连接独占的,当连接断开,队列自动删除。所以实际上这两个参数对exclusive队列没有意义。那么有哪些队列是exclusive呢?一般来说,发布订阅队列及设置了该参数的队列都是exclusive 排他性队列。 如何确定一个队列是不是排他性队列呢? 如果队列的features包含Excl,就代表它是排他性队列。 镜像队列中某个节点宕掉的后果: 当slave宕掉了,除了与slave相连的客户端连接全部断开之外,没有其他影响。 当master宕掉时,会有以下连锁反应: 与master相连的客户端连接全部断开; 2.选举最老的slave节点为master。若此时所有slave处于未同步状态,则未同步部分消息丢失; 3.新的master节点requeue所有unack消息,因为这个新节点无法区分这些unack消息是否已经到达客户端,亦或是ack消息丢失在老的master的链路上,亦或者是丢在master组播ack消息到所有slave的链路上。所以处于消息可靠性的考虑,requeue所有unack的消息。此时客户端可能有重复消息; 4.如果客户端连着slave,并且Basic.Consume消费时指定了x-cancel-on-ha-failover参数,那么客户端会受到一个Consumer Cancellation Notification通知。如果未指定x-cancal-on-ha-failover参数,那么消费者就无法感知master宕机,会一直等待下去。 这就告诉我们,集群中存在镜像队列时,重新master节点有风险。 镜像队列中节点启动顺序,非常有讲究: 假设集群中包含两个节点,一般生产环境会部署三个节点,但为了方便说明,采用两个节点的形式进行说明。 场景1:A先停,B后停 该场景下B是master,只要先启动B,再启动A即可。或者先启动A,再在30s之内启动B即可恢复镜像队列。(如果没有在30s内回复B,那么A自己就停掉自己) 场景2:A,B同时停 该场景下可能是由掉电等原因造成,只需在30s内联系启动A和B即可恢复镜像队列。 场景3:A先停,B后停,且A无法恢复。 因为B是master,所以等B起来后,在B节点上调用rabbitmqctl forget_cluster_node A以接触A的cluster关系,再将新的slave节点加入B即可重新恢复镜像队列。 场景4:A先停,B后停,且B无法恢复 该场景比较难处理,旧版本的RabbitMQ没有有效的解决办法,在现在的版本中,因为B是master,所以直接启动A是不行的,当A无法启动时,也就没版本在A节点上调用rabbitmqctl forget_cluster_node B了,新版本中forget_cluster_node支持-offline参数,offline参数允许rabbitmqctl在离线节点上执行forget_cluster_node命令,迫使RabbitMQ在未启动的slave节点中选择一个作为master。 当在A节点执行rabbitmqctl forget_cluster_node -offline B时,RabbitMQ会mock一个节点代表A,执行forget_cluster_node命令将B提出cluster,然后A就能正常启动了。最后将新的slave节点加入A即可重新恢复镜像队列 场景5:A先停,B后停,且A和B均无法恢复,但是能得到A或B的磁盘文件 这个场景更加难以处理。将A或B的数据库文件($RabbitMQ_HOME/var/lib目录中)copy至新节点C的目录下,再将C的hostname改成A或者B的hostname。如果copy过来的是A节点磁盘文件,按场景4处理,如果拷贝过来的是B节点的磁盘文件,按场景3处理。最后将新的slave节点加入C即可重新恢复镜像队列。 场景6:A先停,B后停,且A和B均无法恢复,且无法得到A和B的磁盘文件 无解。 启动顺序中有一个30s 的概念,这个是MQ 的时间间隔,用于检测master、slave是否可用,因此30s 非常关键。 对于生产环境MQ集群的重启操作,需要分析具体的操作顺序,不可无序的重启,会有可能带来无法弥补的伤害(数据丢失、节点无法启动)。 简单总结下:镜像队列是用于节点之间同步消息的机制,避免某个节点宕机而导致的服务不可用或消息丢失,且针对排他性队列设置是无效的。另外很重要的一点,镜像队列机制不是负载均衡。 文章来源:https://www.cnblogs.com/jiagoushi/p/10189436.html相关内容推荐:https://www.roncoo.com/search/MQ
1、抽象等级 Flink提供了不同级别的抽象来开发流/批处理应用程序。1) 低层级的抽象最低层次的抽象仅仅提供有状态流。它通过Process函数嵌入到DataStream API中。它允许用户自由地处理来自一个或多个流的事件,并使用一致的容错状态。此外,用户可以注册事件时间和处理时间回调,允许程序实现复杂的计算。 2) 核心API 在实践中,大多数应用程序不需要上面描述的低级抽象,而是对核心API进行编程,比如DataStream API(有界或无界数据流)和DataSet API(有界数据集)。这些API提供了用于数据处理的通用构建块,比如由用户定义的多种形式的转换、连接、聚合、窗口、状态等。在这些api中处理的数据类型以类(class)的形式由各自的编程语言所表示。 低级流程函数与DataStream API集成,使得只对某些操作进行低级抽象成为可能。DataSet API为有界数据集提供了额外的原语,比如循环或迭代。 3) Table API Table API是一个以表为中心的声明性DSL,其中表可以动态地改变(当表示流数据时)。表API遵循(扩展)关系模型:表有一个附加模式(类似于关系数据库表)和API提供了类似的操作,如select, project, join, group-by, aggregate 等。Table API 程序以声明的方式定义逻辑操作应该做什么而不是指定操作的代码看起来如何。 虽然Table API可以通过各种用户定义函数进行扩展,但它的表达性不如核心API,但使用起来更简洁(编写的代码更少)。此外,Table API程序还可以在执行之前通过应用优化规则的优化器。可以无缝地在Table API和DataStream/DataSet API之间进行切换,允许程序将Table API和DataStream和DataSet API进行混合使用。 4) Sql层Flink提供的最高级别抽象是SQL。这种抽象在语义和表示方面都类似于Table API,但将程序表示为SQL查询表达式。SQL抽象与表API密切交互,SQL查询可以在表API中定义的表上执行。 2、程序和数据流 Flink程序的基本构建模块是streams 和 transformations 。(请注意,Flink的DataSet API中使用的数据集也是内部流——稍后将对此进行详细介绍。)从概念上讲,streams 是数据记录的(可能是无限的)流,而transformations是将一个或多个流作为输入并产生一个或多个输出流的操作。 执行时,Flink程序被映射到流数据流,由streams 和 transformations 操作符组成。每个数据流以一个或多个sources开始,以一个或多个sinks结束。数据流类似于任意有向无环图(DAGs)。虽然通过迭代构造允许特殊形式的循环,但为了简单起见,我们将在大多数情况下忽略这一点。通常在程序中的transformations和数据流中的操作之间是一对一的对应关系。然而,有时一个transformations可能包含多个transformations操作。 在streming连接器和批处理连接器文档中记录了Sources 和 sinks。在DataStream运算和数据集transformations中记录了transformations。 3、并行数据流 Flink中的程序本质上是并行的和分布式的。在执行期间,流有一个或多个流分区,每个operator 有一个或多个operator subtasks(操作子任务)。operator subtasks相互独立,在不同的线程中执行,可能在不同的机器或容器上执行。 operator subtasks的数量是特定运算符的并行度。一个流的并行性总是它的生产操作符的并行性。同一程序的不同运算符可能具有不同级别的并行性。流可以在两个操作符之间以一对一(或转发)模式传输数据,也可以在重新分配模式中传输数据: One-to-one 流(例如上图中Source和map()运算符之间的流)保持元素的分区和顺序。这意味着map()操作符的subtask[1]将看到与源操作符的subtask[1]生成的元素相同的顺序。 Redistributing 流(如上面的map()和keyBy/window之间,以及keyBy/window和Sink之间)改变流的分区。每个操作符子任务根据所选的转换将数据发送到不同的目标子任务。例如keyBy()(通过散列键来重新分区)、broadcast()或balanced()(随机重新分区)。在重分发交换中,元素之间的顺序只保留在每一对发送和接收子任务中(例如map()的子任务[1]和keyBy/window的子任务[2])。因此,在本例中,每个键中的顺序都是保留的,但是并行性确实引入了关于不同键的聚合结果到达sink的顺序的不确定性。 4、窗口 聚合事件(例如计数、求和)在流上的工作方式与批处理不同。例如,不可能计算流中的所有元素,因为流通常是无限的(无界的)。相反,流上的聚合(计数、求和等)是由窗口限定作用域的,例如“过去5分钟的计数”或“最后100个元素的总和”。 Windows可以是时间驱动(示例:每30秒)或数据驱动(示例:每100个元素)。一个典型的方法是区分不同类型的窗口,比如翻滚窗户(没有重叠)、滑动窗口(有重叠)和会话窗口(中间有一个不活跃的间隙)。5、时间 当提到流程序中的时间(例如定义窗口)时,可以指不同的时间概念: 事件时间 : 是创建事件的时间。它通常由事件中的时间戳描述,例如由生产传感器或生产服务附加。Flink通过时间戳转让者访问事件时间戳。 摄入时间 : 在source操作符中一个事件进入Flink数据流的时间。处理时间 : 是执行基于时间的操作的每个操作符的本地时间。 6、状态操作 虽然一个数据流中有许多操作但只看作一个单独的事件(例如事件解析器),但是一些操作记住了跨多个事件的信息(例如窗口操作符)。这些操作称为有状态操作。 有状态操作的状态被维护在可以认为是嵌入式键/值存储中。状态与有状态操作符读取的流一起被严格地分区和分布。因此,在keyBy()函数之后,只能在键控流上访问键/值状态,并且只能访问与当前事件的键相关联的值。对齐流和状态的键确保所有的状态更新都是本地操作,保证一致性而不增加事务开销。这种对齐还允许Flink透明地重新分配状态和调整流分区。 (EventTime是信息自带的时间,再进入消息队列,IngestionTime是进入Flink的时间,Processing是进入Operator的时间)7、容错检查点 Flink通过流回放和检查点的组合实现了容错。检查点与每个输入流中的特定点以及每个操作符的对应状态相关。通过恢复操作符的状态并从检查点重新播放事件,流数据流可以在检查点恢复,同时保持一致性(准确地说是一次处理语义)。 检查点间隔是在执行期间用恢复时间(需要重放的事件数量)来权衡容错开销的一种方法。 8、批处理流 Flink执行批处理程序作为流程序的特殊情况,其中流是有界的(有限的元素数量)。数据集在内部被视为数据流。因此,上述概念同样适用于批处理程序,也适用于流程序,但有少数例外: 批处理程序的容错不使用检查点。恢复通过完全重放流来实现。这是可能的,因为输入是有界的。这将使成本更多地用于恢复,但使常规处理更便宜,因为它避免了检查点。 数据集API中的有状态操作使用简化的内存/核心外数据结构,而不是键/值索引。 DataSet API引入了特殊的synchronized(基于超步的)迭代,这只能在有界的流上实现。有关详细信息,请查看迭代文档。 文章来源:https://blog.csdn.net/silentwolfyh/article/details/82865579 推荐阅读:https://www.roncoo.com/view/173
背景介绍 国内某移动局点使用Impala组件处理电信业务详单,每天处理约100TB左右详单,详单表记录每天大于百亿级别,在使用impala过程中存在以下问题: 1、详单采用Parquet格式存储,数据表使用时间+MSISDN号码做分区,使用Impala查询,利用不上分区的查询场景,则查询性能比较差。 2、在使用Impala过程中,遇到很多性能问题(比如catalog元数据膨胀导致元数据同步慢等),并发查询性能差等。 3、Impala属于MPP架构,只能做到百节点级,一般并发查询个数达到20左右时,整个系统的吞吐已经达到满负荷状态,在扩容节点也提升不了吞吐量。 4、资源不能通过YARN统一资源管理调度,所以Hadoop集群无法实现Impala、Spark、Hive等组件的动态资源共享。给第三方开放详单查询能力也无法做到资源隔离。 解决方案 针对上面的一系列问题,移动局点客户要求我们给出相应的解决方案,我们大数据团队针对上面的问题进行分析,并且做技术选型,在这个过程中,我们以这个移动局点的几个典型业务场景作为输入,分别对Spark+CarbonData、Impala2.6、HAWQ、Greenplum、SybaseIQ进行原型验证,性能调优,针对我们的业务场景优化CarbonData的数据加载性能,查询性能并贡献给CarbonData开源社区,最终我们选择了Spark+CarbonData的方案,这个也是典型的SQL On Hadoop的方案,也间接印证了传统数据仓库往SQL on Hadoop上迁移的趋势。 参考社区官网资料,结合我们验证测试和理解:CarbonData是大数据Hadoop生态高性能数据存储方案,尤其在数据量较大的情况下加速明显,与Spark进行了深度集成,兼容了Spark生态所有功能(SQL,ML,DataFrame等),Spark+CarbonData适合一份数据满足多种业务场景的需求,它包含如下能力: 1、存储:行、列式文件存储,列存储类似于Parquet、ORC,行存储类似Avro。支持针对话单、日志、流水等数据的多种索引结构。 2、计算:与Spark计算引擎深度集成和优化;支持与Presto, Flink, Hive等引擎对接; 3、接口: API:兼容DataFrame, MLlib, Pyspark等原生API接口; SQL:兼容Spark语法基础,同时支持CarbonSQL语法扩展(更新删除,索引,预汇聚表等)。 4、数据管理: 支持增量数据入库,数据批次管理(老化管理) 支持数据更新,删除 支持与Kafka对接,准实时入库 细的关键技术介绍以及使用,请上官网阅读查看文档https://carbondata.apache.org/ 技术选型介绍 这里补充介绍下为什么选取SQL on Hadoop技术作为最终的解决方案。 接触过大数据的人都知道,大数据有个5V特征,从传统互联网数据到移动互联网数据,再到现在很热门的IoT,实际上随着每一次业界的进步,数据量而言都会出现两到三个数量级的增长。而且现在的数据增长呈现出的是一个加速增长的趋势,所以现在提出了一个包括移动互联网以及物联网在内的互联网大数据的5大特征:Volume、 Velocity、Variety、Value、Veracity。随着数据量的增长传统的数据仓库遇到的挑战越来越多。 传统数据仓库面临的挑战: 同时数据体系也在不断的进化 • 存储方式的进化:离线、近线 -> 全部在线 • 存储架构的进化:集中式存储 -> 分布式存储 • 存储模型的进化:固定结构 -> 灵活结构. 数据处理模式的进化 • 固定模型固定算法 -> 灵活模型灵活算法 数据处理类型的进化 • 结构化集中单源计算 -> 多结构化分布式多源计算 数据处理架构的进化 • 数据库静态处理 -> 数据实时/流式/海量处理 针对上述的变化数据库之父Kimball提出了一个观点: Kimball的核心观点: hadoop改变了传统数仓库的数据处理机制,传统数据库的一个处理单元在hadoop中解耦成三层: • 存储层:HDFS • 元数据层:Hcatalog • 查询层:Hive、Impala、Spark SQL Schema on Read给了用户更多的选择: • 数据以原始格式导入存储层 • 通过元数据层来管理目标数据结构 • 由查询层来决定什么时候提取数据 • 用户在长期探索和熟悉数据之后,可以采取Schema on Write模式固化中间表,提高查询性能 SQL on Hadoop数据仓库技术 数据处理和分析 • SQL on hadoop • Kudu+Impala、Spark、HAWQ、Presto、Hive等 • 数据建模和存储 • Schema on Read • Avro & ORC & Parquet & CarbonData • 流处理 • Flume+Kafka+Spark Streaming SQL-on-Hadoop技术的发展和成熟推动变革 经过上述的技术分析,最终我们选择了SQL on Hadoop的技术作为我们平台未来的数据仓库演进方向,这里肯定有人问了,为什么不选取MPPDB这种技术呢,这里我们同样把SQL on Hadoop与MPPDB进行过对比分析(注Impala其实也是一种类似MPPDB的技术): 方案实施效果 局点2018年9月底上线Spark+CarbonData替换Impala后运行至今,每天处理大于100TB的单据量,在业务高峰期,数据加载性能从之前impala的平均单台60MB/s到平台单台100MB/s的性能,在局点典型业务场景下,查询性能在20并发查询下,Spark+CarbonData的查询性能是Impala+parquet的2倍以上。 同时解决了以下问题: 1、Hadoop集群资源共享问题,Impala资源不能通过Yarn统一资源调度管理,Spark+CarbonData能通过Yarn统一资源调度管理,实现与其他如Spark,Hive等组件的动态资源共享。 2、Hadoop集群扩容问题,之前Impala只能使用百台机器,现在Spark+CarbonData能做到上千台节点集群规模。 实施过程中注意项: 1、数据加载使用CarbonData的local sort方式加载,为了避免大集群产生过多小文件的问题,加载只指定少数机器上进行数据加载,另外对于每次加载数据量比较小的表可以指定表级别的compaction来合并加载过程中产生的小文件。 2、根据业务的查询特点,把经常查询过滤的字段设置为数据表的sort column属性(比如电信业务经常查询的用户号码等),并且设置sort column的字段顺序先按照字段的查询频率由高到低排列,如果查询频率相差不大,则再按照字段distinct值由高到低排列,来提升查询性能。 3、创建数据表设置的blocksize大小,单个表的数据文件block大小可以通过TABLEPROPERTIES进行定义,单位为MB,默认值为1024MB。这个根据实际数据表的每次加载的数据量,根据我们实践经验:一般建议数据量小的表blocksize设置成256MB,数据量比较大的表blocksize设置成512MB。 4、查询性能的调优,还可以结合业务查询的特点,对查询高频率的字段,创建bloomfilter等datamap来提升查询性能。 5、还有一些Spark相关的参数设置,对于数据加载和查询,先结合SparkUI分析性能瓶颈点,在针对性的调整相关的参数,这里不一一介绍了,记住一点性能调优是个技术细活,参数调整要针对性的调整,一次调整只调相关的一个或者几个参数,在看效果,不生效就调整回去,切记千万不要一次性调整的参数过多。 文章来源:https://my.oschina.net/u/4029686/blog/2878526 推荐阅读:https://www.roncoo.com/course/list.html?courseName=%E5%A4%A7%E6%95%B0%E6%8D%AE+
实战前言RabbitMQ 作为目前应用相当广泛的消息中间件,在企业级应用、微服务应用中充当着重要的角色。特别是在一些典型的应用场景以及业务模块中具有重要的作用,比如业务服务模块解耦、异步通信、高并发限流、超时业务、数据延迟处理等。上篇博文我介绍分享了RabbitMQ在业务服务模块异步解耦以及通信的实战业务场景,感兴趣童鞋可以前往观看:https://www.roncoo.com/article/detail/134309 这边博文我们继续介绍分享RabbitMQ消息确认机制以及并发量的配置,并介绍分享其在高并发系统场景下的实战! RabbitMQ 实战:并发量配置与消息确认机制 实战背景对于消息模型中的 listener 而言,默认情况下是“单消费实例”的配置,即“一个 listener 对应一个消费者”,这种配置对于上面所讲的“异步记录用户操作日志”、“异步发送邮件”等并发量不高的场景下是适用的。但是在对于秒杀系统、商城抢单等场景下可能会显得很吃力!我们都知道,秒杀系统跟商城抢单均有一个共同的明显的特征,即在某个时刻会有成百上千万的请求到达我们的接口,即瞬间这股巨大的流量将涌入我们的系统,我们可以采用下面一图来大致体现这一现象:当到了“开始秒杀”、“开始抢单”的时刻,此时系统可能会出现这样的几种现象: 应用系统配置承载不了这股瞬间流量,导致系统直接挂掉,即传说中的“宕机”现象; 接口逻辑没有考虑并发情况,数据库读写锁发生冲突,导致最终处理结果跟理论上的结果数据不一致(如商品存库量只有 100,但是高并发情况下,实际表记录的抢到的用户记录数据量却远远大于 100); 应用占据服务器的资源直接飙高,如 CPU、内存、宽带等瞬间直接飙升,导致同库同表甚至可能同 host 的其他服务或者系统出现卡顿或者挂掉的现象; 于是乎,我们需要寻找解决方案!对于目前来讲,网上均有诸多比较不错的解决方案,在此我顺便提一下我们的应用系统采用的常用解决方案,包括: 我们会将处理抢单的整体业务逻辑独立、服务化并做集群部署;我们会将那股巨大的流量拒在系统的上层,即将其转移至 MQ 而不直接涌入我们的接口,从而减少数据库读写锁冲突的发生以及由于接口逻辑的复杂出现线程堵塞而导致应用占据服务器资源飙升; 我们会将抢单业务所在系统的其他同数据源甚至同表的业务拆分独立出去服务化,并基于某种 RPC 协议走 HTTP 通信进行数据交互、服务通信等等; 采用分布式锁解决同一时间同个手机号、同一时间同个 IP 刷单的现象; 下面,我们用 RabbitMQ 来实战上述的第二点!即我们会在“请求” -> "处理抢单业务的接口" 中间架一层消息中间件做“缓冲”、“缓压”处理,如下图所示:并发量配置与消息确认机制正如上面所讲的,对于抢单、秒杀等高并发系统而言,如果我们需要用 RabbitMQ 在 “请求” - “接口” 之间充当限流缓压的角色,那便需要我们对 RabbitMQ 提出更高的要求,即支持高并发的配置,在这里我们需要明确一点,“并发消费者”的配置其实是针对 listener 而言,当配置成功后,我们可以在 MQ 的后端控制台应用看到 consumers 的数量,如下所示:其中,这个 listener 在这里有 10 个 consumer 实例的配置,每个 consumer 可以预监听消费拉取的消息数量为 5 个(如果同一时间处理不完,会将其缓存在 mq 的客户端等待处理!) 另外,对于某些消息而言,我们有时候需要严格的知道消息是否已经被 consumer 监听消费处理了,即我们有一种消息确认机制来保证我们的消息是否已经真正的被消费处理。在 RabbitMQ 中,消息确认处理机制有三种:Auto - 自动、Manual - 手动、None - 无需确认,而确认机制需要 listener 实现 ChannelAwareMessageListener 接口,并重写其中的确认消费逻辑。在这里我们将用 “手动确认” 的机制来实战用户商城抢单场景。 1.在 RabbitMQConfig 中配置确认消费机制以及并发量的配置2.消息模型的配置信息3.RabbitMQ 后端控制台应用查看此队列的并发量配置4.listener 确认消费处理逻辑:在这里我们需要开发抢单的业务逻辑,即“只有当该商品的库存 >0 时,抢单成功,扣减库存量,并将该抢单的用户信息记录入表,异步通知用户抢单成功!”5.紧接着我们采用 CountDownLatch 模拟产生高并发时的多线程请求(或者采用 jmeter 实施压测也可以!),每个请求将携带产生的随机数:充当手机号 -> 充当消息,最终入抢单队列!在这里,我模拟了 50000 个请求,相当于 50000 手机号同一时间发生抢单的请求,而设置的产品库存量为 100,这在 product 数据库表即可设置6.将抢单请求的手机号信息压入队列,等待排队处理7.在最后我们写个 Junit 或者写个 Controller,进行 initService.generateMultiThread(); 调用模拟产生高并发的抢单请求即可 @RestController public class ConcurrencyController { private static final Logger log= LoggerFactory.getLogger(HelloWorldController.class); private static final String Prefix="concurrency"; @Autowired private InitService initService; @RequestMapping(value = Prefix+"/robbing/thread",method = RequestMethod.GET) public BaseResponse robbingThread(){ BaseResponse response=new BaseResponse(StatusCode.Success); initService.generateMultiThread(); return response; }} 8.最后,我们当然是跑起来,在控制台我们可以观察到系统不断的在产生新的请求(线程)– 相当于不断的有抢单的手机号涌入我们的系统,然后入队列,listener 监听到请求之后消费处理抢单逻辑!最后我们可以观察两张数据库表:商品库存表、商品成功抢单的用户记录表 - 只有当库存表中商品对应的库存量为 0、商品成功抢单的用户记录刚好 100 时 即表示我们的实战目的以及效果已经达到了!!总结:如此一来,我们便将 request 转移到我们的 mq,在一定程度缓解了我们的应用以及接口的压力!当然,实际情况下,我们的配置可能远远不只代码层次上的配置,比如我们的 mq 可能会做集群配置、负载均衡、商品库存的更新可能会考虑分库分表、库存更新可能会考虑独立为库存 Dubbo 服务并通过 Rest Api 异步通信交互并独立部署等等。这些优化以及改进的目的其实无非是为了能限流、缓压、保证系统稳定、数据的一致等!而我们的 MQ,在其中可以起到不可磨灭的作用,其字如其名:“消息队列”,而队列具有 “先进先出” 的特点,故而所有进入 MQ 的消息都将 “乖巧” 的在 MQ 上排好队,先来先排队,先来先被处理消费,由此一来至少可以避免 “瞬间时刻一窝蜂的 request 涌入我们的接口” 的情况! 附注:在用 RabbitMQ 实战上述高并发抢单解决方案,其实我也在数据库层面进行了优化,即在读写存库时采用了“类似乐观锁”的写法,保证:抢单的请求到来时有库存,更新存库时保证有库存可以被更新! 彩蛋:本博文继续分享介绍了RabbitMQ典型应用业务场景的实战-并发系统下RabbitMQ的限流作用以及基于SpringBoot微服务项目的实战,另外也介绍了消息确认机制的配置实战跟并发量配置,下篇博文将继续分享死信队列的相关内容及其实战。另外,博主已将RabbitMQ相关技术以及场景实战的相关要点录制成了视频教程。 感兴趣小伙伴可以前往学习观看:SpringBoot整合RabbitMQ实战
实战前言RabbitMQ 作为目前应用相当广泛的消息中间件,在企业级应用、微服务应用中充当着重要的角色。特别是在一些典型的应用场景以及业务模块中具有重要的作用,比如业务服务模块解耦、异步通信、高并发限流、超时业务、数据延迟处理等。 RabbitMQ 官网拜读首先,让我们先拜读 RabbitMQ 官网的技术开发手册以及相关的 Features,感兴趣的朋友可以耐心的阅读其中的相关介绍,相信会有一定的收获,地址可见:www.rabbitmq.com/getstarted.… 在阅读该手册过程中,我们可以得知 RabbitMQ 其实核心就是围绕 “消息模型” 来展开的,其中就包括了组成消息模型的相关组件:生产者,消费者,队列,交换机,路由,消息等!而我们在实战应用中,实际上也是紧紧围绕着 “消息模型” 来展开撸码的! 下面,我就介绍一下这一消息模型的演变历程,当然,这一历程在 RabbitMQ 官网也是可以窥览得到的!上面几个图就已经概述了几个要点,而且,这几个要点的含义可以说是字如其名!生产者:发送消息的程序消费者:监听接收消费消息的程序消息:一串二进制数据流队列:消息的暂存区/存储区交换机:消息的中转站,用于接收分发消息。其中有 fanout、direct、topic、headers 四种路由:相当于密钥/第三者,与交换机绑定即可路由消息到指定的队列! 正如上图所展示的消息模型的演变,接下来我们将以代码的形式实战各种典型的业务场景! SpringBoot 整合 RabbitMQ 实战工欲善其事,必先利其器。我们首先需要借助 IDEA 的 Spring Initializr 用 Maven 构建一个 SpringBoot 的项目,并引入 RabbitMQ、Mybatis、Log4j 等第三方框架的依赖。搭建完成之后,可以简单的写个 RabbitMQController 测试一下项目是否搭建是否成功(可以暂时用单模块方式构建) 紧接着,我们进入实战的核心阶段,在项目或者服务中使用 RabbitMQ,其实无非是有几个核心要点要牢牢把握住,这几个核心要点在撸码过程中需要“时刻的游荡在自己的脑海里”,其中包括:我要发送的消息是什么 我应该需要创建什么样的消息模型:DirectExchange+RoutingKey?TopicExchange+RoutingKey?等我要处理的消息是实时的还是需要延时/延迟的?消息的生产者需要在哪里写,消息的监听消费者需要在哪里写,各自的处理逻辑是啥 基于这样的几个要点,我们先小试牛刀一番,采用 RabbitMQ 实战异步写日志与异步发邮件。当然啦,在进行实战前,我们需要安装好 RabbitMQ 及其后端控制台应用,并在项目中配置一下 RabbitMQ 的相关参数以及相关 Bean 组件。 1.RabbitMQ 安装完成后,打开后端控制台应用:http://localhost:15672/ guest guest 登录,看到下图即表示安装成功 2.然后是项目配置文件层面的配置 application.properties spring.rabbitmq.host=127.0.0.1 spring.rabbitmq.port=5672 spring.rabbitmq.username=guest spring.rabbitmq.password=guest spring.rabbitmq.listener.concurrency=10 spring.rabbitmq.listener.max-concurrency=20 spring.rabbitmq.listener.prefetch=5 其中,后面三个参数主要是用于“并发量的配置”,表示:并发消费者的初始化值,并发消费者的最大值,每个消费者每次监听时可拉取处理的消息数量。 接下来,我们需要以 Configuration 的方式配置 RabbitMQ 并以 Bean 的方式显示注入 RabbitMQ 在发送接收处理消息时相关 Bean 组件配置其中典型的配置是 RabbitTemplate 以及 SimpleRabbitListenerContainerFactory,前者是充当消息的发送组件,后者是用于管理RabbitMQ监听器 的容器工厂,其代码如下: @Configuration public class RabbitmqConfig { private static final Logger log= LoggerFactory.getLogger(RabbitmqConfig.class); @Autowired private Environment env; @Autowired private CachingConnectionFactory connectionFactory; @Autowired private SimpleRabbitListenerContainerFactoryConfigurer factoryConfigurer; /** * 单一消费者 * @return */ @Bean(name = "singleListenerContainer") public SimpleRabbitListenerContainerFactory listenerContainer(){ SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); factory.setConnectionFactory(connectionFactory); factory.setMessageConverter(new Jackson2JsonMessageConverter()); factory.setConcurrentConsumers(1); factory.setMaxConcurrentConsumers(1); factory.setPrefetchCount(1); factory.setTxSize(1); factory.setAcknowledgeMode(AcknowledgeMode.AUTO); return factory; } /** * 多个消费者 * @return */ @Bean(name = "multiListenerContainer") public SimpleRabbitListenerContainerFactory multiListenerContainer(){ SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); factoryConfigurer.configure(factory,connectionFactory); factory.setMessageConverter(new Jackson2JsonMessageConverter()); factory.setAcknowledgeMode(AcknowledgeMode.NONE); factory.setConcurrentConsumers(env.getProperty("spring.rabbitmq.listener.concurrency",int.class)); factory.setMaxConcurrentConsumers(env.getProperty("spring.rabbitmq.listener.max-concurrency",int.class)); factory.setPrefetchCount(env.getProperty("spring.rabbitmq.listener.prefetch",int.class)); return factory; } @Bean public RabbitTemplate rabbitTemplate(){ connectionFactory.setPublisherConfirms(true); connectionFactory.setPublisherReturns(true); RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory); rabbitTemplate.setMandatory(true); rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() { @Override public void confirm(CorrelationData correlationData, boolean ack, String cause) { log.info("消息发送成功:correlationData({}),ack({}),cause({})",correlationData,ack,cause); } }); rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() { @Override public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) { log.info("消息丢失:exchange({}),route({}),replyCode({}),replyText({}),message:{}",exchange,routingKey,replyCode,replyText,message); } }); return rabbitTemplate; }} RabbitMQ 实战:业务模块解耦以及异步通信在一些企业级系统中,我们经常可以见到一个执行 function 通常是由许多子模块组成的,这个 function 在执行过程中,需要 同步 的将其代码从头开始执行到尾,即执行流程是 module_A -> module_B -> module_C -> module_D,典型的案例可以参见汇编或者 C 语言等面向过程语言开发的应用,现在的一些 JavaWeb 应用也存在着这样的写法。 而我们知道,这个执行流程其实对于整个 function 来讲是有一定的弊端的,主要有两点:整个 function 的执行响应时间将很久;如果某个 module 发生异常而没有处理得当,可能会影响其他 module 甚至整个 function 的执行流程与结果; 整个 function 中代码可能会很冗长,模块与模块之间可能需要进行强通信以及数据的交互,出现问题时难以定位与维护,甚至会陷入 “改一处代码而动全身”的尴尬境地! 故而,我们需要想办法进行优化,我们需要将强关联的业务模块解耦以及某些模块之间实行异步通信!下面就以两个场景来实战我们的优化措施! 场景一:异步记录用户操作日志对于企业级应用系统或者微服务应用中,我们经常需要追溯跟踪记录用户的操作日志,而这部分的业务在某种程度上是不应该跟主业务模块耦合在一起的,故而我们需要将其单独抽出并以异步的方式与主模块进行异步通信交互数据。 下面我们就用 RabbitMQ 的 DirectExchange+RoutingKey 消息模型也实现“用户登录成功记录日志”的场景。如前面所言,我们需要在脑海里回荡着几个要点:消息模型:DirectExchange+RoutingKey 消息模型消息:用户登录的实体信息,包括用户名,登录事件,来源的IP,所属日志模块等信息发送接收:在登录的 Controller 中实现发送,在某个 listener 中实现接收并将监听消费到的消息入数据表;实时发送接收 首先我们需要在上面的 RabbitmqConfig 类中创建消息模型:包括 Queue、Exchange、RoutingKey 等的建立,代码如下: 上图中 env 获取的信息,我们需要在 application.properties 进行配置,其中 mq.env=local此时,我们将整个项目/服务跑起来,并打开 RabbitMQ 后端控制台应用,即可看到队列以及交换机及其绑定已经建立好了,如下所示:接下来,我们需要在 Controller 中执行用户登录逻辑,记录用户登录日志,查询获取用户角色视野资源信息等,由于篇幅关系,在这里我们重点要实现的是用MQ实现 “异步记录用户登录日志” 的逻辑,即在这里 Controller 将充当“生产者”的角色,核心代码如下: @RestController public class UserController { private static final Logger log= LoggerFactory.getLogger(HelloWorldController.class); private static final String Prefix="user"; @Autowired private ObjectMapper objectMapper; @Autowired private UserMapper userMapper; @Autowired private UserLogMapper userLogMapper; @Autowired private RabbitTemplate rabbitTemplate; @Autowired private Environment env; @RequestMapping(value = Prefix+"/login",method = RequestMethod.POST,consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public BaseResponse login(@RequestParam("userName") String userName,@RequestParam("password") String password){ BaseResponse response=new BaseResponse(StatusCode.Success); try { //TODO:执行登录逻辑 User user=userMapper.selectByUserNamePassword(userName,password); if (user!=null){ //TODO:异步写用户日志 try { UserLog userLog=new UserLog(userName,"Login","login",objectMapper.writeValueAsString(user)); userLog.setCreateTime(new Date()); rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter()); rabbitTemplate.setExchange(env.getProperty("log.user.exchange.name")); rabbitTemplate.setRoutingKey(env.getProperty("log.user.routing.key.name")); Message message=MessageBuilder.withBody(objectMapper.writeValueAsBytes(userLog)).setDeliveryMode(MessageDeliveryMode.PERSISTENT).build(); message.getMessageProperties().setHeader(AbstractJavaTypeMapper.DEFAULT_CONTENT_CLASSID_FIELD_NAME, MessageProperties.CONTENT_TYPE_JSON); rabbitTemplate.convertAndSend(message); }catch (Exception e){ e.printStackTrace(); } //TODO:塞权限数据-资源数据-视野数据 }else{ response=new BaseResponse(StatusCode.Fail); } }catch (Exception e){ e.printStackTrace(); } return response; }} 在上面的“发送逻辑”代码中,其实也体现了我们最开始介绍的演进中的几种消息模型,比如我们是将消息发送到 Exchange 的而不是 Queue,消息是以二进制流的形式进行传输等等。当用 postman 请求到这个 controller 的方法时,我们可以在 RabbitMQ 的后端控制台应用看到一条未确认的消息,通过 GetMessage 即可看到其中的详情,如下: 最后,我们将开发消费端的业务代码,如下: @Component public class CommonMqListener { private static final Logger log= LoggerFactory.getLogger(CommonMqListener.class); @Autowired private ObjectMapper objectMapper; @Autowired private UserLogMapper userLogMapper; @Autowired private MailService mailService; /** * 监听消费用户日志 * @param message */ @RabbitListener(queues = "${log.user.queue.name}",containerFactory = "singleListenerContainer") public void consumeUserLogQueue(@Payload byte[] message){ try { UserLog userLog=objectMapper.readValue(message, UserLog.class); log.info("监听消费用户日志 监听到消息: {} ",userLog); //TODO:记录日志入数据表 userLogMapper.insertSelective(userLog); }catch (Exception e){ e.printStackTrace(); } } 将服务跑起来之后,我们即可监听消费到上面 Queue 中的消息,即当前用户登录的信息,而且,我们也可以看到“记录用户登录日志”的逻辑是由一条异于主业务线程的异步线程去执行的:“异步记录用户操作日志”的案例我想足以用于诠释上面所讲的相关理论知识点了,在后续篇章中,由于篇幅限制,我将重点介绍其核心的业务逻辑! 场景二:异步发送邮件发送邮件的场景,其实也是比较常见的,比如用户注册需要邮箱验证,用户异地登录发送邮件通知等等,在这里我以 RabbitMQ 实现异步发送邮件。实现的步骤跟场景一几乎一致! 消息模型的创建 配置信息的创建 生产端 消费端 彩蛋:本博文就先介绍RabbitMQ实战的典型业务场景之业务服务模块异步解耦与通信吧,下篇博文将继续讲解RabbitMQ实战在高并发系统的场景的应用记忆消息确认机制跟并发量的配置实战!另外,博主已将RabbitMQ相关技术以及场景实战的相关要点录制成了视频教程,感兴趣小伙伴可以前往学习观看:RabbitMQ实战 !!
课程介绍 在开始学习前给大家说下什么是Flink? 1.Flink是一个针对流数据和批数据的分布式处理引擎,主要用Java代码实现。 2.Apache Flink作为Apache的顶级项目,Flink集众多优点于一身,包括快速、可靠可扩展、完全兼容Hadoop、使用简便、表现卓越。 通过以上的描述大家对Flink有了一个基本的认识,本套课程不会讲解基础内容,因此建议有Flink基础的同学进行认购。 开始学习前建议大家认真阅读下文: 随着人工智能时代的降临,数据量的爆发,在典型的大数据的业务场景下数据业务最通用的做法是:选用批处理的技术处理全量数据,采用流式计算处理实时增量数据。在绝大多数的业务场景之下,用户的业务逻辑在批处理和流处理之中往往是相同的。但是,用户用于批处理和流处理的两套计算引擎是不同的。 因此,用户通常需要写两套代码。毫无疑问,这带来了一些额外的负担和成本。阿里巴巴的商品数据处理就经常需要面对增量和全量两套不同的业务流程问题,所以阿里就在想,我们能不能有一套统一的大数据引擎技术,用户只需要根据自己的业务逻辑开发一套代码。这样在各种不同的场景下,不管是全量数据还是增量数据,亦或者实时处理,一套方案即可全部支持,这就是阿里选择Flink的背景和初衷。 随着互联网不断发展,数据量不断的增加,大数据也是快速的发展起来了。对于电商系统,拥有着庞大的数据量,对于这么庞大的数据,传统的分析已经满足不了需求。对于电商来说,大数据数据分析是很重要的,它承载着公司的战略部署,以及运营、用户体验等多方面的作用。因此企业对大数据人才的需求会持续旺盛,优秀的大数据人才年收入在50-100万。 目前经过10多年的发展大数据技术也在不断的更新和进步中,大数据计算引擎经历了几个过程,从一代的Hadoop Mapreduce、二代的基于有向无环图的TeZ,OOZIE等,到三代的基于内存计算的Spark,再到最新的第四代Flink。 早期的Hadoop开发通过搭建环境收入都可以轻松破万,到如今Flink的崛起,相信更多的先机者会看到Flink的机遇。对于Flink巨头们早已经应用的非常成熟,比如阿里、Uber、美团等互联网巨头,因此Flink使用会越来越多,这是趋势,现在很多公司都在往Flink转换,足以可见Flink技术的先进和强大。 本课程将基于真实的电商分析系统构建,通过Flink实现真正的实时分析,该系统会从无到有一步一步带大家实现,让大家在实操中快速掌握Flink技术。 课程所涵盖的知识点包括:Flink、Kafka、Flume、Sqoop、SpringMVC、Redis、HDFS、Mapreduce、Hbase、Hive、SpringBoot、SpringCloud等等 分析指标包含:频道分析、产品分析、用户分析、活动效果分析、营销分析、购物车分析、订单分析等 课程所用到的 开发环境为:Window7 开发工具为:IDEA 开发版本为:Flink1.6.1、Hadoop2.6.0、Hbase1.0.0、Hive1.1.0 学完该课程大家会对Flink有非常深入的了解,同时可以体会到Flink的强大之处,以及可以结合自己公司的业务进行使用,减少自己研究和学习Flink的时间。
目前几乎很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题。分布式的CAP理论告诉我们“任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。”所以,很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证“最终一致性”,只要这个最终时间是在用户可以接受的范围内即可。 在很多场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。 选用Redis实现分布式锁原因 Redis有很高的性能 Redis命令对此支持较好,实现起来比较方便 使用命令介绍 SETNX SETNX key val 当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0。 expire expire key timeout 为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。 delete delete key 删除key 在使用Redis实现分布式锁的时候,主要就会使用到这三个命令。 实现 使用的是jedis来连接Redis。 实现思想 获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。 获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。 释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。 分布式锁的核心代码如下: import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.Transaction; import redis.clients.jedis.exceptions.JedisException; import java.util.List; import java.util.UUID; /** * Created by liuyang on 2017/4/20. */ public class DistributedLock { private final JedisPool jedisPool; public DistributedLock(JedisPool jedisPool) { this.jedisPool = jedisPool; } /** * 加锁 * @param locaName 锁的key * @param acquireTimeout 获取超时时间 * @param timeout 锁的超时时间 * @return 锁标识 */ public String lockWithTimeout(String locaName, long acquireTimeout, long timeout) { Jedis conn = null; String retIdentifier = null; try { // 获取连接 conn = jedisPool.getResource(); // 随机生成一个value String identifier = UUID.randomUUID().toString(); // 锁名,即key值 String lockKey = "lock:" + locaName; // 超时时间,上锁后超过此时间则自动释放锁 int lockExpire = (int)(timeout / 1000); // 获取锁的超时时间,超过这个时间则放弃获取锁 long end = System.currentTimeMillis() + acquireTimeout; while (System.currentTimeMillis() < end) { if (conn.setnx(lockKey, identifier) == 1) { conn.expire(lockKey, lockExpire); // 返回value值,用于释放锁时间确认 retIdentifier = identifier; return retIdentifier; } // 返回-1代表key没有设置超时时间,为key设置一个超时时间 if (conn.ttl(lockKey) == -1) { conn.expire(lockKey, lockExpire); } try { Thread.sleep(10); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } catch (JedisException e) { e.printStackTrace(); } finally { if (conn != null) { conn.close(); } } return retIdentifier; } /** * 释放锁 * @param lockName 锁的key * @param identifier 释放锁的标识 * @return */ public boolean releaseLock(String lockName, String identifier) { Jedis conn = null; String lockKey = "lock:" + lockName; boolean retFlag = false; try { conn = jedisPool.getResource(); while (true) { // 监视lock,准备开始事务 conn.watch(lockKey); // 通过前面返回的value值判断是不是该锁,若是该锁,则删除,释放锁 if (identifier.equals(conn.get(lockKey))) { Transaction transaction = conn.multi(); transaction.del(lockKey); List<Object> results = transaction.exec(); if (results == null) { continue; } retFlag = true; } conn.unwatch(); break; } } catch (JedisException e) { e.printStackTrace(); } finally { if (conn != null) { conn.close(); } } return retFlag; } } 测试 下面就用一个简单的例子测试刚才实现的分布式锁。例子中使用50个线程模拟秒杀一个商品,使用--运算符来实现商品减少,从结果有序性就可以看出是否为加锁状态。 模拟秒杀服务,在其中配置了jedis线程池,在初始化的时候传给分布式锁,供其使用。 import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; /** * Created by liuyang on 2017/4/20. */ public class Service { private static JedisPool pool = null; static { JedisPoolConfig config = new JedisPoolConfig(); // 设置最大连接数 config.setMaxTotal(200); // 设置最大空闲数 config.setMaxIdle(8); // 设置最大等待时间 config.setMaxWaitMillis(1000 * 100); // 在borrow一个jedis实例时,是否需要验证,若为true,则所有jedis实例均是可用的 config.setTestOnBorrow(true); pool = new JedisPool(config, "127.0.0.1", 6379, 3000); } DistributedLock lock = new DistributedLock(pool); int n = 500; public void seckill() { // 返回锁的value值,供释放锁时候进行判断 String indentifier = lock.lockWithTimeout("resource", 5000, 1000); System.out.println(Thread.currentThread().getName() + "获得了锁"); System.out.println(--n); lock.releaseLock("resource", indentifier); } } // 模拟线程进行秒杀服务 public class ThreadA extends Thread { private Service service; public ThreadA(Service service) { this.service = service; } @Override public void run() { service.seckill(); } } public class Test { public static void main(String[] args) { Service service = new Service(); for (int i = 0; i < 50; i++) { ThreadA threadA = new ThreadA(service); threadA.start(); } } } 结果如下,结果为有序的。 若注释掉使用锁的部分 public void seckill() { // 返回锁的value值,供释放锁时候进行判断 //String indentifier = lock.lockWithTimeout("resource", 5000, 1000); System.out.println(Thread.currentThread().getName() + "获得了锁"); System.out.println(--n); //lock.releaseLock("resource", indentifier); } 从结果可以看出,有一些是异步进行的。 在分布式环境中,对资源进行上锁有时候是很重要的,比如抢购某一资源,这时候使用分布式锁就可以很好地控制资源。 当然,在具体使用中,还需要考虑很多因素,比如超时时间的选取,获取锁时间的选取对并发量都有很大的影响,上述实现的分布式锁也只是一种简单的实现,主要是一种思想。 文章来源:https://my.oschina.net/rechardchensir/blog/2231537相关阅读推荐:https://www.roncoo.com/course/list.html?courseName=redis
绝大部分写业务的程序员,在实际开发中使用 Redis 的时候,只会 Set Value 和 Get Value 两个操作,对 Redis 整体缺乏一个认知。这里对 Redis 常见问题做一个总结,解决大家的知识盲点。 1、为什么使用 Redis 在项目中使用 Redis,主要考虑两个角度:性能和并发。如果只是为了分布式锁这些其他功能,还有其他中间件 Zookpeer 等代替,并非一定要使用 Redis。 性能: 如下图所示,我们在碰到需要执行耗时特别久,且结果不频繁变动的 SQL,就特别适合将运行结果放入缓存。这样,后面的请求就去缓存中读取,使得请求能够迅速响应。 特别是在秒杀系统,在同一时间,几乎所有人都在点,都在下单。。。执行的是同一操作———向数据库查数据。根据交互效果的不同,响应时间没有固定标准。在理想状态下,我们的页面跳转需要在瞬间解决,对于页内操作则需要在刹那间解决。 并发: 如下图所示,在大并发的情况下,所有的请求直接访问数据库,数据库会出现连接异常。这个时候,就需要使用 Redis 做一个缓冲操作,让请求先访问到 Redis,而不是直接访问数据库。 使用 Redis 的常见问题 缓存和数据库双写一致性问题 缓存雪崩问题 缓存击穿问题 缓存的并发竞争问题 2、单线程的 Redis 为什么这么快 这个问题是对 Redis 内部机制的一个考察。很多人都不知道 Redis 是单线程工作模型。 原因主要是以下三点: 纯内存操作 单线程操作,避免了频繁的上下文切换 采用了非阻塞 I/O 多路复用机制 仔细说一说 I/O 多路复用机制,打一个比方:小名在 A 城开了一家快餐店店,负责同城快餐服务。小明因为资金限制,雇佣了一批配送员,然后小曲发现资金不够了,只够买一辆车送快递。 经营方式一 客户每下一份订单,小明就让一个配送员盯着,然后让人开车去送。慢慢的小曲就发现了这种经营方式存在下述问题: 时间都花在了抢车上了,大部分配送员都处在闲置状态,抢到车才能去送。 随着下单的增多,配送员也越来越多,小明发现快递店里越来越挤,没办法雇佣新的配送员了。 配送员之间的协调很花时间。 综合上述缺点,小明痛定思痛,提出了经营方式二。 经营方式二 小明只雇佣一个配送员。当客户下单,小明按送达地点标注好,依次放在一个地方。最后,让配送员依次开着车去送,送好了就回来拿下一个。上述两种经营方式对比,很明显第二种效率更高。 在上述比喻中: 每个配送员→每个线程 每个订单→每个 Socket(I/O 流) 订单的送达地点→Socket 的不同状态 客户送餐请求→来自客户端的请求 明曲的经营方式→服务端运行的代码 一辆车→CPU 的核数 于是有了如下结论: 经营方式一就是传统的并发模型,每个 I/O 流(订单)都有一个新的线程(配送员)管理。 经营方式二就是 I/O 多路复用。只有单个线程(一个配送员),通过跟踪每个 I/O 流的状态(每个配送员的送达地点),来管理多个 I/O 流。 下面类比到真实的 Redis 线程模型,如图所示: Redis-client 在操作的时候,会产生具有不同事件类型的 Socket。在服务端,有一段 I/O 多路复用程序,将其置入队列之中。然后,文件事件分派器,依次去队列中取,转发到不同的事件处理器中。 3、Redis 的数据类型及使用场景 一个合格的程序员,这五种类型都会用到。 String 最常规的 set/get 操作,Value 可以是 String 也可以是数字。一般做一些复杂的计数功能的缓存。 Hash 这里 Value 存放的是结构化的对象,比较方便的就是操作其中的某个字段。我在做单点登录的时候,就是用这种数据结构存储用户信息,以 CookieId 作为 Key,设置 30 分钟为缓存过期时间,能很好的模拟出类似 Session 的效果。 List 使用 List 的数据结构,可以做简单的消息队列的功能。另外,可以利用 lrange 命令,做基于 Redis 的分页功能,性能极佳,用户体验好。 Set 因为 Set 堆放的是一堆不重复值的集合。所以可以做全局去重的功能。我们的系统一般都是集群部署,使用 JVM 自带的 Set 比较麻烦。另外,就是利用交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能。 Sorted Set Sorted Set 多了一个权重参数 Score,集合中的元素能够按 Score 进行排列。可以做排行榜应用,取 TOP N 操作。Sorted Set 可以用来做延时任务。 4、Redis 的过期策略和内存淘汰机制 Redis 是否用到家,从这就能看出来。比如你 Redis 只能存 5G 数据,可是你写了 10G,那会删 5G 的数据。怎么删的,这个问题思考过么? 正解:Redis 采用的是定期删除+惰性删除策略。 为什么不用定时删除策略 定时删除,用一个定时器来负责监视 Key,过期则自动删除。虽然内存及时释放,但是十分消耗 CPU 资源。在大并发请求下,CPU 要将时间应用在处理请求,而不是删除 Key,因此没有采用这一策略。 定期删除+惰性删除如何工作 定期删除,Redis 默认每个 100ms 检查,有过期 Key 则删除。需要说明的是,Redis 不是每个 100ms 将所有的 Key 检查一次,而是随机抽取进行检查。如果只采用定期删除策略,会导致很多 Key 到时间没有删除。于是,惰性删除派上用场。 采用定期删除+惰性删除就没其他问题了么 不是的,如果定期删除没删除掉 Key。并且你也没及时去请求 Key,也就是说惰性删除也没生效。这样,Redis 的内存会越来越高。那么就应该采用内存淘汰机制。 在 redis.conf 中有一行配置: # maxmemory-policy volatile-lru 该配置就是配内存淘汰策略的: noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。 allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 Key。(推荐使用,目前项目在用这种)(最近最久使用算法) allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个 Key。(应该也没人用吧,你不删最少使用 Key,去随机删) volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 Key。这种情况一般是把 Redis 既当缓存,又做持久化存储的时候才用。(不推荐) volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 Key。(依然不推荐) volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 Key 优先移除。(不推荐) 5、Redis 和数据库双写一致性问题 一致性问题还可以再分为最终一致性和强一致性。数据库和缓存双写,就必然会存在不一致的问题。前提是如果对数据有强一致性要求,不能放缓存。我们所做的一切,只能保证最终一致性。 另外,我们所做的方案从根本上来说,只能降低不一致发生的概率。因此,有强一致性要求的数据,不能放缓存。首先,采取正确更新策略,先更新数据库,再删缓存。其次,因为可能存在删除缓存失败的问题,提供一个补偿措施即可,例如利用消息队列。 6、如何应对缓存穿透和缓存雪崩问题 这两个问题,一般中小型传统软件企业很难碰到。如果有大并发的项目,流量有几百万左右,这两个问题一定要深刻考虑。缓存穿透,即黑客故意去请求缓存中不存在的数据,导致所有的请求都怼到数据库上,从而数据库连接异常。 缓存穿透解决方案: 利用互斥锁,缓存失效的时候,先去获得锁,得到锁了,再去请求数据库。没得到锁,则休眠一段时间重试。 采用异步更新策略,无论 Key 是否取到值,都直接返回。Value 值中维护一个缓存失效时间,缓存如果过期,异步起一个线程去读数据库,更新缓存。需要做缓存预热(项目启动前,先加载缓存)操作。 提供一个能迅速判断请求是否有效的拦截机制,比如,利用布隆过滤器,内部维护一系列合法有效的 Key。迅速判断出,请求所携带的 Key 是否合法有效。如果不合法,则直接返回。 缓存雪崩,即缓存同一时间大面积的失效,这个时候又来了一波请求,结果请求都怼到数据库上,从而导致数据库连接异常。 缓存雪崩解决方案: 给缓存的失效时间,加上一个随机值,避免集体失效。 使用互斥锁,但是该方案吞吐量明显下降了。 双缓存。我们有两个缓存,缓存 A 和缓存 B。缓存 A 的失效时间为 20 分钟,缓存 B 不设失效时间。自己做缓存预热操作。然后细分以下几个小点:从缓存 A 读数据库,有则直接返回;A 没有数据,直接从 B 读数据,直接返回,并且异步启动一个更新线程,更新线程同时更新缓存 A 和缓存 B。 7、如何解决 Redis 的并发竞争 Key 问题 这个问题大致就是,同时有多个子系统去 Set 一个 Key。这个时候要注意什么呢?大家基本都是推荐用 Redis 事务机制。 但是我并不推荐使用 Redis 的事务机制。因为我们的生产环境,基本都是 Redis 集群环境,做了数据分片操作。你一个事务中有涉及到多个 Key 操作的时候,这多个 Key 不一定都存储在同一个 redis-server 上。因此,Redis 的事务机制,十分鸡肋。 如果对这个 Key 操作,不要求顺序 这种情况下,准备一个分布式锁,大家去抢锁,抢到锁就做 set 操作即可,比较简单。 如果对这个 Key 操作,要求顺序 假设有一个 key1,系统 A 需要将 key1 设置为 valueA,系统 B 需要将 key1 设置为 valueB,系统 C 需要将 key1 设置为 valueC。 期望按照 key1 的 value 值按照 valueA > valueB > valueC 的顺序变化。这种时候我们在数据写入数据库的时候,需要保存一个时间戳。 假设时间戳如下: 系统 A key 1 {valueA 3:00}系统 B key 1 {valueB 3:05}系统 C key 1 {valueC 3:10} 那么,假设系统 B 先抢到锁,将 key1 设置为{valueB 3:05}。接下来系统 A 抢到锁,发现自己的 valueA 的时间戳早于缓存中的时间戳,那就不做 set 操作了,以此类推。其他方法,比如利用队列,将 set 方法变成串行访问也可以。 8、总结 Redis 在国内各大公司都能看到其身影,比如我们熟悉的新浪,阿里,腾讯,百度,美团,小米等。学习 Redis,这几方面尤其重要:Redis 客户端、Redis 高级功能、Redis 持久化和开发运维常用问题探讨、Redis 复制的原理和优化策略、Redis 分布式解决方案等。 文章来源:https://www.cnblogs.com/yaodengyan/p/9717080.html推荐阅读:https://www.roncoo.com/course/list.html?courseName=redis
引言 我们先来讲一个段子 面试官:“有并发的经验没?”应聘者:“有一点。” 面试官:“那你们为了处理并发,做了哪些优化?” 应聘者:“前后端分离啊,限流啊,分库分表啊。。” 面试官:"谈谈分库分表吧?" 应聘者:“bala。bala。bala。。” 面试官心理活动:这个仁兄讲的怎么这么像网上的博客抄的,容我再问问。 面试官:“你们分库分表后,如何部署上线的?” 应聘者:“这!!!!!!” 不要惊讶,写这篇文章前,我特意去网上看了下分库分表的文章,很神奇的是,都在讲怎么进行分库分表,却不说分完以后,怎么部署上线的。这样在面试的时候就比较尴尬了。 你们自己摸着良心想一下,如果你真的做过分库分表,你会不知道如何部署的么?因此我们来学习一下如何部署吧。 ps: 我发现一个很神奇的现象。因为很多公司用的技术比较low,那么一些求职者为了提高自己的竞争力,就会将一些高大上的技术写进自己的low项目中。然后呢,他出去面试害怕碰到从这个公司出来的人,毕竟从这个公司出来的人,一定知道自己以前公司的项目情形。因此为了圆谎,他就会说:“他们从事的是这个公司的老项目改造工作,用了很多新技术进去!” 那么,请你好好思考一下,你们的老系统是如何平滑升级为新系统的! 如何部署 停机部署法 大致思路就是,挂一个公告,半夜停机升级,然后半夜把服务停了,跑数据迁移程序,进行数据迁移。 步骤如下: (1)出一个公告,比如“今晚00:00~6:00进行停机维护,暂停服务” (2)写一个迁移程序,读 db-old 数据库,通过中间件写入新库 db-new1 和 db-new2 ,具体如下图所示(3)校验迁移前后一致性,没问题就切该部分业务到新库。 顺便科普一下,这个中间件。现在流行的分库分表的中间件有两种,一种是 proxy 形式的,例如 mycat ,是需要额外部署一台服务器的。还有一种是 client 形式的,例如当当出的 Sharding-JDBC ,就是一个jar包,使用起来十分轻便。我个人偏向 Sharding-JDBC ,这种方式,无需额外部署,无其他依赖,DBA也无需改变原有的运维方式。 评价: 大家不要觉得这种方法low,我其实一直觉得这种方法可靠性很强。而且我相信各位读者所在的公司一定不是什么很牛逼的互联网公司,如果你们的产品凌晨1点的用户活跃数还有超过1000的,你们握个爪!毕竟不是所有人都在什么电商公司的,大部分产品半夜都没啥流量。所以此方案,并非没有可取之处。 但是此方案有一个缺点, 累! 不止身体累,心也累!你想想看,本来定六点结束,你五点把数据库迁移好,但是不知怎么滴,程序切新库就是有点问题。于是,眼瞅着天就要亮了,赶紧把数据库切回老库。第二个晚上继续这么干,简直是身心俱疲。 ps: 这里教大家一些技巧啊,如果你真的没做过分库分表,又想吹一波,涨一下工资,建议答这个方案。因为这个方案比较low,low到没什么东西可以深挖的,所以答这个方案,比较靠谱。 另外,如果面试官的问题是 你们怎么进行分库分表的? 这个问题问的很泛,所以回答这个问题建议自己主动把分表的策略,以及如何部署的方法讲出来。因为这么答,显得严谨一些。 不过,很多面试官为了卖弄自己的技术,喜欢这么问 分表有哪些策略啊?你们用哪种啊? ok。。这个问题具体指向了分库分表的某个方向了,你不要主动答如何进行部署的。等面试官问你,你再答。如果面试官没问,在面试最后一个环节,面试官会让你问让几个问题。你就问 你刚才刚好有提到分库分表的相关问题,我们当时部署的时候,先停机。然后半夜迁移数据,然后第二天将流量切到新库,这种方案太累,不知道贵公司有没有什么更好的方案? 那么这种情况下,面试官会有两种回答。第一种,面试官硬着头皮随便扯。第二种,面试官真的做过,据实回答。记住,面试官怎么回答的不重要。重点的是,你这个问题出去,会给面试官一种错觉:"这个小伙子真的做过分库分表。" 如果你担心进去了,真派你去做分库分表怎么办?OK,不要怕。我赌你试用期碰不到这个活。因为能进行分库分表,必定对业务非常熟。还在试用期的你,必定对业务不熟,如果领导给你这种活,我只能说他有一颗大心脏。 ok,指点到这里。面试本来就是一场斗智斗勇的过程,扯远了,回到我们的主题。 双写部署法(一) 这个就是不停机部署法,这里我需要先引进两个概念: 历史数据 和 增量数据 。 假设,我们是对一张叫做 test_tb 的表进行拆分,因为你要进行双写,系统里头和 test_tb表有关的业务之前必定会加入一段双写代码,同时往老库和新库中写,然后进行部署,那么 历史数据:在该次部署前,数据库表 test_tb 的有关数据,我们称之为历史数据。 增量数据:在该次部署后,数据库表 test_tb 的新产生的数据,我们称之为增量数据。 然后迁移流程如下 (1)先计算你要迁移的那张表的 max(主键) 。在迁移过程中,只迁移 db-old 中 test_tb 表里,主键小等于该 max(主键) 的值,也就是所谓的历史数据。 这里有特殊情况,如果你的表用的是uuid,没法求出 max(主键) ,那就以创建时间作为划分历史数据和增量数据的依据。如果你的表用的是uuid,又没有创建时间这个字段,我相信机智的你,一定有办法区分出历史数据和增量数据。 (2)在代码中,与 test_tb 有关的业务,多加一条往消息队列中发消息的代码,将操作的sql发送到消息队列中,至于消息体如何组装,大家自行考虑。 需要注意的是, 只发写请求的sql,只发写请求的sql,只发写请求的sql。重要的事情说三遍! 原因有二: (1)只有写请求的sql对恢复数据才有用。 (2)系统中,绝大部分的业务需求是读请求,写请求比较少。 注意了,在这个阶段,我们不消费消息队列里的数据。我们只发写请求,消息队列的消息堆积情况不会太严重! (3)系统上线。另外,写一段迁移程序,迁移 db-old 中 test_tb 表里,主键小于该 max(主键)的数据,也就是所谓的历史数据。 上面步骤(1)~步骤(3)的过程如下(3)校验迁移前后一致性,没问题就切该部分业务到新库。 顺便科普一下,这个中间件。现在流行的分库分表的中间件有两种,一种是 proxy 形式的,例如 mycat ,是需要额外部署一台服务器的。还有一种是 client 形式的,例如当当出的 Sharding-JDBC ,就是一个jar包,使用起来十分轻便。我个人偏向 Sharding-JDBC ,这种方式,无需额外部署,无其他依赖,DBA也无需改变原有的运维方式。 评价: 大家不要觉得这种方法low,我其实一直觉得这种方法可靠性很强。而且我相信各位读者所在的公司一定不是什么很牛逼的互联网公司,如果你们的产品凌晨1点的用户活跃数还有超过1000的,你们握个爪!毕竟不是所有人都在什么电商公司的,大部分产品半夜都没啥流量。所以此方案,并非没有可取之处。 但是此方案有一个缺点, 累! 不止身体累,心也累!你想想看,本来定六点结束,你五点把数据库迁移好,但是不知怎么滴,程序切新库就是有点问题。于是,眼瞅着天就要亮了,赶紧把数据库切回老库。第二个晚上继续这么干,简直是身心俱疲。 ps: 这里教大家一些技巧啊,如果你真的没做过分库分表,又想吹一波,涨一下工资,建议答这个方案。因为这个方案比较low,low到没什么东西可以深挖的,所以答这个方案,比较靠谱。 另外,如果面试官的问题是 你们怎么进行分库分表的? 这个问题问的很泛,所以回答这个问题建议自己主动把分表的策略,以及如何部署的方法讲出来。因为这么答,显得严谨一些。 不过,很多面试官为了卖弄自己的技术,喜欢这么问 分表有哪些策略啊?你们用哪种啊? ok。。这个问题具体指向了分库分表的某个方向了,你不要主动答如何进行部署的。等面试官问你,你再答。如果面试官没问,在面试最后一个环节,面试官会让你问让几个问题。你就问 你刚才刚好有提到分库分表的相关问题,我们当时部署的时候,先停机。然后半夜迁移数据,然后第二天将流量切到新库,这种方案太累,不知道贵公司有没有什么更好的方案? 那么这种情况下,面试官会有两种回答。第一种,面试官硬着头皮随便扯。第二种,面试官真的做过,据实回答。记住,面试官怎么回答的不重要。重点的是,你这个问题出去,会给面试官一种错觉:"这个小伙子真的做过分库分表。" 如果你担心进去了,真派你去做分库分表怎么办?OK,不要怕。我赌你试用期碰不到这个活。因为能进行分库分表,必定对业务非常熟。还在试用期的你,必定对业务不熟,如果领导给你这种活,我只能说他有一颗大心脏。 ok,指点到这里。面试本来就是一场斗智斗勇的过程,扯远了,回到我们的主题。 双写部署法(一) 这个就是不停机部署法,这里我需要先引进两个概念: 历史数据 和 增量数据 。 假设,我们是对一张叫做 test_tb 的表进行拆分,因为你要进行双写,系统里头和 test_tb表有关的业务之前必定会加入一段双写代码,同时往老库和新库中写,然后进行部署,那么 历史数据:在该次部署前,数据库表 test_tb 的有关数据,我们称之为历史数据。 增量数据:在该次部署后,数据库表 test_tb 的新产生的数据,我们称之为增量数据。 然后迁移流程如下 (1)先计算你要迁移的那张表的 max(主键) 。在迁移过程中,只迁移 db-old 中 test_tb 表里,主键小等于该 max(主键) 的值,也就是所谓的历史数据。 这里有特殊情况,如果你的表用的是uuid,没法求出 max(主键) ,那就以创建时间作为划分历史数据和增量数据的依据。如果你的表用的是uuid,又没有创建时间这个字段,我相信机智的你,一定有办法区分出历史数据和增量数据。 (2)在代码中,与 test_tb 有关的业务,多加一条往消息队列中发消息的代码,将操作的sql发送到消息队列中,至于消息体如何组装,大家自行考虑。 需要注意的是, 只发写请求的sql,只发写请求的sql,只发写请求的sql。重要的事情说三遍! 原因有二: (1)只有写请求的sql对恢复数据才有用。(2)系统中,绝大部分的业务需求是读请求,写请求比较少。 注意了,在这个阶段,我们不消费消息队列里的数据。我们只发写请求,消息队列的消息堆积情况不会太严重! (3)系统上线。另外,写一段迁移程序,迁移 db-old 中 test_tb 表里,主键小于该 max(主键)的数据,也就是所谓的历史数据。 上面步骤(1)~步骤(3)的过程如下等到 db-old 中的历史数据迁移完毕,则开始迁移增量数据,也就是在消息队列里的数据。 (4)将迁移程序下线,写一段订阅程序订阅消息队列中的数据 (5)订阅程序将订阅到到数据,通过中间件写入新库 (6)新老库一致性验证,去除代码中的双写代码,将涉及到 test_tb 表的读写操作,指向新库。 上面步骤(4)~步骤(6)的过程如下这里大家可能会有一个问题,在步骤(1)~步骤(3),系统对历史数据进行操作,会造成不一致的问题么? OK,不会。这里我们对 delete 操作和 update 操作做分析,因为只有这两个操作才会造成历史数据变动, insert 进去的数据都是属于增量数据。 (1)对 db-old 的 test_tb 表的历史数据发出 delete 操作,数据还未删除,就被迁移程序给迁走了。此时 delete 操作在消息队列里还有记录,后期订阅程序订阅到该 delete 操作,可以进行删除。 (2)对 db-old 的 test_tb 表的历史数据发出 delete 操作,数据已经删除,迁移程序迁不走该行数据。此时 delete 操作在消息队列里还有记录,后期订阅程序订阅到该 delete 操作,再执行一次 delete ,并不会对一致性有影响。 对 update 的操作类似,不赘述。 双写部署法(二) 上面的方法有一个硬伤,注意我有一句话 (2)在代码中,与test_tb有关的业务,多加一条往消息队列中发消息的代码,将操作的sql发送到消息队列中,至于消息体如何组装,大家自行考虑。 大家想一下,这么做,是不是造成了严重的代码入侵。将非业务代码嵌入业务代码,这么做,后期删代码的时候特别累。 有没什么方法,可以避免这个问题的? 有的,订阅 binlog 日志。关于 binlog 日志,我尽量下周写一篇《研发应该掌握的binlog知识》,这边我就介绍一下作用 记录所有数据库表结构变更(例如CREATE、ALTER TABLE…)以及表数据修改(INSERT、UPDATE、DELETE…)的二进制日志。binlog不会记录SELECT和SHOW这类操作,因为这类操作对据本身并没有修改。 还记得我们在 双写部署法(一) 里介绍的,往消息队列里发的消息,都是写操作的消息。而 binlog 日志记录的也是写操作。所以订阅该日志,也能满足我们的需求。 于是步骤如下 (1)打开binlog日志,系统正常上线就好 (2)还是写一个迁移程序,迁移历史数据。步骤和上面类似,不啰嗦了。 步骤(1)~步骤(2)流程图如下(3)写一个订阅程序,订阅binlog(mysql中有 canal 。至于oracle中,大家就随缘自己写吧)。然后将订阅到到数据通过中间件,写入新库。 (4)检验一致性,没问题就切库。 步骤(3)~步骤(4)流程图如下怎么验数据一致性 这里大概介绍一下吧,这篇的篇幅太长了,大家心里有底就行。 (1)先验数量是否一致,因为验数量比较快。 至于验具体的字段,有两种方法: (2.1)有一种方法是,只验关键性的几个字段是否一致。 (2.2)还有一种是 ,一次取50条(不一定50条,具体自己定,我只是举例),然后像拼字符串一样,拼在一起。用md5进行加密,得到一串数值。新库一样如法炮制,也得到一串数值,比较两串数值是否一致。如果一致,继续比较下50条数据。如果发现不一致,用二分法确定不一致的数据在0-25条,还是26条-50条。以此类推,找出不一致的数据,进行记录即可。 合理利用自己每一分每一秒的时间来学习提升自己,不要再用"没有时间“来掩饰自己思想上的懒惰!趁年轻,使劲拼,给未来的自己一个交代! 文章来源:https://segmentfault.com/a/1190000016475827推荐阅读:https://www.roncoo.com/course/list.html?courseName=mysql
前言:刚开始学网络编程,都会先写一个客户端和服务端,不知道你们有没有试一下:再打开一下客户端,是连不上服务端的。还有一个问题不知道你们发现没:有时启服务器,会提示“Address already in use”,过一会就好了,想过为啥么?在这篇博客会解释这个问题。 但现实的服务器都会连很多客户端的,像阿里服务器等,所以这篇主要介绍如何实现并发服务器,主要通过三种方式:进程、线程和select函数来分别实现。 一、进程实现并发服务器先说下什么是并发服务器吧?不是指有多个服务器同时运行,而是可以同时连接多个客户端。 先简单说下原理吧,先画个图,如下:先要搞清楚通信的流程,图上参数说明: lfd:socket函数的返回值,就是监听描述符 cfd1/cfd2/cfd3:accept函数的返回值,用通信的套接字 server:服务器 client:客户端 socket通信过程中,总共有几个套接字呢?答:三个,客户端一个,服务器两个。 根据上图来大致说明一下流程: 客户端创建一个套接字描述符,用于通信,服务器先用socket函数创建套接字,用于监听客户端,然后调用accept函数,会返回一个套接字,用于通信的。图上就是,client1先通过cfd与server建立连接,然后与cfd1建立连接通信,这时lfd就空闲了,再监听客户端,client2再与lfd连接,再跟cfd2通信。client3也是如此。 现在问题就是。如何创建多个进程与客户端通信呢?通过循环创建子进程就可以实现这个问题 服务端程序,如下: #include <stdio.h> #include <string.h> #include <netinet/in.h> #include <arpa/inet.h> #include <signal.h> #include <sys/wait.h> #include <ctype.h> #include <unistd.h> #include "wrap.h" #define MAXLINE 8192 #define SERV_PORT 8000 void do_sigchild(int num) { while (waitpid(0, NULL, WNOHANG) > 0) ; } int main(void) { struct sockaddr_in servaddr, cliaddr; socklen_t cliaddr_len; int listenfd, connfd; char buf[MAXLINE]; char str[INET_ADDRSTRLEN]; int i, n; pid_t pid; struct sigaction newact; newact.sa_handler = do_sigchild; sigemptyset(&newact.sa_mask); newact.sa_flags = 0; sigaction(SIGCHLD, &newact, NULL); //建立信号,处理子进程退出 listenfd = Socket(AF_INET, SOCK_STREAM, 0); // int opt = 1; //setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); //端口复用 bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); Listen(listenfd, 20); printf("Accepting connections ...\n"); while (1) { cliaddr_len = sizeof(cliaddr); connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len); pid = fork(); if (pid == 0) { Close(listenfd); while (1) { n = Read(connfd, buf, MAXLINE); if (n == 0) { printf("the other side has been closed.\n"); break; } printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),ntohs(cliaddr.sin_port)); for (i = 0; i < n; i++) buf[i] = toupper(buf[i]); Write(STDOUT_FILENO, buf, n); Write(connfd, buf, n); } Close(connfd); return 0; } else if (pid > 0) { Close(connfd); } else perr_exit("fork"); } return 0; } 客户端程序如下: /* client.c */ #include <stdio.h> #include <string.h> #include <unistd.h> #include <netinet/in.h> #include <arpa/inet.h> #include "wrap.h" #define MAXLINE 8192 #define SERV_PORT 8000 int main(int argc, char *argv[]) { struct sockaddr_in servaddr; char buf[MAXLINE]; int sockfd, n; sockfd = Socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr); servaddr.sin_port = htons(SERV_PORT); Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); while (fgets(buf, MAXLINE, stdin) != NULL) { Write(sockfd, buf, strlen(buf)); n = Read(sockfd, buf, MAXLINE); if (n == 0) { printf("the other side has been closed.\n"); break; } else Write(STDOUT_FILENO, buf, n); } Close(sockfd); return 0; } 演示效果,服务器可以同时处理两个客户端,如下: 但我想再测试一下程序,执行./server,发现有个bind error,如下: 下面来解释一下这个问题: 先来一张图片(出自UNP),如下: 这张图将三次握手、四次挥手和TCP状态转换图,这些在我的这篇博客都由介绍,可以参考一下:https://www.cnblogs.com/liudw-0215/p/9661583.html 注意最后有一个TIME_WAIT状态,主动关闭一端会经历2MSL时长等待(大约40秒),再变为最开始的状态CLOSED。 复现上面的“bind error”,只需退出服务器,在启服务器,就会报出此错。因为主动关闭一端,会经历2MSL时长,端口IP会被占用,所以会报“bind error”。 但可能会问:为啥先退出客户端没有此问题?因为客户端没有调用bind函数地址结构,会“隐式”生成端口。 有没有方法可以解决这个问题呢?当然有的,调用函数setsockopt即可,服务端程序如下: #include <stdio.h> #include <string.h> #include <netinet/in.h> #include <arpa/inet.h> #include <signal.h> #include <sys/wait.h> #include <ctype.h> #include <unistd.h> #include "wrap.h" #define MAXLINE 8192 #define SERV_PORT 8000 void do_sigchild(int num) { while (waitpid(0, NULL, WNOHANG) > 0) ; } int main(void) { struct sockaddr_in servaddr, cliaddr; socklen_t cliaddr_len; int listenfd, connfd; char buf[MAXLINE]; char str[INET_ADDRSTRLEN]; int i, n; pid_t pid; struct sigaction newact; newact.sa_handler = do_sigchild; sigemptyset(&newact.sa_mask); newact.sa_flags = 0; sigaction(SIGCHLD, &newact, NULL); //建立信号,处理子进程退出 listenfd = Socket(AF_INET, SOCK_STREAM, 0); int opt = 1; setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); //端口复用 bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); Listen(listenfd, 20); printf("Accepting connections ...\n"); while (1) { cliaddr_len = sizeof(cliaddr); connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len); pid = fork(); if (pid == 0) { Close(listenfd); while (1) { n = Read(connfd, buf, MAXLINE); if (n == 0) { printf("the other side has been closed.\n"); break; } printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),ntohs(cliaddr.sin_port)); for (i = 0; i < n; i++) buf[i] = toupper(buf[i]); Write(STDOUT_FILENO, buf, n); Write(connfd, buf, n); } Close(connfd); return 0; } else if (pid > 0) { Close(connfd); } else perr_exit("fork"); } return 0; } 二、线程实现并发服务器 理解了进程的方式,就是创建多个线程来实现,就不过多解释了,程序需要对线程有一定了解,之后我还会写篇博客来介绍线程,敬请期待哦。 服务器代码如下,有有详细的解释: #include <stdio.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> #include <ctype.h> #include <unistd.h> #include <fcntl.h> #include "wrap.h" #define MAXLINE 8192 #define SERV_PORT 8000 struct s_info { //定义一个结构体, 将地址结构跟cfd捆绑 struct sockaddr_in cliaddr; int connfd; }; void *do_work(void *arg) { int n,i; struct s_info *ts = (struct s_info*)arg; char buf[MAXLINE]; char str[INET_ADDRSTRLEN]; //#define INET_ADDRSTRLEN 16 可用"[+d"查看 while (1) { n = Read(ts->connfd, buf, MAXLINE); //读客户端 if (n == 0) { printf("the client %d closed...\n", ts->connfd); break; //跳出循环,关闭cfd } printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &(*ts).cliaddr.sin_addr, str, sizeof(str)), ntohs((*ts).cliaddr.sin_port)); //打印客户端信息(IP/PORT) for (i = 0; i < n; i++) buf[i] = toupper(buf[i]); //小写-->大写 Write(STDOUT_FILENO, buf, n); //写出至屏幕 Write(ts->connfd, buf, n); //回写给客户端 } Close(ts->connfd); return (void *)0; } int main(void) { struct sockaddr_in servaddr, cliaddr; socklen_t cliaddr_len; int listenfd, connfd; pthread_t tid; struct s_info ts[256]; //根据最大线程数创建结构体数组. int i = 0; listenfd = Socket(AF_INET, SOCK_STREAM, 0); //创建一个socket, 得到lfd bzero(&servaddr, sizeof(servaddr)); //地址结构清零 servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //指定本地任意IP servaddr.sin_port = htons(SERV_PORT); //指定端口号 8000 Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); //绑定 Listen(listenfd, 128); //设置同一时刻链接服务器上限数 printf("Accepting client connect ...\n"); while (1) { cliaddr_len = sizeof(cliaddr); connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len); //阻塞监听客户端链接请求 ts[i].cliaddr = cliaddr; ts[i].connfd = connfd; /* 达到线程最大数时,pthread_create出错处理, 增加服务器稳定性 */ pthread_create(&tid, NULL, do_work, (void*)&ts[i]); pthread_detach(tid); //子线程分离,防止僵线程产生. i++; } return 0; } 客户端代码如下: /* client.c */ #include <stdio.h> #include <string.h> #include <unistd.h> #include <netinet/in.h> #include <arpa/inet.h> #include "wrap.h" #define MAXLINE 80 #define SERV_PORT 8000 int main(int argc, char *argv[]) { struct sockaddr_in servaddr; char buf[MAXLINE]; int sockfd, n; sockfd = Socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr.s_addr); servaddr.sin_port = htons(SERV_PORT); Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); while (fgets(buf, MAXLINE, stdin) != NULL) { Write(sockfd, buf, strlen(buf)); n = Read(sockfd, buf, MAXLINE); if (n == 0) printf("the other side has been closed.\n"); else Write(STDOUT_FILENO, buf, n); } Close(sockfd); return 0; } 三、select实现并发服务器select和进程主要区别在于,进程是阻塞的,而select是交给内核自己来实现的,由于select比较复杂,参考另一篇博客: 文章来源:https://www.cnblogs.com/liudw-0215/p/9664204.html推荐阅读:https://www.roncoo.com/article/index?title=%E5%B9%B6%E5%8F%91
一 前言 虽然已经有很多前辈已经分析过AbstractQueuedSynchronizer(简称AQS,也叫队列同步器)类,但是感觉那些点始终是别人的,看一遍甚至几遍终不会印象深刻。所以还是记录下来印象更深刻,还能和大家一起探讨(这就是重复造轮子的好处,另外也主要是这篇篇幅太长了,犹豫了好久才决定写作)。既然有很多前辈都分析过这个类说明它是多么的重要,下面我们看下concurrent包的实现示意图就清楚AQS的所占有的地位了。 二 什么是AQS AbstractQueuedSynchronizer,中文简称队列同步器,英文简称AQS。它是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。从上面图可以看出AQS是实现锁或任意同步组件的关键,通过继承同步器并实现它的抽象方法来管理同步状态等。 三 AQS的内部结构 个人习惯喜欢先看其内部结构,因为内部结果是一个类实现的核心。经过分析得知:AQS类底层的数据结构是使用双向链表,包括head结点和tail结点,head结点主要用作后续的调度。另外还包含一个单向链表,只有当使用Condition时,才会存在此单向链表。并且可能会有多个Condition 链表(其中链表是队列的一种具体表现,所以也可称作队列)。如下图: 四 内部结构源码解析 3.1 类的继承关系 1、说明它是一个抽象类,就说明它可能存在抽象方法需要子类去重写实现(具体有哪些方法需要重写后续会说明)。 2、它还继承了AbstractOwnableSynchronizer(简称AOS)类可以设置独占资源线程和获取独占资源线程(独占锁会涉及到,AOS的源码自己可以进去看看)。 另外建议各位多看看类上的注释,其实还蛮有作用的。 3.2 类的内部类 先分析内部类中的结构再看AQS是怎么引用它的。下面先看Node.class,主要分析都在注释上了。 /** * Wait queue node class. * 注意看类上的注释,上面是原注释的第一行,表示等待队列节点类(虽然实际上是一个双向链表)。 */ static final class Node { /** * 总共分为两者模式:共享和独占 */ /** 在共享模式中等待的节点 */ static final Node SHARED = new Node(); /** 在独占模式中等待的节点 */ static final Node EXCLUSIVE = null; /** * 下面几个表示节点状态,也就是waitStatus所具有可能的值。 */ /** * 标记线程处于取消状态 * 节点进入该状态就不会变化。 * / static final int CANCELLED = 1; /** * 标记后继节点的线程处于等待状态,需要被取消停放(即被唤醒unpark)。 * 变化情况:当当前节点的线程如果释放了同步状态或者被取消,将会通知后继节点,使后继节点的线程得以运行。 */ static final int SIGNAL = -1; /** * 标记线程正在等待条件(Condition),也就是该节点处于等待队列中。 * 变化情况:当其他线程对Condition调用了signal()方法后,该节点将会从等待队列中转移到同步队列中,加入到同步状态的获取中。 */ static final int CONDITION = -2; /** * 表示下一次共享式同步状态获取将会无条件的被传播下去。 */ static final int PROPAGATE = -3; /** * 节点状态,包含上面四种状态(另外还有一种初始化状态0) * 特别注意:它是volatile关键字修饰的,保证对其线程可见性,但是不保证原子性。 * 所以更新状态时,采用CAS方式去更新, 如:compareAndSetWaitStatus */ volatile int waitStatus; /** * 前驱节点,比如当前节点被取消,那就需要前驱节点和后继节点来完成连接。 */ volatile Node prev; /** * 后继节点。 */ volatile Node next; /** * 入队列时的当前线程。 */ volatile Thread thread; /** * 存储condition队列中的后继节点。 */ Node nextWaiter; /** * 判断是否共享模式 */ final boolean isShared() { return nextWaiter == SHARED; } /** * 获取前置节点,如果前置节点为空就抛出异常 */ final Node predecessor() throws NullPointerException { Node p = prev; if (p == null) throw new NullPointerException(); else return p; } // 省略三个构造函数 } 总结下:当每个线程被阻塞时都会封装成一个Node节点,放入队列中。每个节点都包含了当前节点对应的线程、状态、前置节点引用、后继节点引用以及下一个等待者。 其中还需要注意的是waitStatus对应的各个状态代表着什么意思,另外不清楚volatile关键字作用的请前去阅读下。 接下来简单看看ConditionObject的源码,后续我们会单独分析下这个类的作用。 /** * 实现Condition接口 */ public class ConditionObject implements Condition, java.io.Serializable { private static final long serialVersionUID = 1173984872572414699L; /** * 条件队列的第一个节点。 */ private transient AbstractQueuedSynchronizer.Node firstWaiter; /** * 条件队列的最后一个节点。 */ private transient AbstractQueuedSynchronizer.Node lastWaiter; } 从中可以看它还是实现了Condition接口,而Condition接口又定义了什么规范呢?自己去看:),你会不会发现有点跟Object中的几个方法类似呢。 3.3 主要内部成员 // 头结点 private transient volatile Node head; // 尾结点 private transient volatile Node tail; // 同步状态 private volatile int state; 五 总结 通过上述分析就很清楚其内部结构是什么了吧。总结下: 节点(Node)是成为sync队列和condition队列构建的基础,在同步器中就包含了sync队列(Node双向链表)。同步器拥有三个成员变量:sync队列的头结点head、sync队列的尾节点tail和状态state。对于锁的获取,请求形成节点,将其挂载在尾部,而锁资源的转移(释放再获取)是从头部开始向后进行。对于同步器维护的状态state,多个线程对其的获取将会产生一个链式的结构。 文章来源:https://www.cnblogs.com/yuanfy008/p/9608666.html 推荐阅读:https://www.roncoo.com/course/list.html?courseName=%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B
什么是线程中断? 在我们的Java程序中其实有不止一条执行线程,只有当所有的线程都运行结束的时候,这个Java程序才算运行结束。 官方的话给你描述一下:当所有的非守护线程运行结束时,或者其中一个线程调用了System.exit()方法时,这个Java程序才能运行结束。 线程中断的应用场景 我们先来举一个例子,比如我们现在在下载一个500多M的大片,我们点击开始下载,那个这个时候就等于开启了一个线程去下载我们的文件,然而这个时候我们的网速不是很给力,几十KB的在这跑,作为一个年轻人我是等不了了,我不下来,那么这个时候我们第一个操作就是结束掉这个下载文件的操作,其实更接近程序的来说,这个时候我们就需要把这个线程给中断了。 我们接下来写一下这个下载的代码,看一下如何中断一个线程,这里我已经默认你们已经掌握了如何创建一个线程了,这段程序我们模拟下载,最开始获取系统时间,然后进入循环每次获取系统时间,如果时间超过10秒我们就中断线程,不在继续下载,下载速度时每秒1M: public void run() { int number = 0; // 记录程序开始的时间 Long start = System.currentTimeMillis(); while (true) { // 每次执行一次结束的时间 Long end = System.currentTimeMillis(); // 获取时间差 Long interval = end - start; // 如果时间超过了10秒,那么我们就结束下载 if (interval >= 10000) { // 中断线程 interrupted(); System.err.println("太慢了,我不下了"); return; } else if (number >= 500) { System.out.println("文件下载完成"); // 中断线程 interrupted(); return; } number++; System.out.println("已下载" + number + "M"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } } 中断线程的方式 Thread类中给我们提供了中断线程的方法,我们先来看下这个方法到底是如何让线程中断的: public static boolean interrupted() { return currentThread().isInterrupted(true); } 这个方法是检查当前线程是否被中断,中断返回true,未中断返回false private native boolean isInterrupted(boolean ClearInterrupted); 通过查看源码我们可以发现,中断线程就是通过调用检查线程是否被中断的方法,把值设为true。这个时候你再去调用检查线程是否中断的方法时就返回true了。 这里大家需要注意一个问题:Thread.interrupted()方法只是修改了当前线程的状态告诉他被中断了,但是对于非阻塞中的线程,只是改变了中断状态,即 Thread.isInterrupted()返回true,对于可取消的阻塞状态中的线程,例如等待在这些函数上的线程 ,Thread.sleep(),这个线程收到中断信号之后就会抛出InterruptedException异常,同时会把中断状态设置为true。 线程睡眠引起InterruptedException异常的原因 其实这样说大家也是一知半解,我就写一个错误的示例,大家来看一下,把这个问题彻底的搞清楚: public void run() { int number = 0; while (true) { // 检查线程是否被中断,中断就停止下载 if (isInterrupted()) { System.err.println("太慢了,我不下了"); return; } else if (number >= 500) { System.out.println("下载完成"); return; } number++; System.out.println("已下载" + number + "M"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } } 这是我们的主程序,等待10秒后中断线程 public static void main(String[] args) throws InterruptedException { Thread thread = new PrimeGenerator(); // 启动线程 thread.start(); // 等待10秒后中断线程 Thread.sleep(1000); // 中断线程 thread.interrupt(); } 看起来很通常的一个程序,但是事实却并非你看到的样子,其实这段代码是会抛出InterruptedException异常的,我们来分析原因。 这里我们先要了解Thread.interrupt()方法不会中断一个正在运行的线程,调用Thread.sleep()方法时,这个时候就不再占用CPU,我们来分析下我们这个程序,我们下载是要等待10秒,每次下载的速度是0.5M/S,也就是当我们下载到5M的时候等待时间已经到了,这个时候调用Thread.interrupt()方法中断线程,但是run()方法中的睡眠还要接着往下执行,它是不会因为中断而放弃执行下面的代码的,那么这个时候当它再执行Thread.sleep()的时候就会抛出InterruptedException异常,因为当前的线程已经被中断了。 说到这里,你是否已经明白产生这个异常的原因了?另外还有另外的两个原因致使线程产生InterruptedException异常的原因,wait()、join()两个方法使用不当也会引起线程抛出该异常。 查看线程是否中断的两种方式 在Thread类中有一个方法interrupted()可以用来检查当前线程时候被中断,还有isInterrupted()方法可以用来检查当前线程是否被中断。 中断线程的方法其实底层就是将这个属性设置为true,isInterrupted()方法只是返回了这个属性值而已。 这两个方法有一个区别就是isInterrupted()不能改变interrupted()的属性值,但是interrupted()方法却能改变interrupted的属性值,所以在判断一个线程时候被中断的时候我们更推荐使用isInterrupted()。 文章来源:https://my.oschina.net/u/3178270/blog/2045625 推荐阅读:https://www.roncoo.com/article/index?title=%E7%BA%BF%E7%A8%8B
事务定义的是一系列数据库操作的序列,这个序列是一个不可分割的逻辑单元,在其中的操作要么全部完成,要么全部无法完成。Spring事务通过Transactional.isolation属性进行定义,其具体值则存储在Isolation枚举中。Spring对事务隔离级别的定义与数据库隔离级别的定义是完全一致的,因而本文主要从数据库的层面对事务进行讲解。 1. 事务 在事务的定义上,其主要有四大特性:原子性、一致性、隔离性和持久性,简称为ACID。这四大特性的含义如下: 原子性:在一个事务中的操作都是一个逻辑的单元,在执行事务序列时,这些操作要么全部成功,要么全部失败; 一致性:对于事务操作,其始终能够保证在事务操作成功或者失败回滚时能够达到一种一致的状态,简而言之,事务中的操作要么全部成功,要么全部失败; 隔离性:各个事务之间的执行是相互不可见的,在事务还未执行成功时,其他的事务只能看到当前事务开始执行的状态,只有在当前事务执行完成之后才能看到执行之后的状态; 持久性:事务的持久性指的是事务一旦执行成功,那么其所做的修改将永久的保存在数据库中,此时即使数据库崩溃,修改的数据也不会丢失。 关于事务的持久性需要说明的是,从事务的角度能够保证数据能够一致性的保存在磁盘上,即使数据库发生故障也能够从故障中恢复,但是如果是数据库之外的问题,比如RAID卡损坏,自然灾害等,这种问题在数据库层面是无法避免的,其也不属于事务的范畴。事务能够保证数据的高可靠性,但是事务并不能保证系统的高可用行。 事务能够始终保证数据保持在一种一致的状态,但是如果严格按照事务的定义来处理事务,那么事务的执行效率将会很低,因为只有保证了所有事务的串行执行才能保证事务,因而在事务规范中为事务定义了四种隔离级别:Read uncommitted、Read committed、Repeatable read和Serializable。关于这四种隔离级别,其主要区别在于三个点:脏读、不可重复读和幻读。这三个点的主要含义如下: 脏读:脏读表示一个事务能够读取另一个事务中还未提交的数据。比如,某个事务尝试插入记录A,此时该事务还未提交,然后另一个事务尝试读取记录A,这时其是会成功读取到记录A的; 不可重复读:不可重复读表示当前事务对同一记录的两次重复读取结果不一致。比如一个事务首先读取一条记录A,读完之后另一个事务将该记录修改并且成功提交了,然后当前事务再次读取记录A,此时该事务会发现两次读取的结果不一致; 读:幻读指的是一个事务在进行一次查询之后发现某个记录不存在,然后会根据这个结果进行下一步操作,此时如果另一个事务成功插入了该记录,那么对于第一个事务而言,其进行下一步操作(比如插入该记录)的时候很可能会报错。从事务使用的角度来看,在检查一条记录不存在之后,其进行插入应该完全没问题的,但是这里却抛出主键冲突的异常。 关于事务的四种隔离级别,其主要区别点也就在于是否能够解决这三个问题。这四种事务的隔离级别主要区别如下: Read uncommitted:这是隔离性最低的一种隔离级别,在这种隔离级别下,当前事务能够读取到其他事务已经更改但还未提交的记录,也就是脏读; Read committed:顾名思义,这种隔离级别只能读取到其他事务已经提交的数据,也就解决了脏读的问题,但是其无法解决不可重复读和幻读的问题; Repeatable read:从事务的定义上,这种隔离级别能够解决脏读和不可重复读的问题,但是其无法解决幻读的问题; Serializable:也称为序列化读,这是隔离性最高的一种隔离级别,所有的事务执行(包括查询)都会为所处理的数据加锁,操作同一数据的事务将会串行的等待。 从事务隔离级别的定义上可以看出,Serializable级别隔离性最高,但是其效率也最低,因为其要求所有操作相同记录的事务都串行的执行。这里需要说明的是,对于MySql而言,其默认事务级别是Repeatable read,虽然在定义上讲,这种隔离级别无法解决幻读的问题,但是MySql使用了一种Next key-lock的算法来实现Repeatable read,这种算法是能够解决幻读问题的。关于Next key-lock算法,在进行查询时,其不仅会将当前的操作记录锁住,也会将查询所涉及到的范围锁住。也就是说,其他事务如果想要在当前事务查询的范围内进行数据操作,那么其是会被阻塞的,因而MySql在Repeatable read隔离级别下就已经具备了Serializable隔离级别的事务隔离性。 2. 示例演示 关于四种事务隔离级别的演示,我们主要使用MySql客户端进行。这里首先需要说明的几个命令是关于事务的几个基本操作命令: -- 设置当前会话的事务隔离级别,需要严格注意区分命令中的大小写,这里四种隔离级别分别是:Read uncommitted,Read committed,Repeatable read,Serializable SET session TRANSACTION ISOLATION LEVEL Read uncommitted; -- 查看当前会话的事务隔离级别 show variables like 'transaction_isolation'; -- 开始一个事务 start transaction; -- 回滚当前事务 rollback; -- 提交当前事务 commit; 首先我们建立如下的数据库表结构: create table user( id bigint auto_increment comment '主键', name varchar(20) not null default '' comment '名称', age int(3) not null default 0 comment '年龄', primary key(id) ); 关于下面的演示过程,这里都省略了事务隔离级别切换的命令,读者可以自行进行切换。 1、Read uncommitted 首先我们开启两个Mysql命令行,并且设置事务隔离级别为Read uncommitted。对于Read uncommitted,理论上在一个会话中开启事务之后,另一个会话插入一条未提交的数据,当前会话是可以读取到这条记录的。这里我们首先在会话A中执行如下命令: -- 会话A mysql> start transaction; Query OK, 0 rows affected (0.00 sec) 然后在会话B中开启事务,并插入一条记录: -- 会话B mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> insert into user(id, name, age) value (1, 'Mary', 23); Query OK, 1 row affected (0.00 sec) 此时再在会话A中尝试读取该记录: -- 会话A mysql> select * from user where id=1; +----+------+------+ | id | name | age | +----+------+------+ | 1 | Mary | 23 | +----+------+------+ 1 row in set (0.00 sec) 可以看到,在A和B两个会话事务都未提交的情况下,会话A中的事务是能够成功读取到会话B中事务未提交的数据的,这也就产生了脏读的问题。 2、Read committed 关于Read committed,其表示当前事务只能读取其他事务已经提交的数据,但是无法解决不可重复读的问题。 读取已提交的数据 这里的演示方式与脏读类似,首先在会话A中开启事务,然后在会话B中也开启事务,并且插入一条数据,此时在会话A中尝试读取该记录,应该是无法读取到结果的,如果在会话B中提交该事务之后,会话A中则应该可以读取到这条记录。 -- 会话A mysql> start transaction; Query OK, 0 rows affected (0.00 sec) 然后在会话B中开启事务并插入一条记录: -- 会话B mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> insert into user(id, name, age) value (1, 'Mary', 23); Query OK, 1 row affected (0.01 sec) 此时在会话A中读取该记录应该是无法读取到的: -- 会话A mysql> select * from user where id=1; Empty set (0.00 sec) 可以看到,这里会话A中的事务是无法读取到会话B中事务插入的还未提交的数据的。此时我们提交会话B中的事务,并且再次在会话A中尝试读取该记录: -- 会话B mysql> commit; Query OK, 0 rows affected (0.06 sec) -- 会话A mysql> select * from user where id=1; +----+------+------+ | id | name | age | +----+------+------+ | 1 | Mary | 23 | +----+------+------+ 1 row in set (0.00 sec) 可以看到,在会话B提交了事务之后,会话A是能够获取到会话B进行的修改的。 不可重复读 关于不可重复读,理论上,一个事务中,在对同一条记录的多次重复读取,得到的结果应该是始终一致的。这里Read committed隔离级别是没有这个特性的,因而如果我们在会话A中读取一条记录,然后在会话B中修改该记录并且提交,接着在会话A中再次进行读取,那么此时会话A中读取到的应该是修改之后的值。首先我们在会话A中开启事务,并且读取一条记录: -- 会话A mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from user where id=1; +----+------+------+ | id | name | age | +----+------+------+ | 1 | Mary | 23 | +----+------+------+ 1 row in set (0.00 sec) 然后在会话B中开启事务,修改一条记录,并且提交: -- 会话B mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> update user set age=25 where id=1; Query OK, 1 row affected (0.01 sec) Rows matched: 1 Changed: 1 Warnings: 0 mysql> commit; Query OK, 0 rows affected (0.04 sec) 接着我们在会话A中再次读取该记录: -- 会话A mysql> select * from user where id=1; +----+------+------+ | id | name | age | +----+------+------+ | 1 | Mary | 25 | +----+------+------+ 1 row in set (0.00 sec) 可以看到,会话A在事务还未提交的情况下,其重复读取同一条记录,两次读取的结果居然不一致,这也就是不可重复读。 3、Repeatable read 关于Repeatable read,在定义上,其解决了不可重复读的问题,但是没解决幻读的问题,这里由于MySql使用了Next key-lock算法,因而在这个隔离级别下,其也解决了幻读的问题。这里我们会对着两种情况依次进行演示。 可重复读 这里可重复读的演示与上面不可重复读的演示方式是一样的,只是这里将隔离级别设置为Repeatable read。 -- 会话A mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from user where id=1; +----+------+------+ | id | name | age | +----+------+------+ | 1 | Mary | 25 | +----+------+------+ 1 row in set (0.00 sec) 然后在会话B中开启事务,修改该记录,并且提交: -- 会话B mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> update user set age=30 where id=1; Query OK, 1 row affected (0.01 sec) Rows matched: 1 Changed: 1 Warnings: 0 mysql> commit; Query OK, 0 rows affected (0.05 sec) 这里会话B在事务中修改了id为1的记录的值,此时我们再次在会话A中读取该记录: -- 会话A mysql> select * from user where id=1; +----+------+------+ | id | name | age | +----+------+------+ | 1 | Mary | 25 | +----+------+------+ 1 row in set (0.00 sec) 可以看到,在会话A还未提交的时候,其读取的结果始终是一致的,并未受到会话B中已经提交的事务的影响。 幻读 关于幻读,最典型的示例就是在一个事务中进行数据插入时,MySQL首先会先检查该数据是否存在,如果不存在则插入数据,这个过程中,如果另一个事务也插入了同样的数据,那么这个事务是会被阻塞的,如果当前事务提交了,那么另一个事务就会抛出主键冲突的异常。 -- 会话A mysql> select * from user where id=2 for update; Empty set (0.01 sec) mysql> insert into user (id, name, age) value (2, 'Jack', 24); Query OK, 1 row affected (0.00 sec) 此时在会话B中开启事务,并且尝试插入同一条数据,那么其是会被阻塞的: -- 会话B mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> insert into user (id, name, age) value (2, 'Bob', 28); 可以看到,这里会话B中的插入操作是被阻塞了的。如果此时将会话A中的事务提交,那么会话B中将会抛出异常: -- 会话B mysql> insert into user (id, name, age) value (2, 'Bob', 28); ERROR 1062 (23000): Duplicate entry '2' for key 'PRIMARY' 这里关于幻读需要说明的一点是,MySql在Repeatable read级别解决的幻读问题是在MySQL级别处理的,比如上面的示例中,我们在会话A中查询时加上了for update,该命令会针对目标记录加锁,如果目标记录不存在,则会加上Gap锁,这样后面在同一事物中插入是可以成功的。如果在同一事物中只是单纯的查询,然后进行插入,那么还是会出现幻读的问题的。也就是说上面的示例中,如果将for update去掉,那么其还是会出现幻读的问题的。 4、Serializable 关于序列化读,这里就比较简单。对于一个事务而言,其所有的操作都会锁定所操作的数据和Gap,此时另外的事务只能等待该事务完成才能进行下一步操作。这里我们以两个事务同时查询同一事务中的同一记录为例进行展示: -- 会话A mysql> select * from user where id=2; +----+------+------+ | id | name | age | +----+------+------+ | 2 | Jack | 24 | +----+------+------+ 1 row in set (0.00 sec) 这里会话A是可以正常读取记录的,此时我们在会话B中尝试使用加锁的方式读取同一记录: -- 会话B mysql> select * from user where id=2 for update; ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction 可以看到,会话B中的事务尝试加锁是失败的,因为目标记录在会话A中已经被锁定了。 3. 小结 本文首先对事务的四个特性进行了讲解,然后讲解了事务存在的三个问题,接着讲解了事务定义的四种隔离级别是如何解决这三个问题的,最后通过示例讲解了这四个隔离级别的区别。 文章来源:https://my.oschina.net/zhangxufeng/blog/1942110 推荐阅读:https://www.roncoo.com/course/view/7ae3d7eddc4742f78b0548aa8bd9ccdb
前言 在我们日常的开发中,很多时候,定时任务都不是写死的,而是写到数据库中,从而实现定时任务的动态配置,下面就通过一个简单的示例,来实现这个功能。 一、新建一个springboot工程,并添加依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency><!-- 为了方便测试,此处使用了内存数据库 --> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz</artifactId> <version>2.2.1</version> <exclusions> <exclusion> <artifactId>slf4j-api</artifactId> <groupId>org.slf4j</groupId> </exclusion> </exclusions> </dependency> <dependency><!-- 该依赖必加,里面有sping对schedule的支持 --> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> </dependency> 二、配置文件application.properties # 服务器端口号 server.port=7902 # 是否生成ddl语句 spring.jpa.generate-ddl=false # 是否打印sql语句 spring.jpa.show-sql=true # 自动生成ddl,由于指定了具体的ddl,此处设置为none spring.jpa.hibernate.ddl-auto=none # 使用H2数据库 spring.datasource.platform=h2 # 指定生成数据库的schema文件位置 spring.datasource.schema=classpath:schema.sql # 指定插入数据库语句的脚本位置 spring.datasource.data=classpath:data.sql # 配置日志打印信息 logging.level.root=INFO logging.level.org.hibernate=INFO logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE logging.level.org.hibernate.type.descriptor.sql.BasicExtractor=TRACE logging.level.com.itmuch=DEBUG 三、Entity类 package com.chhliu.springboot.quartz.entity; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; @Entity public class Config { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; @Column private String cron; /** * @return the id */ public Long getId() { return id; } ……此处省略getter和setter方法…… } 四、任务类 package com.chhliu.springboot.quartz.entity; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.stereotype.Component; @Configuration @Component // 此注解必加 @EnableScheduling // 此注解必加 public class ScheduleTask { private static final Logger LOGGER = LoggerFactory.getLogger(ScheduleTask.class); public void sayHello(){ LOGGER.info("Hello world, i'm the king of the world!!!"); } } 五、Quartz配置类由于springboot追求零xml配置,所以下面会以配置Bean的方式来实现 package com.chhliu.springboot.quartz.entity; import org.quartz.Trigger; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.quartz.CronTriggerFactoryBean; import org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean; import org.springframework.scheduling.quartz.SchedulerFactoryBean; @Configuration public class QuartzConfigration { /** * attention: * Details:配置定时任务 */ @Bean(name = "jobDetail") public MethodInvokingJobDetailFactoryBean detailFactoryBean(ScheduleTask task) {// ScheduleTask为需要执行的任务 MethodInvokingJobDetailFactoryBean jobDetail = new MethodInvokingJobDetailFactoryBean(); /* * 是否并发执行 * 例如每5s执行一次任务,但是当前任务还没有执行完,就已经过了5s了, * 如果此处为true,则下一个任务会执行,如果此处为false,则下一个任务会等待上一个任务执行完后,再开始执行 */ jobDetail.setConcurrent(false); jobDetail.setName("srd-chhliu");// 设置任务的名字 jobDetail.setGroup("srd");// 设置任务的分组,这些属性都可以存储在数据库中,在多任务的时候使用 /* * 为需要执行的实体类对应的对象 */ jobDetail.setTargetObject(task); /* * sayHello为需要执行的方法 * 通过这几个配置,告诉JobDetailFactoryBean我们需要执行定时执行ScheduleTask类中的sayHello方法 */ jobDetail.setTargetMethod("sayHello"); return jobDetail; } /** * attention: * Details:配置定时任务的触发器,也就是什么时候触发执行定时任务 */ @Bean(name = "jobTrigger") public CronTriggerFactoryBean cronJobTrigger(MethodInvokingJobDetailFactoryBean jobDetail) { CronTriggerFactoryBean tigger = new CronTriggerFactoryBean(); tigger.setJobDetail(jobDetail.getObject()); tigger.setCronExpression("0 30 20 * * ?");// 初始时的cron表达式 tigger.setName("srd-chhliu");// trigger的name return tigger; } /** * attention: * Details:定义quartz调度工厂 */ @Bean(name = "scheduler") public SchedulerFactoryBean schedulerFactory(Trigger cronJobTrigger) { SchedulerFactoryBean bean = new SchedulerFactoryBean(); // 用于quartz集群,QuartzScheduler 启动时更新己存在的Job bean.setOverwriteExistingJobs(true); // 延时启动,应用启动1秒后 bean.setStartupDelay(1); // 注册触发器 bean.setTriggers(cronJobTrigger); return bean; } } 六、定时查库,并更新任务 package com.chhliu.springboot.quartz.entity; import javax.annotation.Resource; import org.quartz.CronScheduleBuilder; import org.quartz.CronTrigger; import org.quartz.JobDetail; import org.quartz.Scheduler; import org.quartz.SchedulerException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import com.chhliu.springboot.quartz.repository.ConfigRepository; @Configuration @EnableScheduling @Component public class ScheduleRefreshDatabase { @Autowired private ConfigRepository repository; @Resource(name = "jobDetail") private JobDetail jobDetail; @Resource(name = "jobTrigger") private CronTrigger cronTrigger; @Resource(name = "scheduler") private Scheduler scheduler; @Scheduled(fixedRate = 5000) // 每隔5s查库,并根据查询结果决定是否重新设置定时任务 public void scheduleUpdateCronTrigger() throws SchedulerException { CronTrigger trigger = (CronTrigger) scheduler.getTrigger(cronTrigger.getKey()); String currentCron = trigger.getCronExpression();// 当前Trigger使用的 String searchCron = repository.findOne(1L).getCron();// 从数据库查询出来的 System.out.println(currentCron); System.out.println(searchCron); if (currentCron.equals(searchCron)) { // 如果当前使用的cron表达式和从数据库中查询出来的cron表达式一致,则不刷新任务 } else { // 表达式调度构建器 CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(searchCron); // 按新的cronExpression表达式重新构建trigger trigger = (CronTrigger) scheduler.getTrigger(cronTrigger.getKey()); trigger = trigger.getTriggerBuilder().withIdentity(cronTrigger.getKey()) .withSchedule(scheduleBuilder).build(); // 按新的trigger重新设置job执行 scheduler.rescheduleJob(cronTrigger.getKey(), trigger); currentCron = searchCron; } } } 七、相关脚本1、data.sql insert into config(id,cron) values(1,'0 0/2 * * * ?'); # 每2分钟执行一次定时任务 2、schema.sql drop table config if exists; create table config( id bigint generated by default as identity, cron varchar(40), primary key(id) ); 八、运行测试测试结果如下:(Quartz默认的线程池大小为10) 0 30 20 * * ? 0 0/2 * * * ? 2017-03-08 18:02:00.025 INFO 5328 --- [eduler_Worker-1] c.c.s.quartz.entity.ScheduleTask : Hello world, i'm the king of the world!!! 2017-03-08 18:04:00.003 INFO 5328 --- [eduler_Worker-2] c.c.s.quartz.entity.ScheduleTask : Hello world, i'm the king of the world!!! 2017-03-08 18:06:00.002 INFO 5328 --- [eduler_Worker-3] c.c.s.quartz.entity.ScheduleTask : Hello world, i'm the king of the world!!! 2017-03-08 18:08:00.002 INFO 5328 --- [eduler_Worker-4] c.c.s.quartz.entity.ScheduleTask : Hello world, i'm the king of the world!!! 从上面的日志打印时间来看,我们实现了动态配置,最初的时候,任务是每天20:30执行,后面通过动态刷新变成了每隔2分钟执行一次。虽然上面的解决方案没有使用Quartz推荐的方式完美,但基本上可以满足我们的需求,当然也可以采用触发事件的方式来实现,例如当前端修改定时任务的触发时间时,异步的向后台发送通知,后台收到通知后,然后再更新程序,也可以实现动态的定时任务刷新 文章来源:https://blog.csdn.net/liuchuanhong1/article/details/60873295 推荐阅读:https://www.roncoo.com/course/view/e2b459016e2e477dbd5d67c8b23fe86d
权限设计的杂谈 这篇文章的定位,不是宣传某个框架,仅仅之是梳理一下有关权限方面的一些想法和最近项目中的一些探索过程。 我们主要想解决一下问题。 1、什么是权限,程序员理解的权限和客户所理解的权限是不是一致的。2、权限的划分原则,权限到底是根据什么原则进行组合的。3、角色是用户与权限之间的必要的关系吗?角色到底承接了什么作用。4、如何进行合理的表设计。5、安全框架。 1.什么是权限 在很多与开发者也好,与客户也好,沟通的过程中我们很多次提到了权限,但是权限具体的含义每个人理解的含义都不明确,这样很容易造成双方信息不对称,有的人就只是把权限理解成某个页面的是否可访问,但是有的人却理解成其他的东西。所以我们要彻底的定义一下权限是什么。 权限到底是名词属性还是动词属性,还是名词、动词属性均包含,这对于权限的含义很重要。如果是名词属性的话,那么它应该是有具体的指代物;如果是动词,则应该具有行为表示。 权限的名词属性:api接口、页面、功能点。 权限的动词属性:可操作、不可操作。 那么我们现在来看,其实权限是名词、动词属性,它一定是表达了两层含义。即控制的对象、操作。 1、例如:权限A表示页面A的可访问。2、例如:权限B表示页面B可访问且页面内的功能b不可使用3、例如:权限C表示接口C不可调用。4、例如:权限D表示页面D可访问,且接口D可访问。 那么进一步的说明,权限可以表示单个控制的对象的操作集合,也可以表示多个控制的对象的操作集合。而这两者的取舍则是有设计人员决定的。 一句话总结权限的含义:what(若干元素)进行how(若干操作) 2.权限的划分原则 我们了解了权限的具体含义之后,接下来就是用的问题,我们该如何去使用权限,如何将系统中的操作元素进行一个组合,这个我借鉴网上的一篇文章来解释。划分原则可以按照“最小特权原则”和“数据抽象原则”。 最小特权原则 1、我先举一个反例,我把系统中所有的元素和操作都组合成一个权限。一个用户拥有这个权限就相当拥有了系统所有的功能,实际上这肯定是不行的,用户在一套系统中一定有他不允许操作的内容,哪怕是超级管理员也可能会有不能操作的元素,那么最大化权限则是行不通,因为不符合常理。 2、据此,我们就把权限再进行一个拆分,按照业务模块进行拆分,但是这实际上也是不行的。就比如系统中的财务模块,假定模块中含有报销页面和申报页面,如果按照模块进行拆分,那么肯定有用户同时包含了两个互斥功能。 3、根据1和2,我们需要按照最小化进行权限划分。但是这个也是值得商榷的,因为不同系统,最小的权限划分对于提供的功能来说,划分的角度也是不同的。 数据抽象原则 1、“最小特权划分”从某个程度上来说决定了控制的对象 ,而数据抽象原则是是决定了操作。 2、数据抽象从字面的意思来看,其实很难理解到底是什么意思。通常我们口头上说最多的是CRUD增删查改,这实际上就是数据抽象的一种,我们可以理解成元素操作许可权的意思。 3、但是CRUD并不是数据抽象的全部,增删查改用于单实体,基本是没问题的,但是在构建关系上,其实是不够用的,例如任免某个经理管辖某个部门,从业务表面而言它修改了经理的管辖范围。但是从代码底层构建上来说,它属于在经理和部门间新增了一道关系,所以根据需求我们需要再额外的增加一类许可权“任免许可”,这一类型的扩展则需要根据系统实际的业务情况进行划分。 “最小特权”和“数据抽象”分别决定了权限中控制的对象和操作,但是这里面还差了一个角度,则是现阶段非常普遍的前后端分离的权限划分的问题。 服务端的权限 1、前后端分离下的服务端,本质而言只是提供接口的或者rpc服务等其他资源服务的服务提供方。 2、服务端能提供的权限的鉴权机制的对象:接口服务(api或者其他形式的服务)不包含前端的页面或页面中的功能点。 3、前端或移动端的页面元素的控制和鉴权实质上不由服务端控制。 4、服务端可以单独的控制服务的权限。 5服务端的服务对象是前端、移动端、第三方客户端,提供的服务是接口服务。 在前后端已经分离的情况下,服务端对于前端而言只是接口的提供者,但无权干涉前端页面的展示,服务端对于前端而言,能提供的是仅鉴权服务的接口而已,但是页面的构成,页面的栏目菜单或页面内的功能点的构成均由前端单独完成的。 前端或移动端的权限 1、前端的鉴权包含页面的可访问,和页面上的某项功能按钮是否可以操作。 2、前端和移动端的服务对象是用户,提供的服务是可视化的页面。 前后端的服务对象的责任划分清晰之后,我们就不会混杂权限的归属的问题,在过去前后端没分离的情况下,页面本身就是服务端的一体,就没有这方面的问题。虽然分清楚了各端本质提供的服务的情况,但是前后端分离的权限划分中仍有新的问题。 1、因为服务端和前端的鉴权对象不一致,服务端只能鉴权到api接口,那么是否将api接口和前端的页面乃至页面功能点进行数据库表与表层面的绑定关系。 2、如果进行了进行了表与表之间的绑定关系,那么整个权限系统的维护量,是否能在能承受范围之内。 3、如果不进行表与表之间的绑定关系,前端页面在操作功能的时候,服务端如何鉴权页面调用的api接口是否在用户可操作的权限之内? 其实上面的问题则需要一个取舍,要么增加运维成本严格控制前端调用api接口的关系,偏重服务端的接口服务鉴权。要么是给api接口和前端页面及功能点再提供一个通性的逻辑判断处理,如:页面及调用的功能点属于某个业务模块的操作许可,而页面触发的接口也刚好是这个业务模块的操作许可,那么鉴权通过,否则鉴权失败。这种就是属于侧重前端对于用户的控制,弱化了接口级的控制。 3.角色与权限的关系 通过1,2的描述,基本确定了权限的定义和划分一个权限的通用法则。用户在系统中最终是通过权限来使用各种功能点,是否有必要在用户和权限中间再额外的附加一个关系。在我们现在的权限设计中,是增加了这样一层关系的,就是角色。 1、减少操作层面的重复性。角色其实就是一组权限的集合,是权限集合的更高级抽象,为了便于运维和实际管理,通过角色的赋予,替代了权限赋予用户的繁琐性,在一套系统中,普遍情况都是权限的数量多于角色的数量。 2、权限是控制对象和操作集合,它本身不存在任何状态,但是在赋予在用户身上则拥有了状态,比如权限A中允许用户访问页面A,权限B允许用户访问页面B,权限D运行用户访问B页面,但是不允许访问A页面。那么这层关系的维护在角色层面的话,会更加清晰,也就是说本身角色具有权限集合组装的策略问题,对于互斥的权限有不同的方案处理。(权限中没有某个操作和权限中禁止某个操作,是两个不同的角度,不能混为一谈) 3、因为权限的可能存在互斥性,在实际业务中也会引发角色的互斥性,举一个现实中的案例来解释互斥性:张三是软件部的负责人但因为工作的特殊性也同样隶属于业务部的普通员工,我们设定负责人是可以要求人事部门给本部门进行招聘的,在实际的情况中,张三能给软件部招聘新员工,但是不能给业务部招聘员工。我们把这个案例运用在系统中,张三则是拥有负责人和普通员工两个角色,但是招聘的功能如果不加以控制,则会发生张三给业务部招人的结果。于是为了解决角色的这类问题,引入了职责划分的方案。 4、职责划分分为:静态、动态。所谓静态职责划分则是在角色创建之初就已经确定了角色的职责内容。动态职责划分是系统运行过程中对用户已有的角色进行控制,例如:某些角色不能共存在用户身上(互斥)、角色或角色的分配数量限定(控制用量)、角色与角色同时只能激活一个进行使用(时刻唯一)。 引入角色的概念后,实际上这已经是一个比较完整的RBAC的权限设计的模型了。 4.数据表的设计思路 根据3的结论,实质上已经有了一个基础的表设计的雏形。在这里就有一些值得注意的点。 (1)问:权限表是否有必要存在? (1)答:这个要结合系统的实际使用场景进行考虑,如果系统中的权限的对象很单一,比如只有页面,或者只有api接口的话,其实权限表可有可无。增加权限表反而会导致初始化项目权限的工作量增加。但是若系统中的权限对象是多个,那么权限表的存在就有了更深层次的意义。在权限对象是多个的情况,权限表的存在就是为了更好更抽象的组合“最小特权”及“责任划分”的操作对象。同时,一旦系统中的操作对象增加了,只需要给权限表增加一个对象表和关系表就可以了。这样易于扩展。 (2)问:api接口和页面实际上是没有关系的,但是在鉴权活动是有关系的,页面若和api没有一点绑定联系的话,服务端接口调用的时候则要么拦截掉所有指定的接口(页面和api接口没绑定的话,则页面的接口调用都不能成功),服务端接口完全不拦截接口,也会不安全,但是api接口和页面功能在表结构层面的绑定会产生运维的大量工作成本,如何更好的设计。 (2)答:在权限如何划分中已经提过了这一点,在表结构中,我们可以增加一张业务模块表和操作表(也可以在数据字典表中增加这两类数据),我们可以在页面和功能点钟 绑定业务模块和操作表关系,在api接口的代码层面去绑定业务模块和操作,在逻辑上绑定关系,解耦表结构之间的关系,那么可以在一定程度上解决这一点,这样做只会出现一种问题,那就是用户访问页面的时候可调用的api接口会比实际可调用的接口数要多,但是前端权限管理会隐藏功能点,这样就在可视化的程度上解决了这个问题。 5.安全框架 由于我们是基于RBAC的权限设计,现行java框架下最常见的就是shiro和Spring Security 。这两个就是仁者见仁智者见智了,我两者都实用过。仅建议使用shiro的话,可以更好的理解RBAC的设计思路,Spring Security 也是个不错的框架,但是它涉及到的概念太多,并不利于初学者去了解最基本的权限设计。我在这只在学习的角度上去比较这两个框架,并没有再其他领域去做比较,也不去比较。 文章来源:https://my.oschina.net/cloudcross/blog/1920706 阅读参考:https://www.roncoo.com/course/list.html?courseName=%E6%9D%83%E9%99%90
redis架构演变与redis-cluster群集读写方案 导言 redis-cluster是近年来redis架构不断改进中的相对较好的redis高可用方案。本文涉及到近年来redis多实例架构的演变过程,包括普通主从架构(Master、slave可进行写读分离)、哨兵模式下的主从架构、redis-cluster高可用架构(redis官方默认cluster下不进行读写分离)的简介。同时还介绍使用Java的两大redis客户端:Jedis与Lettuce用于读写redis-cluster的数据的一般方法。再通过官方文档以及互联网的相关技术文档,给出redis-cluster架构下的读写能力的优化方案,包括官方的推荐的扩展redis-cluster下的Master数量以及非官方默认的redis-cluster的读写分离方案,案例中使用Lettuce的特定方法进行redis-cluster架构下的数据读写分离。 近年来redis多实例用架构的演变过程 redis是基于内存的高性能key-value数据库,若要让redis的数据更稳定安全,需要引入多实例以及相关的高可用架构。而近年来redis的高可用架构亦不断改进,先后出现了本地持久化、主从备份、哨兵模式、redis-cluster群集高可用架构等等方案。 1、redis普通主从模式 通过持久化功能,Redis保证了即使在服务器重启的情况下也不会损失(或少量损失)数据,因为持久化会把内存中数据保存到硬盘上,重启会从硬盘上加载数据。 。但是由于数据是存储在一台服务器上的,如果这台服务器出现硬盘故障等问题,也会导致数据丢失。为了避免单点故障,通常的做法是将数据库复制多个副本以部署在不同的服务器上,这样即使有一台服务器出现故障,其他服务器依然可以继续提供服务。为此, Redis 提供了复制(replication)功能,可以实现当一台数据库中的数据更新后,自动将更新的数据同步到其他数据库上。 在复制的概念中,数据库分为两类,一类是主数据库(master),另一类是从数据库(slave)。主数据库可以进行读写操作,当写操作导致数据变化时会自动将数据同步给从数据库。而从数据库一般是只读的,并接受主数据库同步过来的数据。一个主数据库可以拥有多个从数据库,而一个从数据库只能拥有一个主数据库。主从模式的配置,一般只需要再作为slave的redis节点的conf文件上加入“slaveof masterip masterport”, 或者作为slave的redis节点启动时使用如下参考命令: redis-server --port 6380 --slaveof masterIp masterPort redis的普通主从模式,能较好地避免单独故障问题,以及提出了读写分离,降低了Master节点的压力。互联网上大多数的对redis读写分离的教程,都是基于这一模式或架构下进行的。但实际上这一架构并非是目前最好的redis高可用架构。 2、redis哨兵模式高可用架构 当主数据库遇到异常中断服务后,开发者可以通过手动的方式选择一个从数据库来升格为主数据库,以使得系统能够继续提供服务。然而整个过程相对麻烦且需要人工介入,难以实现自动化。 为此,Redis 2.8开始提供了哨兵工具来实现自动化的系统监控和故障恢复功能。 哨兵的作用就是监控redis主、从数据库是否正常运行,主出现故障自动将从数据库转换为主数据库。 顾名思义,哨兵的作用就是监控Redis系统的运行状况。它的功能包括以下两个。 (1)监控主数据库和从数据库是否正常运行。(2)主数据库出现故障时自动将从数据库转换为主数据库。可以用info replication查看主从情况 例子: 1主2从 1哨兵,可以用命令起也可以用配置文件里 可以使用双哨兵,更安全,参考命令如下: redis-server --port 6379 redis-server --port 6380 --slaveof 192.168.0.167 6379 redis-server --port 6381 --slaveof 192.168.0.167 6379 redis-sentinel sentinel.conf 其中,哨兵配置文件sentinel.conf参考如下: sentinel monitor mymaster 192.168.0.167 6379 1 其中mymaster表示要监控的主数据库的名字。配置哨兵监控一个系统时,只需要配置其监控主数据库即可,哨兵会自动发现所有复制该主数据库的从数据库。Master与slave的切换过程:(1)slave leader升级为master(2)其他slave修改为新master的slave(3)客户端修改连接(4)老的master如果重启成功,变为新master的slave 3、redis-cluster群集高可用架构 即使使用哨兵,redis每个实例也是全量存储,每个redis存储的内容都是完整的数据,浪费内存且有木桶效应。为了最大化利用内存,可以采用cluster群集,就是分布式存储。即每台redis存储不同的内容。 采用redis-cluster架构正是满足这种分布式存储要求的集群的一种体现。redis-cluster架构中,被设计成共有16384个hash slot。每个master分得一部分slot,其算法为:hash_slot = crc16(key) mod 16384 ,这就找到对应slot。采用hash slot的算法,实际上是解决了redis-cluster架构下,有多个master节点的时候,数据如何分布到这些节点上去。key是可用key,如果有{}则取{}内的作为可用key,否则整个可以是可用key。群集至少需要3主3从,且每个实例使用不同的配置文件。在redis-cluster架构中,redis-master节点一般用于接收读写,而redis-slave节点则一般只用于备份,其与对应的master拥有相同的slot集合,若某个redis-master意外失效,则再将其对应的slave进行升级为临时redis-master。在redis的官方文档中,对redis-cluster架构上,有这样的说明:在cluster架构下,默认的,一般redis-master用于接收读写,而redis-slave则用于备份,当有请求是在向slave发起时,会直接重定向到对应key所在的master来处理。但如果不介意读取的是redis-cluster中有可能过期的数据并且对写请求不感兴趣时,则亦可通过readonly命令,将slave设置成可读,然后通过slave获取相关的key,达到读写分离。具体可以参阅redis官方文档(https://redis.io/commands/readonly)等相关内容: Enables read queries for a connection to a Redis Cluster slave node. Normally slave nodes will redirect clients to the authoritative master for the hash slot involved in a given command, however clients can use slaves in order to scale reads using the READONLY command. READONLY tells a Redis Cluster slave node that the client is willing to read possibly stale data and is not interested in running write queries. When the connection is in readonly mode, the cluster will send a redirection to the client only if the operation involves keys not served by the slave's master node. This may happen because: The client sent a command about hash slots never served by the master of this slave. The cluster was reconfigured (for example resharded) and the slave is no longer able to serve commands for a given hash slot. 例如,我们假设已经建立了一个三主三从的redis-cluster架构,其中A、B、C节点都是redis-master节点,A1、B1、C1节点都是对应的redis-slave节点。在我们只有master节点A,B,C的情况下,对应redis-cluster如果节点B失败,则群集无法继续,因为我们没有办法再在节点B的所具有的约三分之一的hash slot集合范围内提供相对应的slot。然而,如果我们为每个主服务器节点添加一个从服务器节点,以便最终集群由作为主服务器节点的A,B,C以及作为从服务器节点的A1,B1,C1组成,那么如果节点B发生故障,系统能够继续运行。节点B1复制B,并且B失效时,则redis-cluster将促使B的从节点B1作为新的主服务器节点并且将继续正确地操作。但请注意,如果节点B和B1在同一时间发生故障,则Redis群集无法继续运行。 Redis群集配置参数:在继续之前,让我们介绍一下Redis Cluster在redis.conf文件中引入的配置参数。有些命令的意思是显而易见的,有些命令在你阅读下面的解释后才会更加清晰。 (1)cluster-enabled :如果想在特定的Redis实例中启用Redis群集支持就设置为yes。 否则,实例通常作为独立实例启动。(2)cluster-config-file :请注意,尽管有此选项的名称,但这不是用户可编辑的配置文件,而是Redis群集节点每次发生更改时自动保留群集配置(基本上为状态)的文件。(3)cluster-node-timeout :Redis群集节点可以不可用的最长时间,而不会将其视为失败。 如果主节点超过指定的时间不可达,它将由其从属设备进行故障切换。(4)cluster-slave-validity-factor :如果设置为0,无论主设备和从设备之间的链路保持断开连接的时间长短,从设备都将尝试故障切换主设备。 如果该值为正值,则计算最大断开时间作为节点超时值乘以此选项提供的系数,如果该节点是从节点,则在主链路断开连接的时间超过指定的超时值时,它不会尝试启动故障切换。(5)cluster-migration-barrier :主设备将保持连接的最小从设备数量,以便另一个从设备迁移到不受任何从设备覆盖的主设备。有关更多信息,请参阅本教程中有关副本迁移的相应部分。(6)cluster-require-full-coverage :如果将其设置为yes,则默认情况下,如果key的空间的某个百分比未被任何节点覆盖,则集群停止接受写入。 如果该选项设置为no,则即使只处理关于keys子集的请求,群集仍将提供查询。 以下是最小的Redis集群配置文件: port 7000 cluster-enabled yes cluster-config-file nodes.conf cluster-node-timeout 5000 appendonly yes 注意:(1)redis-cluster最小配置为三主三从,当1个主故障,大家会给对应的从投票,把从立为主,若没有从数据库可以恢复则redis群集就down了。 (2)在这个redis cluster中,如果你要在slave读取数据,那么需要带上readonly指令。redis cluster的核心的理念,主要是用slave做高可用的,每个master挂一两个slave,主要是做数据的热备,当master故障时的作为主备切换,实现高可用的。redis cluster默认是不支持slave节点读或者写的,跟我们手动基于replication搭建的主从架构不一样的。slave node要设置readonly,然后再get,这个时候才能在slave node进行读取。对于redis -cluster主从架构,若要进行读写分离,官方其实是不建议的,但也能做,只是会复杂一些。具体见下面的章节。 (3)redis-cluster的架构下,实际上本身master就是可以任意扩展的,你如果要支撑更大的读吞吐量,或者写吞吐量,或者数据量,都可以直接对master进行横向扩展就可以了。也扩容master,跟之前扩容slave进行读写分离,效果是一样的或者说更好。 (4)可以使用自带客户端连接:使用redis-cli -c -p cluster中任意一个端口,进行数据获取测试。 Java中对redis-cluster数据的一般读取方法简介 使用Jedis读写redis-cluster的数据 由于Jedis类一般只能对一台redis-master进行数据操作,所以面对redis-cluster多台master与slave的群集,Jedis类就不能满足了。这个时候我们需要引用另外一个操作类:JedisCluster类。 例如我们有6台机器组成的redis-cluster:172.20.52.85:7000、 172.20.52.85:7001、172.20.52.85:7002、172.20.52.85:7003、172.20.52.85:7004、172.20.52.85:7005其中master机器对应端口:7000、7004、7005slave对应端口:7001、7002、7003 使用JedisCluster对redis-cluster进行数据操作的参考代码如下: // 添加nodes服务节点到Set集合 Set<HostAndPort> hostAndPortsSet = new HashSet<HostAndPort>(); // 添加节点 hostAndPortsSet.add(new HostAndPort("172.20.52.85", 7000)); hostAndPortsSet.add(new HostAndPort("172.20.52.85", 7001)); hostAndPortsSet.add(new HostAndPort("172.20.52.85", 7002)); hostAndPortsSet.add(new HostAndPort("172.20.52.85", 7003)); hostAndPortsSet.add(new HostAndPort("172.20.52.85", 7004)); hostAndPortsSet.add(new HostAndPort("172.20.52.85", 7005)); // Jedis连接池配置 JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); jedisPoolConfig.setMaxIdle(100); jedisPoolConfig.setMaxTotal(500); jedisPoolConfig.setMinIdle(0); jedisPoolConfig.setMaxWaitMillis(2000); // 设置2秒 jedisPoolConfig.setTestOnBorrow(true); JedisCluster jedisCluster = new JedisCluster(hostAndPortsSet ,jedisPoolConfig); String result = jedisCluster.get("event:10"); System.out.println(result); 运行结果截图如下图所示:第一节中我们已经介绍了redis-cluster架构下master提供读写功能,而slave一般只作为对应master机器的数据备份不提供读写。如果我们只在hostAndPortsSet中只配置slave,而不配置master,实际上还是可以读到数据,但其内部操作实际是通过slave重定向到相关的master主机上,然后再将结果获取和输出。 上面是普通项目使用JedisCluster的简单过程,若在spring boot项目中,可以定义JedisConfig类,使用@Configuration、@Value、@Bean等一些列注解完成JedisCluster的配置,然后再注入该JedisCluster到相关service逻辑中引用,这里介绍略。 使用Lettuce读写redis-cluster数据 Lettuce 和 Jedis 的定位都是Redis的client。Jedis在实现上是直接连接的redis server,如果在多线程环境下是非线程安全的,这个时候只有使用连接池,为每个Jedis实例增加物理连接,每个线程都去拿自己的 Jedis 实例,当连接数量增多时,物理连接成本就较高了。 Lettuce的连接是基于Netty的,连接实例(StatefulRedisConnection)可以在多个线程间并发访问,应为StatefulRedisConnection是线程安全的,所以一个连接实例(StatefulRedisConnection)就可以满足多线程环境下的并发访问,当然这个也是可伸缩的设计,一个连接实例不够的情况也可以按需增加连接实例。 其中spring boot 2.X版本中,依赖的spring-session-data-redis已经默认替换成Lettuce了。同样,例如我们有6台机器组成的redis-cluster:172.20.52.85:7000、 172.20.52.85:7001、172.20.52.85:7002、172.20.52.85:7003、172.20.52.85:7004、172.20.52.85:7005其中master机器对应端口:7000、7004、7005slave对应端口:7001、7002、7003 在spring boot 2.X版本中使用Lettuce操作redis-cluster数据的方法参考如下:(1)pom文件参考如下:parent中指出spring boot的版本,要求2.X以上: <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.4.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> 依赖中需要加入spring-boot-starter-data-redis,参考如下: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> (2)springboot的配置文件要包含如下内容: spring.redis.database=0 spring.redis.lettuce.pool.max-idle=10 spring.redis.lettuce.pool.max-wait=500 spring.redis.cluster.timeout=1000 spring.redis.cluster.max-redirects=3 spring.redis.cluster.nodes=172.20.52.85:7000,172.20.52.85:7001,172.20.52.85:7002,172.20.52.85:7003,172.20.52.85:7004,172.20.52.85:7005 (3)新建RedisConfiguration类,参考代码如下: @Configuration public class RedisConfiguration { [@Resource](https://my.oschina.net/u/929718) private LettuceConnectionFactory myLettuceConnectionFactory; @Bean public RedisTemplate<String, Serializable> redisTemplate() { RedisTemplate<String, Serializable> template = new RedisTemplate<>(); template.setKeySerializer(new StringRedisSerializer()); //template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); template.setValueSerializer(new StringRedisSerializer()); template.setConnectionFactory(myLettuceConnectionFactory); return template; } } (4)新建RedisFactoryConfig类,参考代码如下: @Configuration public class RedisFactoryConfig { @Autowired private Environment environment; @Bean public RedisConnectionFactory myLettuceConnectionFactory() { Map<String, Object> source = new HashMap<String, Object>(); source.put("spring.redis.cluster.nodes", environment.getProperty("spring.redis.cluster.nodes")); source.put("spring.redis.cluster.timeout", environment.getProperty("spring.redis.cluster.timeout")); source.put("spring.redis.cluster.max-redirects", environment.getProperty("spring.redis.cluster.max-redirects")); RedisClusterConfiguration redisClusterConfiguration; redisClusterConfiguration = new RedisClusterConfiguration(new MapPropertySource("RedisClusterConfiguration", source)); return new LettuceConnectionFactory(redisClusterConfiguration); } } (5)在业务类service中注入Lettuce相关的RedisTemplate,进行相关操作。以下是我化简到了springbootstarter中进行,参考代码如下: @SpringBootApplication public class NewRedisClientApplication { public static void main(String[] args) { ApplicationContext context = SpringApplication.run(NewRedisClientApplication.class, args); RedisTemplate redisTemplate = (RedisTemplate)context.getBean("redisTemplate"); String rtnValue = (String)redisTemplate.opsForValue().get("event:10"); System.out.println(rtnValue); } } 运行结果的截图如下:以上的介绍,是采用Jedis以及Lettuce对redis-cluster数据的简单读取。Jedis也好,Lettuce也好,其对于redis-cluster架构下的数据的读取,都是默认是按照redis官方对redis-cluster的设计,自动进行重定向到master节点中进行的,哪怕是我们在配置中列出了所有的master节点和slave节点。查阅了Jedis以及Lettuce的github上的源码,默认不支持redis-cluster下的读写分离,可以看出Jedis若要支持redis-cluster架构下的读写分离,需要自己改写和构建多一些包装类,定义好Master和slave节点的逻辑;而Lettuce的源码中,实际上预留了方法(setReadForm(ReadFrom.SLAVE))进行redis-cluster架构下的读写分离,相对来说修改会简单一些,具体可以参考后面的章节。 redis-cluster架构下的读写能力的优化方案 在上面的一些章节中,已经有讲到redis近年来的高可用架构的演变,以及在redis-cluster架构下,官方对redis-master、redis-slave的其实有使用上的建议,即redis-master节点一般用于接收读写,而redis-slave节点则一般只用于备份,其与对应的master拥有相同的slot集合,若某个redis-master意外失效,则再将其对应的slave进行升级为临时redis-master。但如果不介意读取的是redis-cluster中有可能过期的数据并且对写请求不感兴趣时,则亦可通过readonly命令,将slave设置成可读,然后通过slave获取相关的key,达到读写分离。具体可以参阅redis官方文档(https://redis.io/commands/readonly),以下是reids在线文档中,对slave的readonly说明内容:实际上本身master就是可以任意扩展的,所以如果要支撑更大的读吞吐量,或者写吞吐量,或者数据量,都可以直接对master进行横向水平扩展就可以了。也就是说,扩容master,跟之前扩容slave并进行读写分离,效果是一样的或者说更好。 所以下面我们将按照redis-cluster架构下分别进行水平扩展Master,以及在redis-cluster架构下对master、slave进行读写分离两套方案进行讲解。 (一)水平扩展Master实例来进行redis-cluster性能的提升 redis官方在线文档以及一些互联网的参考资料都表明,在redis-cluster架构下,实际上不建议做物理的读写分离。那么如果我们真的不做读写分离的话,能否通过简单的方法进行redis-cluster下的性能的提升?我们可以通过master的水平扩展,来横向扩展读写吞吐量,并且能支撑更多的海量数据。 对master进行水平扩展有两种方法,一种是单机上面进行master实例的增加(建议每新增一个master,也新增一个对应的slave),另一种是新增机器部署新的master实例(同样建议每新增一个master,也新增一个对应的slave)。当然,我们也可以进行这两种方法的有效结合。 (1)单机上通过多线程建立新redis-master实例,即逻辑上的水平扩展:一般的,对于redis单机,单线程的读吞吐是4w/s~5W/s,写吞吐为2w/s。单机合理开启redis多线程情况下(一般线程数为CPU核数的倍数),总吞吐量会有所上升,但每个线程的平均处理能力会有所下降。例如一个2核CPU,开启2线程的时候,总读吞吐能上升是6W/s~7W/s,即每个线程平均约3W/s再多一些。但过多的redis线程反而会限制了总吞吐量。 (2)扩展更多的机器,部署新redis-master实例,即物理上的水平扩展:例如,我们可以再原来只有3台master的基础上,连入新机器继续新实例的部署,最终水平扩展为6台master(建议每新增一个master,也新增一个对应的slave)。例如之前每台master的处理能力假设是读吞吐5W/s,写吞吐2W/s,扩展前一共的处理能力是:15W/s读,6W/s写。如果我们水平扩展到6台master,读吞吐可以达到总量30W/s,写可以达到12w/s,性能能够成倍增加。 (3)若原本每台部署redis-master实例的机器都性能良好,则可以通过上述两者的结合,进行一个更优的组合。 使用该方案进行redis-cluster性能的提升的优点有:(1)符合redis官方要求和数据的准确性。(2)真正达到更大吞吐量的性能扩展。(3)无需代码的大量更改,只需在配置文件中重新配置新的节点信息。 当然缺点也是有的:(1)需要新增机器,提升性能,即成本会增加。(2)若不新增机器,则需要原来的实例所运行的机器性能较好,能进行以多线程的方式部署新实例。但随着线程的增多,而机器的能力不足以支撑的时候,实际上总体能力会提升不太明显。(3)redis-cluster进行新的水平扩容后,需要对master进行新的hash slot重新分配,这相当于需要重新加载所有的key,并按算法平均分配到各个Master的slot当中。 (二)引入Lettuce以及修改相关方法,达到对redis-cluster的读写分离 通过上面的一些章节,我们已经可以了解到Lettuce客户端读取redis的一些操作,使用Lettuce能体现出了简单,安全,高效。实际上,查阅了Lettuce对redis的读写,许多地方都进行了redis的读写分离。但这些都是基于上述redis架构中最普通的主从分离架构下的读写分离,而对于redis-cluster架构下,Lettuce可能是遵循了redis官方的意见,在该架构下,Lettuce在源码中直接设置了只由master上进行读写(具体参见gitHub的Lettuce项目):那么如果真的需要让Lettuce改为能够读取redis-cluster的slave,进行读写分离,是否可行?实际上还是可以的。这就需要我们自己在项目中进行二次加工,即不使用spring-boot中的默认Lettuce初始化方法,而是自己去写一个属于自己的Lettuce的新RedisClusterClient的连接,并且对该RedisClusterClient的连接进行一个比较重要的设置,那就是由connection.setReadFrom(ReadFrom.MASTER)改为connection.setReadFrom(ReadFrom.SLAVE)。 下面我们开始对之前章节中的Lettuce读取redis-cluster数据的例子,进行改写,让Lettuce能够支持该架构下的读写分离: spring boot 2.X版本中,依赖的spring-session-data-redis已经默认替换成Lettuce了。同样,例如我们有6台机器组成的redis-cluster:172.20.52.85:7000、 172.20.52.85:7001、172.20.52.85:7002、172.20.52.85:7003、172.20.52.85:7004、172.20.52.85:7005其中master机器对应端口:7000、7004、7005slave对应端口:7001、7002、7003 在spring boot 2.X版本中使用Lettuce操作redis-cluster数据的方法参考如下:(1)pom文件参考如下:parent中指出spring boot的版本,要求2.X以上: <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.4.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> 依赖中需要加入spring-boot-starter-data-redis,参考如下: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> (2)springboot的配置文件要包含如下内容: spring.redis.database=0 spring.redis.lettuce.pool.max-idle=10 spring.redis.lettuce.pool.max-wait=500 spring.redis.cluster.timeout=1000 spring.redis.cluster.max-redirects=3 spring.redis.cluster.nodes=172.20.52.85:7000,172.20.52.85:7001,172.20.52.85:7002,172.20.52.85:7003,172.20.52.85:7004,172.20.52.85:7005 (3)我们回到RedisConfiguration类中,删除或屏蔽之前的RedisTemplate方法,新增自定义的redisClusterConnection方法,并且设置好读写分离,参考代码如下: @Configuration public class RedisConfiguration { @Autowired private Environment environment; @Bean public StatefulRedisClusterConnection redisClusterConnection(){ String strRedisClusterNodes = environment.getProperty("spring.redis.cluster.nodes"); String[] listNodesInfos = strRedisClusterNodes.split(","); List<RedisURI> listRedisURIs = new ArrayList<RedisURI>(); for(String tmpNodeInfo : listNodesInfos){ String[] tmpInfo = tmpNodeInfo.split(":"); listRedisURIs.add(new RedisURI(tmpInfo[0],Integer.parseInt(tmpInfo[1]),Duration.ofDays(10))); } RedisClusterClient clusterClient = RedisClusterClient.create(listRedisURIs); StatefulRedisClusterConnection<String, String> connection = clusterClient.connect(); connection.setReadFrom(ReadFrom.SLAVE); return connection; } } 其中,这三行代码是能进行redis-cluster架构下读写分离的核心: RedisClusterClient clusterClient = RedisClusterClient.create(listRedisURIs); StatefulRedisClusterConnection<String, String> connection = clusterClient.connect(); connection.setReadFrom(ReadFrom.SLAVE); 在业务类service中注入Lettuce相关的redisClusterConnection,进行相关读写操作。以下是我直接化简到了springbootstarter中进行,参考代码如下: @SpringBootApplication public class NewRedisClientApplication { public static void main(String[] args) { ApplicationContext context = SpringApplication.run(NewRedisClientApplication.class, args); StatefulRedisClusterConnection<String, String> redisClusterConnection = (StatefulRedisClusterConnection)context.getBean("redisClusterConnection"); System.out.println(redisClusterConnection.sync().get("event:10")); } } 运行的结果如下图所示:可以看到,经过改写的redisClusterConnection的确能读取到redis-cluster的数据。但这一个数据我们还需要验证一下到底是不是通过slave读取到的,又或者还是通过slave重定向给master才获取到的?带着疑问,我们可以开通debug模式,在redisClusterConnection.sync().get("event:10")等类似的获取数据的代码行上面打上断点。通过代码的走查,我们可以看到,在ReadFromImpl类中,最终会select到key所在的slave节点,进行返回,并在该slave中进行数据的读取: ReadFromImpl显示: 另外我们通过connectFuture中的显示也验证了对于slave的readonly生效了:这样,就达到了通过Lettuce客户端对redis-cluster的读写分离了。 使用该方案进行redis-cluster性能的提升的优点有:(1)直接通过代码级更改,而不需要配置新的redis-cluster环境。(2)无需增加机器或升级硬件设备。 但同时,该方案也有缺点:(1)非官方对redis-cluster的推荐方案,因为在redis-cluster架构下,进行读写分离,有可能会读到过期的数据。(2)需对项目进行全面的替换,将Jedis客户端变为Lettuce客户端,对代码的改动较大,而且使用Lettuce时,使用的并非spring boot的自带集成Lettuce的redisTemplate配置方法,而是自己配置读写分离的 redisClusterConnetcion,日后遇到问题的时候,可能官方文档的支持率或支撑能力会比较低。(3)需修改redis-cluster的master、slave配置,在各个节点中都需要加入slave-read-only yes。(4)性能的提升没有水平扩展master主机和实例来得直接干脆。 总结 总体上来说,redis-cluster高可用架构方案是目前最好的redis架构方案,redis的官方对redis-cluster架构是建议redis-master用于接收读写,而redis-slave则用于备份(备用),默认不建议读写分离。但如果不介意读取的是redis-cluster中有可能过期的数据并且对写请求不感兴趣时,则亦可通过readonly命令,将slave设置成可读,然后通过slave获取相关的key,达到读写分离。Jedis、Lettuce都可以进行redis-cluster的读写操作,而且默认只针对Master进行读写,若要对redis-cluster架构下进行读写分离,则Jedis需要进行源码的较大改动,而Lettuce开放了setReadFrom()方法,可以进行二次封装成读写分离的客户端,相对简单,而且Lettuce比Jedis更安全。redis-cluster架构下可以直接通过水平扩展master来达到性能的提升。 文章来源:https://my.oschina.net/u/2600078/blog/1923696推荐阅读:https://www.roncoo.com/course/view/af7d501667fe4a19a60e9467e6d2b3d9
RocketMQ是一款分布式、队列模型的消息中间件,具有以下特点: 能够保证严格的消息顺序 提供丰富的消息拉取模式 高效的订阅者水平扩展能力 实时的消息订阅机制 亿级消息堆积能力 RocketMQ网络部署特 (1)NameServer是一个几乎无状态的节点,可集群部署,节点之间无任何信息同步 (2)Broker部署相对复杂,Broker氛围Master与Slave,一个Master可以对应多个Slaver,但是一个Slaver只能对应一个Master,Master与Slaver的对应关系通过指定相同的BrokerName,不同的BrokerId来定义,BrokerId为0表示Master,非0表示Slaver。Master可以部署多个。每个Broker与NameServer集群中的所有节点建立长连接,定时注册Topic信息到所有的NameServer (3)Producer与NameServer集群中的其中一个节点(随机选择)建立长连接,定期从NameServer取Topic路由信息,并向提供Topic服务的Master建立长连接,且定时向Master发送心跳。Produce完全无状态,可集群部署 (4)Consumer与NameServer集群中的其中一个节点(随机选择)建立长连接,定期从NameServer取Topic路由信息,并向提供Topic服务的Master、Slaver建立长连接,且定时向Master、Slaver发送心跳。Consumer即可从Master订阅消息,也可以从Slave订阅消息,订阅规则由Broker配置决定 RocketMQ储存特点 (1)零拷贝原理:Consumer消费消息过程,使用了零拷贝,零拷贝包括一下2中方式,RocketMQ使用第一种方式,因小块数据传输的要求效果比sendfile方式好 a )使用mmap+write方式 优点:即使频繁调用,使用小文件块传输,效率也很高 缺点:不能很好的利用DMA方式,会比sendfile多消耗CPU资源,内存安全性控制复杂,需要避免JVM Crash问题 b)使用sendfile方式 优点:可以利用DMA方式,消耗CPU资源少,大块文件传输效率高,无内存安全新问题 缺点:小块文件效率低于mmap方式,只能是BIO方式传输,不能使用NIO (2)数据存储结构 RocketMQ关键特性 1.单机支持1W以上的持久化队(1)所有数据单独储存到commit Log ,完全顺序写,随机读 (2)对最终用户展现的队列实际只储存消息在Commit Log 的位置信息,并且串行方式刷盘 这样做的好处: (1)队列轻量化,单个队列数据量非常少 (2)对磁盘的访问串行话,避免磁盘竞争,不会因为队列增加导致IOWait增高 每个方案都有优缺点,他的缺点是: (1)写虽然是顺序写,但是读却变成了随机读 (2)读一条消息,会先读Consume Queue,再读Commit Log,增加了开销 (3)要保证Commit Log 与 Consume Queue完全的一致,增加了编程的复杂度 以上缺点如何客服: (1)随机读,尽可能让读命中pagecache,减少IO操作,所以内存越大越好。如果系统中堆积的消息过多,读数据要访问硬盘会不会由于随机读导致系统性能急剧下降,答案是否定的。 a)访问pagecache时,即使只访问1K的消息,系统也会提前预读出更多的数据,在下次读时就可能命中pagecache b)随机访问Commit Log 磁盘数据,系统IO调度算法设置为NOOP方式,会在一定程度上将完全的随机读变成顺序跳跃方式,而顺序跳跃方式读较完全的随机读性能高5倍 (2)由于Consume Queue存储数量极少,而且顺序读,在pagecache的与读取情况下,Consume Queue的读性能与内存几乎一直,即使堆积情况下。所以可以认为Consume Queue完全不会阻碍读性能 (3)Commit Log中存储了所有的元信息,包含消息体,类似于MySQl、Oracle的redolog,所以只要有Commit Log存在, Consume Queue即使丢失数据,仍可以恢复出来 2.刷盘策略 rocketmq中的所有消息都是持久化的,先写入系统pagecache,然后刷盘,可以保证内存与磁盘都有一份数据,访问时,可以直接从内存读取 2.1异步刷盘 在有 RAID 卡, SAS 15000 转磁盘测试顺序写文件,速度可以达到 300M 每秒左右,而线上的网卡一般都为千兆网卡,写磁盘速度明显快于数据网络入口速度,那么是否可以做到写完 内存就向用户返回,由后台线程刷盘呢? (1). 由于磁盘速度大于网卡速度,那么刷盘的进度肯定可以跟上消息的写入速度。 (2). 万一由于此时系统压力过大,可能堆积消息,除了写入 IO,还有读取 IO,万一出现磁盘读取落后情况,会不会导致系统内存溢出,答案是否定的,原因如下: a) 写入消息到 PAGECACHE 时,如果内存不足,则尝试丢弃干净的 PAGE,腾出内存供新消息使用,策略是 LRU 方式。 b) 如果干净页不足,此时写入 PAGECACHE 会被阻塞,系统尝试刷盘部分数据,大约每次尝试 32 个 PAGE,来找出更多干净 PAGE。综上,内存溢出的情况不会出现 2.2同步刷盘: 同步刷盘与异步刷盘的唯一区别是异步刷盘写完 PAGECACHE 直接返回,而同步刷盘需要等待刷盘完成才返回,同步刷盘流程如下: (1)写入 PAGECACHE 后,线程等待,通知刷盘线程刷盘。 (2)刷盘线程刷盘后,唤醒前端等待线程,可能是一批线程。 (3)前端等待线程向用户返回成功。 3.消息查询 3.1按照MessageId查询消息MsgId总共16个字节,包含消息储存主机地址,消息Commit Log Offset。从MsgId中解析出Broker的地址和Commit Log 偏移地址,然后按照存储格式所在位置消息buffer解析成一个完整消息 3.2按照Message Key查询消息1.根据查询的key的hashcode%slotNum得到具体的槽位置 (slotNum是一个索引文件里面包含的最大槽目数目,例如图中所示slotNum=500W) 2.根据slotValue(slot对应位置的值)查找到索引项列表的最后一项(倒序排列,slotValue总是指向最新的一个索引项) 3.遍历索引项列表返回查询时间范围内的结果集(默认一次最大返回的32条记录) 4.Hash冲突,寻找key的slot位置时相当于执行了两次散列函数,一次key的hash,一次key的hash取值模,因此这里存在两次冲突的情况;第一种,key的hash值不同但模数相同,此时查询的时候会在比较第一次key的hash值(每个索引项保存了key的hash值),过滤掉hash值不想等的情况。第二种,hash值相等key不想等,出于性能的考虑冲突的检测放到客户端处理(key的原始值是存储在消息文件中的,避免对数据文件的解析),客户端比较一次消息体的key是否相同 5.存储,为了节省空间索引项中存储的时间是时间差值(存储时间——开始时间,开始时间存储在索引文件头中),整个索引文件是定长的,结构也是固定的 4.服务器消息过滤 RocketMQ的消息过滤方式有别于其他的消息中间件,是在订阅时,再做过滤,先来看下Consume Queue存储结构1.在Broker端进行Message Tag比较,先遍历Consume Queue,如果存储的Message Tag与订阅的Message Tag不符合,则跳过,继续比对下一个,符合则传输给Consumer。注意Message Tag是字符串形式,Consume Queue中存储的是其对应的hashcode,比对时也是比对hashcode 2.Consumer收到过滤消息后,同样也要执行在broker端的操作,但是比对的是真实的Message Tag字符串,而不是hashcode 为什么过滤要这么做? 1.Message Tag存储hashcode,是为了在Consume Queue定长方式存储,节约空间 2.过滤过程中不会访问Commit Log 数据,可以保证堆积情况下也能高效过滤 3.即使存在hash冲突,也可以在Consumer端进行修正,保证万无一失 5.单个JVM进程也能利用机器超大内存 1.Producer发送消息,消息从socket进入java 堆 2.Producer发送消息,消息从java堆进入pagecache,物理内存 3.Producer发送消息,由异步线程刷盘,消息从pagecache刷入磁盘 4.Consumer拉消息(正常消费),消息直接从pagecache(数据在物理内存)转入socket,到达Consumer,不经过java堆。这种消费场景最多,线上96G物理内存,按照1K消息算,可以物理缓存1亿条消息 5.Consumer拉消息(异常消费),消息直接从pagecache转入socket 6.Consumer拉消息(异常消费),由于socket访问了虚拟内存,产生缺页中断,此时会产生磁盘IO,从磁盘Load消息到pagecache,然后直接从socket发出去 7.同5 8.同6 6.消息堆积问题解决办法 1 消息的堆积容量、依赖磁盘大小 2 发消息的吞吐量大小受影响程度、无Slave情况,会受一定影响、有Slave情况,不受影响 3 正常消费的Consumer是否会受影响、无Slave情况,会受一定影响、有Slave情况,不受影响 4 访问堆积在磁盘的消息时,吞吐量有多大、与访问的并发有关,最终会降到5000左右 在有Slave情况下,Master一旦发现Consumer访问堆积在磁盘的数据时,回想Consumer下达一个重定向指令,令Consumer从Slave拉取数据,这样正常的发消息与正常的消费不会因为堆积受影响,因为系统将堆积场景与非堆积场景分割在了两个不同的节点处理。这里会产生一个问题,Slave会不会写性能下降,答案是否定的。因为Slave的消息写入只追求吞吐量,不追求实时性,只要整体的吞吐量高就行了,而Slave每次都是从Master拉取一批数据,如1M,这种批量顺序写入方式使堆积情况,整体吞吐量影响相对较小,只是写入RT会变长。 服务端安装部署 我是在虚拟机中的CentOS6.5中进行部署。 1.下载程序 2.tar -xvf alibaba-rocketmq-3.0.7.tar.gz 解压到适当的目录如/opt/目录 3.启动RocketMQ:进入rocketmq/bin 目录 执行4.启动Broker,设置对应的NameServer 编写客户端 可以查看sameple中的quickstart源码 1.Consumer 消息消费者2.Producer消息生产者3.首先运行Consumer程序,一直在运行状态接收服务器端推送过来的消息4.再次运行Producer程序,生成消息并发送到Broker,Producer的日志冲没了,但是可以看到Broker推送到Consumer的一条消息 Consumer最佳实践 1.消费过程要做到幂等(即消费端去重) RocketMQ无法做到消息重复,所以如果业务对消息重复非常敏感,务必要在业务层面去重,有以下一些方式: (1).将消息的唯一键,可以是MsgId,也可以是消息内容中的唯一标识字段,例如订单ID,消费之前判断是否在DB或Tair(全局KV存储)中存在,如果不存在则插入,并消费,否则跳过。(实践过程要考虑原子性问题,判断是否存在可以尝试插入,如果报主键冲突,则插入失败,直接跳过) msgid一定是全局唯一的标识符,但是可能会存在同样的消息有两个不同的msgid的情况(有多种原因),这种情况可能会使业务上重复,建议最好使用消息体中的唯一标识字段去重 (2).使业务层面的状态机去重 2.批量方式消费 如果业务流程支持批量方式消费,则可以很大程度上的提高吞吐量,可以通过设置Consumer的consumerMessageBatchMaxSize参数,默认是1,即一次消费一条参数 3.跳过非重要的消息 发生消息堆积时,如果消费速度一直跟不上发送速度,可以选择丢弃不重要的消息如以上代码所示,当某个队列的消息数堆积到 100000 条以上,则尝试丢弃部分或全部消息,这样就可以快速追上发送消息的速度 4.优化没条消息消费过程 举例如下,某条消息的消费过程如下 1、根据消息从 DB 查询数据 12、根据消息从 DB 查询数据23、复杂的业务计算4、向 DB 插入数据35、向 DB 插入数据 4 这条消息的消费过程与 DB 交互了 4 次,如果按照每次 5ms 计算,那么总共耗时 20ms,假设业务计算耗时 5ms,那么总过耗时 25ms,如果能把 4 次 DB 交互优化为 2 次,那么总耗时就可以优化到 15ms,也就是说总体性能提高了 40%。 对于 Mysql 等 DB,如果部署在磁盘,那么与 DB 进行交互,如果数据没有命中 cache,每次交互的 RT 会直线上升, 如果采用 SSD,则 RT 上升趋势要明显好于磁盘。 个别应用可能会遇到这种情况:在线下压测消费过程中,db 表现非常好,每次 RT 都很短,但是上线运行一段时间,RT 就会变长,消费吞吐量直线下降 主要原因是线下压测时间过短,线上运行一段时间后,cache 命中率下降,那么 RT 就会增加。建议在线下压测时,要测试足够长时间,尽可能模拟线上环境,压测过程中,数据的分布也很重要,数据不同,可能 cache 的命中率也会完全不同 Producer最佳实践 1.发送消息注意事项 (1) 一个应用尽可能用一个 Topic,消息子类型用 tags 来标识,tags 可以由应用自由设置。只有发送消息设置了tags,消费方在订阅消息时,才可以利用 tags 在 broker 做消息过滤。 (2)每个消息在业务层面的唯一标识码,要设置到 keys 字段,方便将来定位消息丢失问题。服务器会为每个消息创建索引(哈希索引),应用可以通过 topic,key 来查询这条消息内容,以及消息被谁消费。由于是哈希索引,请务必保证 key 尽可能唯一,这样可以避免潜在的哈希冲突。 (3)消息发送成功或者失败,要打印消息日志,务必要打印 sendresult 和 key 字段 (4)send 消息方法,只要不抛异常,就代表发送成功。但是发送成功会有多个状态,在 sendResult 里定义 SEND_OK:消息发送成功 FLUSH_DISK_TIMEOUT:消息发送成功,但是服务器刷盘超时,消息已经进入服务器队列,只有此时服务器宕机,消息才会丢失 FLUSH_SLAVE_TIMEOUT:消息发送成功,但是服务器同步到 Slave 时超时,消息已经进入服务器队列,只有此时服务器宕机,消息才会丢失 SLAVE_NOT_AVAILABLE:消息发送成功,但是此时 slave 不可用,消息已经进入服务器队列,只有此时服务器宕机,消息才会丢失。对于精确发送顺序消息的应用,由于顺序消息的局限性,可能会涉及到主备自动切换问题,所以如果sendresult 中的 status 字段不等于 SEND_OK,就应该尝试重试。对于其他应用,则没有必要这样 (5)对于消息不可丢失应用,务必要有消息重发机制 2.消息发送失败处理 Producer 的 send 方法本身支持内部重试,重试逻辑如下: (1) 至多重试 3 次 (2) 如果发送失败,则轮转到下一个 Broker (3) 这个方法的总耗时时间不超过 sendMsgTimeout 设置的值,默认 10s所以,如果本身向 broker 发送消息产生超时异常,就不会再做重试 如: 如果调用 send 同步方法发送失败,则尝试将消息存储到 db,由后台线程定时重试,保证消息一定到达 Broker。 上述 db 重试方式为什么没有集成到 MQ 客户端内部做,而是要求应用自己去完成,基于以下几点考虑: (1)MQ 的客户端设计为无状态模式,方便任意的水平扩展,且对机器资源的消耗仅仅是 cpu、内存、网络 (2)如果 MQ 客户端内部集成一个 KV 存储模块,那么数据只有同步落盘才能较可靠,而同步落盘本身性能开销较大,所以通常会采用异步落盘,又由于应用关闭过程不受 MQ 运维人员控制,可能经常会发生 kill -9 这样暴力方式关闭,造成数据没有及时落盘而丢失 (3)Producer 所在机器的可靠性较低,一般为虚拟机,不适合存储重要数据。 综上,建议重试过程交由应用来控制。 3.选择 oneway 形式发送 一个 RPC 调用,通常是这样一个过程 (1)客户端发送请求到服务器 (2)服务器处理该请求 (3)服务器向客户端返回应答 所以一个 RPC 的耗时时间是上述三个步骤的总和,而某些场景要求耗时非常短,但是对可靠性要求并不高,例如日志收集类应用,此类应用可以采用 oneway 形式调用,oneway 形式只发送请求不等待应答,而发送请求在客户端实现层面仅仅是一个 os 系统调用的开销,即将数据写入客户端的 socket 缓冲区,此过程耗时通常在微秒级。 RocketMQ不止可以直接推送消息,在消费端注册监听器进行监听,还可以由消费端决定自己去拉取数据刚开始的没有细看PullResult对象,以为拉取到的结果没有MessageExt对象还跑到群里面问别人,犯2了 特别要注意 静态变量offsetTable的作用,拉取的是按照从offset(理解为下标)位置开始拉取,拉取N条,offsetTable记录下次拉取的offset位置。 推荐阅读:JAVA-ACE-架构师系列- RocketMQ 文章来源:https://segmentfault.com/a/1190000015951993
案例介绍 在尝试了解分布式锁之前,大家可以想象一下,什么场景下会使用分布式锁?单机应用架构中,秒杀案例使用ReentrantLcok或者synchronized来达到秒杀商品互斥的目的。然而在分布式系统中,会存在多台机器并行去实现同一个功能。也就是说,在多进程中,如果还使用以上JDK提供的进程锁,来并发访问数据库资源就可能会出现商品超卖的情况。因此,需要我们来实现自己的分布式锁。 实现一个分布式锁应该具备的特性: 高可用、高性能的获取锁与释放锁 在分布式系统环境下,一个方法或者变量同一时间只能被一个线程操作 具备锁失效机制,网络中断或宕机无法释放锁时,锁必须被删除,防止死锁 具备阻塞锁特性,即没有获取到锁,则继续等待获取锁 具备非阻塞锁特性,即没有获取到锁,则直接返回获取锁失败 具备可重入特性,一个线程中可以多次获取同一把锁,比如一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法,而无需重新获得锁 在之前的秒杀案例中,我们曾介绍过关于分布式锁几种实现方式: 基于数据库实现分布式锁 基于 Redis 实现分布式锁 基于 Zookeeper 实现分布式锁 前两种对于分布式生产环境来说并不是特别推荐,高并发下数据库锁性能太差,Redis在锁时间限制和缓存一致性存在一定问题。这里我们重点介绍一下 Zookeeper 如何实现分布式锁。 实现原理 ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能存在唯一文件名。 数据模型 PERSISTENT 持久化节点,节点创建后,不会因为会话失效而消失 EPHEMERAL 临时节点, 客户端session超时此类节点就会被自动删除 EPHEMERAL_SEQUENTIAL 临时自动编号节点 PERSISTENT_SEQUENTIAL 顺序自动编号持久化节点,这种节点会根据当前已存在的节点数自动加 1 监视器(watcher) 当创建一个节点时,可以注册一个该节点的监视器,当节点状态发生改变时,watch被触发时,ZooKeeper将会向客户端发送且仅发送一条通知,因为watch只能被触发一次。 根据zookeeper的这些特性,我们来看看如何利用这些特性来实现分布式锁: 创建一个锁目录lock 线程A获取锁会在lock目录下,创建临时顺序节点 获取锁目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁 线程B创建临时节点并获取所有兄弟节点,判断自己不是最小节点,设置监听(watcher)比自己次小的节点(只关注比自己次小的节点是为了防止发生“羊群效应”) 线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是最小的节点,获得锁 代码分析 尽管ZooKeeper已经封装好复杂易出错的关键服务,将简单易用的接口和性能高效、功能稳定的系统提供给用户。但是如果让一个普通开发者去手撸一个分布式锁还是比较困难的,在秒杀案例中我们直接使用 Apache 开源的curator 开实现 Zookeeper 分布式锁。 这里我们使用以下版本,截止目前最新版4.0.1: <!-- zookeeper 分布式锁、注意zookeeper版本 这里对应的是3.4.6--> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> <version>2.10.0</version> </dependency> 首先,我们看下InterProcessLock接口中的几个方法: /** * 获取锁、阻塞等待、可重入 */ public void acquire() throws Exception; /** * 获取锁、阻塞等待、可重入、超时则获取失败 */ public boolean acquire(long time, TimeUnit unit) throws Exception; /** * 释放锁 */ public void release() throws Exception; /** * Returns true if the mutex is acquired by a thread in this JVM */ boolean isAcquiredInThisProcess(); 获取锁: //获取锁 public void acquire() throws Exception { if ( !internalLock(-1, null) ) { throw new IOException("Lost connection while trying to acquire lock: " + basePath); } } private boolean internalLock(long time, TimeUnit unit) throws Exception { /* 实现同一个线程可重入性,如果当前线程已经获得锁, 则增加锁数据中lockCount的数量(重入次数),直接返回成功 */ //获取当前线程 Thread currentThread = Thread.currentThread(); //获取当前线程重入锁相关数据 LockData lockData = threadData.get(currentThread); if ( lockData != null ) { //原子递增一个当前值,记录重入次数,后面锁释放会用到 lockData.lockCount.incrementAndGet(); return true; } //尝试连接zookeeper获取锁 String lockPath = internals.attemptLock(time, unit, getLockNodeBytes()); if ( lockPath != null ) { //创建可重入锁数据,用于记录当前线程重入次数 LockData newLockData = new LockData(currentThread, lockPath); threadData.put(currentThread, newLockData); return true; } //获取锁超时或者zk通信异常返回失败 return false; } Zookeeper获取锁实现: String attemptLock(long time, TimeUnit unit, byte[] lockNodeBytes) throws Exception { //获取当前时间戳 final long startMillis = System.currentTimeMillis(); //如果unit不为空(非阻塞锁),把当前传入time转为毫秒 final Long millisToWait = (unit != null) ? unit.toMillis(time) : null; //子节点标识 final byte[] localLockNodeBytes = (revocable.get() != null) ? new byte[0] : lockNodeBytes; //尝试次数 int retryCount = 0; String ourPath = null; boolean hasTheLock = false; boolean isDone = false; //自旋锁,循环获取锁 while ( !isDone ) { isDone = true; try { //在锁节点下创建临时且有序的子节点,例如:_c_008c1b07-d577-4e5f-8699-8f0f98a013b4-lock-000000001 ourPath = driver.createsTheLock(client, path, localLockNodeBytes); //如果当前子节点序号最小,获得锁则直接返回,否则阻塞等待前一个子节点删除通知(release释放锁) hasTheLock = internalLockLoop(startMillis, millisToWait, ourPath); } catch ( KeeperException.NoNodeException e ) { //异常处理,如果找不到节点,这可能发生在session过期等时,因此,如果重试允许,只需重试一次即可 if ( client.getZookeeperClient().getRetryPolicy().allowRetry(retryCount++, System.currentTimeMillis() - startMillis, RetryLoop.getDefaultRetrySleeper()) ) { isDone = false; } else { throw e; } } } //如果获取锁则返回当前锁子节点路径 if ( hasTheLock ) { return ourPath; } return null; } private boolean internalLockLoop(long startMillis, Long millisToWait, String ourPath) throws Exception { boolean haveTheLock = false; boolean doDelete = false; try { if ( revocable.get() != null ) { client.getData().usingWatcher(revocableWatcher).forPath(ourPath); } //自旋获取锁 while ( (client.getState() == CuratorFrameworkState.STARTED) && !haveTheLock ) { //获取所有子节点集合 List<String> children = getSortedChildren(); //判断当前子节点是否为最小子节点 String sequenceNodeName = ourPath.substring(basePath.length() + 1); // +1 to include the slash PredicateResults predicateResults = driver.getsTheLock(client, children, sequenceNodeName, maxLeases); //如果是最小节点则获取锁 if ( predicateResults.getsTheLock() ) { haveTheLock = true; } else { //获取前一个节点,用于监听 String previousSequencePath = basePath + "/" + predicateResults.getPathToWatch(); synchronized(this) { try { //这里使用getData()接口而不是checkExists()是因为,如果前一个子节点已经被删除了那么会抛出异常而且不会设置事件监听器,而checkExists虽然也可以获取到节点是否存在的信息但是同时设置了监听器,这个监听器其实永远不会触发,对于Zookeeper来说属于资源泄露 client.getData().usingWatcher(watcher).forPath(previousSequencePath); if ( millisToWait != null ) { millisToWait -= (System.currentTimeMillis() - startMillis); startMillis = System.currentTimeMillis(); //如果设置了获取锁等待时间 if ( millisToWait <= 0 ) { doDelete = true; // 超时则删除子节点 break; } //等待超时时间 wait(millisToWait); } else { wait();//一直等待 } } catch ( KeeperException.NoNodeException e ) { // it has been deleted (i.e. lock released). Try to acquire again //如果前一个子节点已经被删除则deException,只需要自旋获取一次即可 } } } } } catch ( Exception e ) { ThreadUtils.checkInterrupted(e); doDelete = true; throw e; } finally { if ( doDelete ) { deleteOurPath(ourPath);//获取锁超时则删除节点 } } return haveTheLock; } 释放锁: public void release() throws Exception { Thread currentThread = Thread.currentThread(); LockData lockData = threadData.get(currentThread); //没有获取锁,你释放个球球,如果为空抛出异常 if ( lockData == null ) { throw new IllegalMonitorStateException("You do not own the lock: " + basePath); } //获取重入数量 int newLockCount = lockData.lockCount.decrementAndGet(); //如果重入锁次数大于0,直接返回 if ( newLockCount > 0 ) { return; } //如果重入锁次数小于0,抛出异常 if ( newLockCount < 0 ) { throw new IllegalMonitorStateException("Lock count has gone negative for lock: " + basePath); } try { //释放锁 internals.releaseLock(lockData.lockPath); } finally { //移除当前线程锁数据 threadData.remove(currentThread); } } 测试案例 为了更好的理解其原理和代码分析中获取锁的过程,这里我们实现一个简单的Demo: /** * 基于curator的zookeeper分布式锁 */ public class CuratorUtil { private static String address = "192.168.1.180:2181"; public static void main(String[] args) { //1、重试策略:初试时间为1s 重试3次 RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3); //2、通过工厂创建连接 CuratorFramework client = CuratorFrameworkFactory.newClient(address, retryPolicy); //3、开启连接 client.start(); //4 分布式锁 final InterProcessMutex mutex = new InterProcessMutex(client, "/curator/lock"); //读写锁 //InterProcessReadWriteLock readWriteLock = new InterProcessReadWriteLock(client, "/readwriter"); ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5); for (int i = 0; i < 5; i++) { fixedThreadPool.submit(new Runnable() { @Override public void run() { boolean flag = false; try { //尝试获取锁,最多等待5秒 flag = mutex.acquire(5, TimeUnit.SECONDS); Thread currentThread = Thread.currentThread(); if(flag){ System.out.println("线程"+currentThread.getId()+"获取锁成功"); }else{ System.out.println("线程"+currentThread.getId()+"获取锁失败"); } //模拟业务逻辑,延时4秒 Thread.sleep(4000); } catch (Exception e) { e.printStackTrace(); } finally{ if(flag){ try { mutex.release(); } catch (Exception e) { e.printStackTrace(); } } } } }); } } } 这里我们开启5个线程,每个线程获取锁的最大等待时间为5秒,为了模拟具体业务场景,方法中设置4秒等待时间。开始执行main方法,通过ZooInspector监控/curator/lock下的节点如下图: 对,没错,设置4秒的业务处理时长就是为了观察生成了几个顺序节点。果然如案例中所述,每个线程都会生成一个节点并且还是有序的。 观察控制台,我们会发现只有两个线程获取锁成功,另外三个线程超时获取锁失败会自动删除节点。线程执行完毕我们刷新一下/curator/lock节点,发现刚才创建的五个子节点已经不存在了。 小结 通过分析第三方开源工具实现的分布式锁方式,收获还是满满的。学习本身就是一个由浅入深的过程,从如何调用API,到理解其代码逻辑实现,想要更深入可以去挖掘Zookeeper的核心算法ZAB协议。 最后为了方便大家学习,总结了学习过程中遇到的几个关键词:重入锁、自旋锁、有序节点、阻塞、非阻塞、监听,希望对大家有所帮助。 文章来源:https://mp.weixin.qq.com/s/UQbSeGHAak1WDerPlQBq7w相关推荐:https://www.roncoo.com/course/view/086c2c4027ac4a00aa1e9d63d7bac36d
课程介绍该课程以实战方式实现一套经典的分布式系统架构;讲解如何进行系统拆分架构:1、传统ssm框架搭建、2、独立restful服务工程搭建、3、服务接口底层访问、4、redis实现业务缓存、5、单点登录系统实现。 将传统的单系统工程,拆分成多个独立发布的系统工程: maven工程结构图: 服务工程结构图: sso实现流程图 内容大纲 第1节分布式框架系统整体介绍 00:23:21分钟 | 第2节规划工程结构、使用maven进行构建 00:52:32分钟 | 第3节分布式系统框架搭建-SSM工程搭建 00:33:44分钟 | 第4节分布式系统框架搭建-SSM的测试 00:29:38分钟 | 第5节Restful原理分析和服务工程搭建 00:32:05分钟 | 第6节Restful服务发布 00:24:31分钟 | 第7节Restful服务测试 00:48:38分钟 | 第8节使用HttpClient实现系统之间服务调用 00:17:30分钟 | 第9节业务功能缓存的实现-redis单机版安装 00:19:43分钟 | 第10节业务功能缓存的实现-redis集群环境搭建 00:21:36分钟 | 第11节业务功能缓存的实现-redis单机和集群环境测试 00:20:57分钟 | 第12节业务功能缓存的实现-spring和redis的集成 00:20:54分钟 | 第13节业务功能缓存的实现-使用redis实现业务缓存 00:40:36分钟 | 第14节单点登录系统SSO原理分析 00:10:45分钟 | 第15节单点登录系统SSO工程搭建 00:17:44分钟 | 第16节单点登录系统SSO服务规划 00:09:40分钟 | 第17节单点登录系统SSO的服务发布 00:52:30分钟 | 第18节单点登录系统SSO的注册功能实现 00:15:59分钟 | 第19节单点登录系统SSO的登录功能实现 00:17:35分钟 | 第20节业务系统与单点登录系统的整合 01:19:17分钟 | 参考内容:https://www.roncoo.com/course/view/e5693f3f0d144e7ba4b7d1e34c0dd265
1、Spark简介 Apache Spark 是专为大规模数据处理而设计的快速通用的计算引擎。Spark是UC Berkeley AMP lab (加州大学伯克利分校的AMP实验室)所开源的类Hadoop MapReduce的通用并行框架,Spark,拥有Hadoop MapReduce所具有的优点;但不同于MapReduce的是——Job中间输出结果可以保存在内存中,从而不再需要读写HDFS,因此Spark能更好地适用于数据挖掘与机器学习等需要迭代的MapReduce的算法。 Spark 是一种与 Hadoop 相似的开源集群计算环境,但是两者之间还存在一些不同之处,这些有用的不同之处使 Spark 在某些工作负载方面表现得更加优越,换句话说,Spark 启用了内存分布数据集,除了能够提供交互式查询外,它还可以优化迭代工作负载。 Spark 是在 Scala 语言中实现的,它将 Scala 用作其应用程序框架。与 Hadoop 不同,Spark 和 Scala 能够紧密集成,其中的 Scala 可以像操作本地集合对象一样轻松地操作分布式数据集。 尽管创建 Spark 是为了支持分布式数据集上的迭代作业,但是实际上它是对 Hadoop 的补充,可以在 Hadoop 文件系统中并行运行。通过名为 Mesos 的第三方集群框架可以支持此行为。Spark 由加州大学伯克利分校 AMP 实验室 (Algorithms, Machines, and People Lab) 开发,可用来构建大型的、低延迟的数据分析应用程序。 2、部署准备 2.1、安装包准备 spark-2.2.0-bin-hadoop2.6.tgz jdk-8u161-linux-x64.tar.gz scala-2.11.0.tgz 2.2、节点配置信息2.3、节点资源配置信息3、集群配置与启动 3.1、安装包上传与解压 操作节点:risen01 操作用户:root 上传安装包spark-2.2.0-bin-hadoop2.6.tgz,scala-2.11.0.tgz,jdk-8u161-linux-x64.tar.gz(如果已经存在则不需要此步骤)到 risen01节点下的~/packages目录下,结果如图所示: 2、解压JDK安装包,Spark安装包Scala安装包和到/usr/local下 操作节点:risen01 操作用户:root 解压JDK命令: tar -zxvf ~/packeages/jdk-8u161-linux-x64.tar.gz -C /usr/local 解压spark命令: tar -zxvf ~/packages/spark-2.2.0-bin-hadoop2.6.tgz -C /usr/local 解压Scala命令: tar -zxvf ~/packages/scala-2.11.0.tgz -C /usr/local 3.2、启动前准备 操作节点:risen01,risen02,risen03 操作用户:root 在/data目录下新建立spark/work目录用来存放spark的任务处理日志 在/log目录下新建立spark目录用来存放spark的启动日志等 3.3、修改配置文件 3.3.1、编辑spark-env.sh文件 操作节点:risen01 操作用户:root 说明:请根据实际集群的规模和硬件条件来配置每一项参数 进入到/usr/local/spark-2.2.0-bin-hadoop2.6/conf目录下执行命令: cp spark-env.sh.template spark-env.sh 编辑spark-env.sh文件,添加以下内容: #设置spark的web访问端口 SPARK_MASTER_WEBUI_PORT=18080 #设置spark的任务处理日志存放目录 SPARK_WORKER_DIR=/data/spark/work #设置spark每个worker上面的核数 SPARK_WORKER_CORES=2 #设置spark每个worker的内存 SPARK_WORKER_MEMORY=1g #设置spark的启动日志等目录 SPARK_LOG_DIR=/log/spark #指定spark需要的JDK目录 export JAVA_HOME=/usr/local/jdk1.8.0_161 #指定spark需要的Scala目录 export SCALA_HOME=/usr/local/scala-2.11.0 #指定Hadoop的安装目录 export HADOOP_HOME=/opt/cloudera/parcels/CDH/lib/hadoop #指定Hadoop的配置目录 export HADOOP_CONF_DIR=/opt/cloudera/parcels/CDH/lib/hadoop/etc/hadoop/ #实现spark-standlone HA(因为我们HA实现的是risen01和risen02之间的切换不涉及risen03,所以这段配置risen03可有可无) export SPARK_DAEMON_JAVA_OPTS="-Dspark.deploy.recoveryMode=ZOOKEEPER -Dspark.deploy.zookeeper.url=risen01:2181,risen02:2181,risen03:2181 -Dspark.deploy.zookeeper.dir=/data/spark" 3.3.2、 编辑spark-defaults.conf文件 操作节点:risen01 操作用户:root 说明:请根据实际集群的规模和硬件条件来配置每一项参数 进入到/usr/local/spark-2.2.0-bin-hadoop2.6/conf目录下执行命令: cp spark-defaults.conf.template spark-defaults.conf 编辑spark-defaults.conf文件,添加以下内容: #设置spark的主节点 spark.master spark://risen01:7077 #开启eventLog spark.eventLog.enabled true #设置eventLog存储目录 spark.eventLog.dir /log/spark/eventLog #设置spark序列化方式 spark.serializer org.apache.spark.serializer.KryoSerializer #设置spark的driver内存 spark.driver.memory 1g #设置spark的心跳检测时间间隔 spark.executor.heartbeatInterval 20s #默认并行数 spark.default.parallelism 20 #最大网络延时 spark.network.timeout 3000s 3.3.3、 编辑slaves文件 操作节点:risen01 操作用户:root 说明:请根据实际集群的规模和硬件条件来配置每一项参数 进入到/usr/local/spark-2.2.0-bin-hadoop2.6/conf目录下执行命令: cp slaves.templete slaves 编辑slaves文件,修改localhost为: risen01 risen02 risen03 3.4、分发其他节点 执行scp命令: 操作节点:risen01 操作用户:root scp -r /usr/local/spark-2.2.0-bin-hadoop2.6 root@risen02:/usr/local scp -r /usr/local/scala-2.11.0 root@risen02:/usr/local scp -r /usr/local/jdk1.8.0_161 root@risen02:/usr/local scp -r /usr/local/spark-2.2.0-bin-hadoop2.6 root@risen03:/usr/local scp -r /usr/local/scala-2.11.0 root@risen03:/usr/local scp -r /usr/local/jdk1.8.0_161 root@risen03:/usr/local 需要提前创建好bigdata用户并实现免密(这里不再赘述,此步骤如果做过可不做) 权限修改 操作节点:risen01,risen02,risen03 操作用户:root 修改/log/spark权限命令: chown -R bigdata.bigdata /log/spark 修改/data/spark权限命令: chown -R bigdata.bigdata /data/spark 修改spark的安装目录命令: chown -R bigdata.bigdata /usr/local/spark-2.2.0-bin-hadoop2.6 修改Scala的安装目录命令: chown -R bigdata.bigdata /usr/local/scala-2.11.0 修改JDK1.8的安装目录命令:(此步骤如果做过可不做) chown -R bigdata.bigdata /usr/local/jdk1.8.0_161 结果如图下所示:3.5、启动集群 操作节点:risen01,risen02 操作用户:bigdata (1) 进入到/usr/local/spark-2.2.0-bin-hadoop2.6/sbin目录下执行./start-all.sh,查看web界面如下图所示: 然后在进入到risen02机器的spark安装目录下/usr/local/spark-2.2.0-bin-hadoop2.6/sbin执行命令./start-master.sh启动spark集群的备用主节点。(记得一定要启动备用主节点的进程,这里我们只用risen02做备用主节点,risen03虽然也配置了有资格,但是暂时我们不需要) (2) 进入到/usr/local/spark-2.2.0-bin-hadoop2.6/bin目录下执行spark-shell,并测试统计词频的测试,结果如下图所示: 截止到此,spark-standlone模式便安装成功了! 推荐阅读:https://www.roncoo.com/course/view/c4e0130ea2354c71a2cb9ba24348746c 文章来源:https://my.oschina.net/blogByRzc/blog/1800450
Kafka的特性 高吞吐量、低延迟:kafka每秒可以处理几十万条消息,它的延迟最低只有几毫秒,每个topic可以分多个partition, consumer group 对partition进行consume操作。 可扩展性:kafka集群支持热扩展 持久性、可靠性:消息被持久化到本地磁盘,并且支持数据备份防止数据丢失 容错性:允许集群中节点失败(若副本数量为n,则允许n-1个节点失败) 高并发:支持数千个客户端同时读写 Kafka的使用场景 日志收集:一个公司可以用Kafka可以收集各种服务的log,通过kafka以统一接口服务的方式开放给各种consumer,例如hadoop、Hbase、Solr等。 消息系统:解耦和生产者和消费者、缓存消息等。 用户活动跟踪:Kafka经常被用来记录web用户或者app用户的各种活动,如浏览网页、搜索、点击等活动,这些活动信息被各个服务器发布到kafka的topic中,然后订阅者通过订阅这些topic来做实时的监控分析,或者装载到hadoop、数据仓库中做离线分析和挖掘。 运营指标:Kafka也经常用来记录运营监控数据。包括收集各种分布式应用的数据,生产各种操作的集中反馈,比如报警和报告。 流式处理:比如spark streaming和storm 事件源 Kafka部分名词解释如下 Kafka中发布订阅的对象是topic。我们可以为每类数据创建一个topic,把向topic发布消息的客户端称作producer,从topic订阅消息的客户端称作consumer。Producers和consumers可以同时从多个topic读写数据。一个kafka集群由一个或多个broker服务器组成,它负责持久化和备份具体的kafka消息。 Broker:Kafka节点,一个Kafka节点就是一个broker,多个broker可以组成一个Kafka集群。 Topic:一类消息,消息存放的目录即主题,例如page view日志、click日志等都可以以topic的形式存在,Kafka集群能够同时负责多个topic的分发。 Partition:topic物理上的分组,一个topic可以分为多个partition,每个partition是一个有序的队列 Segment:partition物理上由多个segment组成,每个Segment存着message信息 Producer : 生产message发送到topic下的partition leader. Consumer : 订阅topic消费message, consumer作为一个线程来消费 Consumer Group:一个Consumer Group包含多个consumer, 这个是预先在配置文件中配置好的。 Kakfa Broker Leader的选举 Kakfa Broker集群受Zookeeper管理。所有的Kafka Broker节点一起去Zookeeper上注册一个临时节点,因为只有一个Kafka Broker会注册成功,其他的都会失败,所以这个成功在Zookeeper上注册临时节点的这个Kafka Broker会成为Kafka Broker Controller,其他的Kafka broker叫Kafka Broker follower。 这个Controller会监听其他的Kafka Broker的所有信息,例如:一旦有一个broker宕机了,这个kafka broker controller会读取该宕机broker上所有的partition在zookeeper上的状态,并选取ISR列表中的一个replica作为partition leader(如果ISR列表中的replica全挂,选一个幸存的replica作为leader; 如果该partition的所有的replica都宕机了,则将新的leader设置为-1,等待恢复,等待ISR中的任一个Replica“活”过来,并且选它作为Leader; 或选择第一个“活”过来的Replica(不一定是ISR中的)作为Leader),这个broker宕机的事情,kafka controller也会通知zookeeper,zookeeper就会通知其他的kafka broker。 如果这个kafka broker controller宕机了,在zookeeper上面的那个临时节点就会消失,此时所有的kafka broker又会一起去Zookeeper上注册一个临时节点,Kafka的核心是日志文件,日志文件在集群中的同步是分布式数据系统最基础的要素。 Kafka动态维护了一个同步状态的副本的集合,简称ISR,在这个集合中的节点都是和leader保持高度一致的,任何一条消息必须被这个集合中的每个节点读取并追加到日志中了,才会通知外部这个消息已经被提交了。 因此这个集合中的任何一个节点随时都可以被选为leader,ISR在ZooKeeper中维护。ISR中有f+1个节点,就可以允许在f个节点down掉的情况下不会丢失消息并正常提供服。ISR的成员是动态的,如果一个节点被淘汰了,当它重新达到“同步中”的状态时,他可以重新加入ISR,这种leader的选择方式是非常快速的,适合kafka的应用场景。 kafka消息生产 Topic & Partition:Topic相当于传统消息系统MQ中的一个队列queue,producer端发送的message必须指定是发送到哪个topic,但是不需要指定topic下的哪个partition,因为kafka会把收到的message进行load balance,均匀的分布在这个topic下的不同的partition上( hash(message) % [partition数量] )。 物理上存储上,这个topic会分成一个或多个partition,每个partiton相当于是一个子queue。在物理结构上,每个partition对应一个物理的目录(文件夹),文件夹命名是[topicname]_[partition]_[序号],一个topic可以有无数多的partition,根据业务需求和数据量来设置。 在kafka配置文件中可随时更高num.partitions参数来配置更改topic的partition数量,在创建Topic时通过参数指定parittion数量。Topic创建之后通过Kafka提供的工具也可以修改partiton数量。 一般来说,(1)一个Topic的Partition数量大于等于Broker的数量,可以提高吞吐率。(2)同一个Partition的备份(Replica)尽量分散到不同的机器,高可用。 当add a new partition的时候,partition里面的message不会重新进行分配,原来的partition里面的message数据不会变,新加的这个partition刚开始是空的,随后进入这个topic的message就会重新参与所有partition的load balance Partition Replica(备份):每个partition可以在其他的kafka broker节点上存副本,以便某个kafka broker节点宕机不会影响这个kafka集群。存replica副本的方式是按照kafka broker的顺序存。 例如有5个kafka broker节点,某个topic有3个partition,每个partition存2个副本,那么partition1存broker1,broker2,partition2存broker2,broker3。。。以此类推(replica副本数目不能大于kafka broker节点的数目,否则报错。 这里的replica数其实就是partition的副本总数,其中包括一个leader,其他的就是copy副本)。这样如果某个broker宕机,其实整个kafka内数据依然是完整的。但是,replica副本数越高,系统虽然越稳定,但是会带来资源和性能上的下降;replica副本少的话,也会造成系统丢数据的风险。 (1)怎样传送消息:Producer端使用zookeeper用来"发现"broker列表,以及和Topic下每个partition leader建立socket连接并发送消息,每个Topic下面有多个partition, 每个partition又有多个备份(Replica),以及一个Leader,消息仅会被发送到Topic下的其中一个partition的Leader,再由Leader发送给其他partition follower。 (如果让producer发送给每个replica那就太慢了), Producer客户端自己控制着消息被推送到哪些partition leader,不需要经过任何中介或其他路由转发。 为了实现这个特性,kafka集群中的每个broker都可以响应producer的请求,并返回topic的一些元信息,这些元信息包括哪些机器是存活的,topic的leader partition都在哪,现阶段哪些leader partition是可以直接被访问的。 (2)在向Producer发送ACK前需要保证有多少个Replica已经收到该消息:根据ack配的个数而定 (3)怎样处理某个Replica不工作的情况:如果这个部工作的partition replica不在ack列表中,就是producer在发送消息到partition leader上,partition leader向partition follower发送message没有响应而已,这个不会影响整个系统,也不会有什么问题。 如果这个不工作的partition replica在ack列表中的话,producer发送的message的时候会等待这个不工作的partition replca写message成功,但是会等到time out,然后返回失败因为某个ack列表中的partition replica没有响应,此时kafka会自动的把这个部工作的partition replica从ack列表中移除,以后的producer发送message的时候就不会有这个ack列表下的这个部工作的partition replica了。 (4)怎样处理Failed Replica恢复回来的情况:如果这个partition replica之前不在ack列表中,那么启动后重新受Zookeeper管理即可,之后producer发送message的时候,partition leader会继续发送message到这个partition follower上。 如果这个partition replica之前在ack列表中,此时重启后,需要把这个partition replica再手动加到ack列表中。(ack列表是手动添加的,出现某个部工作的partition replica的时候自动从ack列表中移除的) Partition leader与follower:partition也有leader和follower之分。leader是主partition,producer写kafka的时候先写partition leader,再由partition leader push给其他的partition follower。partition leader与follower的信息受Zookeeper控制,一旦partition leader所在的broker节点宕机,zookeeper会冲其他的broker的partition follower上选择follower变为parition leader。 Topic分配partition和partition replica的算法:(1)将Broker(size=n)和待分配的Partition排序。(2)将第i个Partition分配到第(i%n)个Broker上。(3)将第i个Partition的第j个Replica分配到第((i + j) % n)个Broker上 Partition ack:当ack=1,表示producer写partition leader成功后,broker就返回成功,无论其他的partition follower是否写成功。当ack=2,表示producer写partition leader和其他一个follower成功的时候,broker就返回成功,无论其他的partition follower是否写成功。当ack=-1[parition的数量]的时候,表示只有producer全部写成功的时候,才算成功,kafka broker才返回成功信息。这里需要注意的是,如果ack=1的时候,一旦有个broker宕机导致partition的follower和leader切换,会导致丢数据。 message状态:在Kafka中,消息的状态被保存在consumer中,broker不会关心哪个消息被消费了被谁消费了,只记录一个offset值(指向partition中下一个要被消费的消息位置),这就意味着如果consumer处理不好的话,broker上的一个消息可能会被消费多次。 message有效期:Kafka会长久保留其中的消息,以便consumer可以多次消费,当然其中很多细节是可配置的。 不是严格的JMS, 因此kafka对消息的重复、丢失、错误以及顺序型没有严格的要求。(这是与AMQ最大的区别,所以没有用在支付领域) kafka提供at-least-once delivery,即当consumer宕机后,有些消息可能会被重复delivery。 因每个partition只会被consumer group内的一个consumer消费,故kafka保证每个partition内的消息会被顺序的订阅。 Kafka为每条消息为每条消息计算CRC校验,用于错误检测,crc校验不通过的消息会直接被丢弃掉。 Consumer Group 各个consumer(consumer 线程)可以组成一个组(Consumer group ),partition中的每个message只能被组(Consumer group )中的一个consumer(consumer 线程)消费,如果一个message可以被多个consumer(consumer 线程)消费的话,那么这些consumer必须在不同的组。 Kafka不支持一个partition中的message由两个或两个以上的同一个consumer group下的consumer thread来处理,除非再启动一个新的consumer group。 所以如果想同时对一个topic做消费的话,启动多个consumer group就可以了,一般这种情况都是多个不同的业务逻辑,才会启动多个consumer group来处理一个topic,,但是要注意的是,这里的多个consumer的消费都必须是顺序读取partition里面的message,新启动的consumer默认从partition队列最头端最新的地方开始阻塞的读message。 它不能像AMQ那样可以多个BET作为consumer去互斥的(for update悲观锁)并发处理message,这是因为多个BET去消费一个Queue中的数据的时候,由于要保证不能多个线程拿同一条message,所以就需要行级别悲观所(for update),这就导致了consume的性能下降,吞吐量不够。 而kafka为了保证吞吐量,只允许同一个consumer group下的一个consumer线程去访问一个partition。如果觉得效率不高的时候,可以加partition的数量来横向扩展,那么再加新的consumer thread去消费。如果想多个不同的业务都需要这个topic的数据,起多个consumer group就好了,大家都是顺序的读取message,offsite的值互不影响。 这样没有锁竞争,充分发挥了横向的扩展性,吞吐量极高。这也就形成了分布式消费的概念。 当启动一个consumer group去消费一个topic的时候,无论topic里面有多个少个partition,无论我们consumer group里面配置了多少个consumer thread,这个consumer group下面的所有consumer thread一定会消费全部的partition; 即便这个consumer group下只有一个consumer thread,那么这个consumer thread也会去消费所有的partition。 因此,最优的设计就是,consumer group下的consumer thread的数量等于partition数量,这样效率是最高的。一个consumer thread处理一个partition。 如果这个consumer group里面consumer的数量小于topic里面partition的数量,就会有consumer thread同时处理多个partition(这个是kafka自动的机制,我们不用指定),但是总之这个topic里面的所有partition都会被处理到的。 如果这个consumer group里面consumer的数量大于topic里面partition的数量,多出的consumer thread就会闲着啥也不干,因为一个partition不可能被两个consumer thread去处理。 如果producer的流量增大,当前的topic的parition数量=consumer数量,这时候的应对方式就是扩展:增加topic下的partition,同时增加这个consumer group下的consumer。 Consumer Rebalance的触发条件:(1)Consumer增加或删除会触发 Consumer Group的Rebalance(2)Broker的增加或者减少都会触发 Consumer Rebalance Consumer: Consumer处理partition里面的message的时候是O(1)顺序读取的。所以必须维护着上一次读到哪里的offsite信息。high level API,offset存于Zookeeper中,low level API的offset由自己维护。一般来说都是使用high level api的。 Kafka delivery guarantee(message传送保证):(1)At most once消息可能会丢,绝对不会重复传输; 消费者fetch消息,然后保存offset,然后处理消息;当client保存offset之后,但是在消息处理过程中consumer进程失效(crash),导致部分消息未能继续处理.那么此后可能其他consumer会接管,但是因为offset已经提前保存,那么新的consumer将不能fetch到offset之前的消息(尽管它们尚没有被处理), (2)At least once 消息绝对不会丢,但是可能会重复传输;消费者fetch消息,然后处理消息,然后保存offset.如果消息处理成功之后,但是在保存offset阶段zookeeper异常或者consumer失效,导致保存offset操作未能执行成功,这就导致接下来再次fetch时可能获得上次已经处理过的消息,这就是"at least once". (3)kafka利用主键幂等性实现Exactly once每条信息肯定会被传输一次且仅传输一次,这是用户想要的。最少1次+消费者的输出中额外增加已处理消息最大编号:由于已处理消息最大编号的存在,不会出现重复处理消息的情况。 在kafka中,当前读到哪条消息的offset值是由consumer来维护的,因此,consumer可以自己决定如何读取kafka中的数据。比如,consumer可以通过重设offset值来重新消费已消费过的数据。不管有没有被消费,kafka会保存数据一段时间,这个时间周期是可配置的,只有到了过期时间,kafka才会删除这些数据。(这一点与AMQ不一样,AMQ的message一般来说都是持久化到mysql中的,消费完的message会被delete掉) 其他JMS实现,消息消费的位置是有prodiver保留,以便避免重复发送消息或者将没有消费成功的消息重发等,同时还要控制消息的状态.这就要求JMS broker需要太多额外的工作. 在kafka中,partition中的消息只有一个consumer在消费,且不存在消息状态的控制,也没有复杂的消息确认机制,可见kafka broker端是相当轻量级的. 当消息被consumer接收之后,consumer可以在本地保存最后消息的offset,并间歇性的向zookeeper注册offset.由此可见,consumer客户端也很轻量级。 kafka中consumer负责维护消息的消费记录,而broker则不关心这些,这种设计不仅提高了consumer端的灵活性,也适度的减轻了broker端设计的复杂度; 这是和众多JMS prodiver的区别.此外,kafka中消息ACK的设计也和JMS有很大不同,kafka中的消息是批量(通常以消息的条数或者chunk的尺寸为单位)发送给consumer,当消息消费成功后,向zookeeper提交消息的offset,而不会向broker交付ACK. 或许你已经意识到,这种"宽松"的设计,将会有"丢失"消息/"消息重发"的危险. Delivery Mode : Kafka producer 发送message不用维护message的offsite信息,因为这个时候,offsite就相当于一个自增id,producer就尽管发送message就好了。 而且Kafka与AMQ不同,AMQ大都用在处理业务逻辑上,而Kafka大都是日志,所以Kafka的producer一般都是大批量的batch发送message,向这个topic一次性发送一大批message,load balance到一个partition上,一起插进去,offsite作为自增id自己增加就好。 但是Consumer端是需要维护这个partition当前消费到哪个message的offsite信息的,这个offsite信息,high level api是维护在Zookeeper上,low level api是自己的程序维护。 (Kafka管理界面上只能显示high level api的consumer部分,因为low level api的partition offsite信息是程序自己维护,kafka是不知道的,无法在管理界面上展示 )当使用high level api的时候,先拿message处理,再定时自动commit offsite+1(也可以改成手动), 并且kakfa处理message是没有锁操作的。 因此如果处理message失败,此时还没有commit offsite+1,当consumer thread重启后会重复消费这个message。但是作为高吞吐量高并发的实时处理系统,at least once的情况下,至少一次会被处理到,是可以容忍的。如果无法容忍,就得使用low level api来自己程序维护这个offsite信息,那么想什么时候commit offsite+1就自己搞定了。 Kafka高吞吐量 除磁盘IO之外,我们还需要考虑网络IO,这直接关系到kafka的吞吐量问题,可以采用如下方式: 发布者每次可发布多条消息(将消息加到一个消息集合中发布), consumer每次迭代消费一条消息。以Batch的方式推送数据可以极大的提高处理效率,kafka Producer 可以将消息在内存中累计到一定数量后作为一个batch发送请求。 Batch的数量大小可以通过Producer的参数控制,参数值可以设置为累计的消息的数量(如500条)、累计的时间间隔(如100ms)或者累计的数据大小(64KB)。这里需要注意,可以根据不同的业务场景,设置不同的参数 不创建单独的cache,使用系统的page cache。发布者顺序发布,订阅者通常比发布者滞后一点点,直接使用Linux的page cache效果也比较后,同时减少了cache管理及垃圾收集的开销。 使用sendfile优化网络传输,减少一次内存拷贝 Sendfile 函数在两个文件描写叙述符之间直接传递数据(全然在内核中操作,传送),从而避免了内核缓冲区数据和用户缓冲区数据之间的拷贝,操作效率非常高,被称之为零拷贝。 Sendfile 函数的定义例如以下: #include<sys/sendfile.h> ssize_t sendfile(int out_fd,int in_fd,off_t*offset,size_t count); 传统方式read/write send/recv在传统的文件传输里面(read/write方式),在实现上事实上是比較复杂的,须要经过多次上下文的切换。我们看一下例如以下两行代码: 1. read(file, tmp_buf, len); 2. write(socket, tmp_buf, len); 以上两行代码是传统的read/write方式进行文件到socket的传输。 当须要对一个文件进行传输的时候,其详细流程细节例如以下: 1、调用read函数,文件数据被copy到内核缓冲区2、read函数返回。文件数据从内核缓冲区copy到用户缓冲区3、write函数调用。将文件数据从用户缓冲区copy到内核与socket相关的缓冲区。4、数据从socket缓冲区copy到相关协议引擎。 以上细节是传统read/write方式进行网络文件传输的方式,我们能够看到,在这个过程其中。文件数据实际上是经过了四次copy操作: 硬盘—>内核buf—>用户buf—>socket相关缓冲区(内核)—>协议引擎新方式sendfile而sendfile系统调用则提供了一种降低以上多次copy。提升文件传输性能的方法。Sendfile系统调用是在2.1版本号内核时引进的: sendfile(socket, file, len); 执行流程例如以下: 1、sendfile系统调用,文件数据被copy至内核缓冲区2、再从内核缓冲区copy至内核中socket相关的缓冲区3、最后再socket相关的缓冲区copy到协议引擎 相较传统read/write方式,2.1版本号内核引进的sendfile已经降低了内核缓冲区到user缓冲区。再由user缓冲区到socket相关 缓冲区的文件copy,而在内核版本号2.4之后,文件描写叙述符结果被改变,sendfile实现了更简单的方式,系统调用方式仍然一样,细节与2.1版本号的 不同之处在于,当文件数据被拷贝到内核缓冲区时,不再将全部数据copy到socket相关的缓冲区,而是只将记录数据位置和长度相关的数据保存到 socket相关的缓存,而实际数据将由DMA模块直接发送到协议引擎,再次降低了一次copy操作。 4、其实对于producer/consumer/broker三者而言,CPU的开支应该都不大,因此启用消息压缩机制是一个良好的策略;压缩需要消耗少量的CPU资源,不过对于kafka而言,网络IO更应该需要考虑.可以将任何在网络上传输的消息都经过压缩.kafka支持gzip/snappy等多种压缩方式 push-and-pull : Kafka中的Producer和consumer采用的是push-and-pull模式,即Producer只管向broker push消息,consumer只管从broker pull消息,两者对消息的生产和消费是异步的。 负载均衡方面: Kafka提供了一个 metadata API来管理broker之间的负载(对Kafka0.8.x而言,对于0.7.x主要靠zookeeper来实现负载均衡)。 同步异步:Producer采用异步push方式,极大提高Kafka系统的吞吐率(可以通过参数控制是采用同步还是异步方式)。 离线数据装载:Kafka由于对可拓展的数据持久化的支持,它也非常适合向Hadoop或者数据仓库中进行数据装载。 实时数据与离线数据:kafka既支持离线数据也支持实时数据,因为kafka的message持久化到文件,并可以设置有效期,因此可以把kafka作为一个高效的存储来使用,可以作为离线数据供后面的分析。当然作为分布式实时消息系统,大多数情况下还是用于实时的数据处理的,但是当cosumer消费能力下降的时候可以通过message的持久化在淤积数据在kafka。 插件支持:现在不少活跃的社区已经开发出不少插件来拓展Kafka的功能,如用来配合Storm、Hadoop、flume相关的插件。 峰值: 在访问量剧增的情况下,kafka水平扩展, 应用仍然需要继续发挥作用 可恢复性: 系统的一部分组件失效时,由于有partition的replica副本,不会影响到整个系统。 顺序保证性:由于kafka的producer的写message与consumer去读message都是顺序的读写,保证了高效的性能。 缓冲:由于producer那面可能业务很简单,而后端consumer业务会很复杂并有数据库的操作,因此肯定是producer会比consumer处理速度快,如果没有kafka,producer直接调用consumer,那么就会造成整个系统的处理速度慢,加一层kafka作为MQ,可以起到缓冲的作用。 消息持久化 消息格式每个log entry格式为"4个字节的数字N表示消息的长度" + "N个字节的消息内容";每个日志都有一个offset来唯一的标记一条消息,offset的值为8个字节的数字,表示此消息在此partition中所处的起始位置..每个partition在物理存储层面,有多个log file组成(称为segment).segment file的命名为"最小offset".kafka.例如"00000000000.kafka";其中"最小offset"表示此segment中起始消息的offset. 获取消息时,需要指定offset和最大chunk尺寸,offset用来表示消息的起始位置,chunk size用来表示最大获取消息的总长度(间接的表示消息的条数).根据offset,可以找到此消息所在segment文件,然后根据segment的最小offset取差值,得到它在file中的相对位置,直接读取输出即可. message持久化:Kafka中会把消息持久化到本地文件系统中,并且保持o(1)极高的效率。我们众所周知IO读取是非常耗资源的性能也是最慢的,这就是为了数据库的瓶颈经常在IO上,需要换SSD硬盘的原因。但是Kafka作为吞吐量极高的MQ,却可以非常高效的message持久化到文件。这是因为Kafka是顺序写入O(1)的时间复杂度,速度非常快。也是高吞吐量的原因。由于message的写入持久化是顺序写入的,因此message在被消费的时候也是按顺序被消费的,保证partition的message是顺序消费的。一般的机器,单机每秒100k条数据。 kafka使用文件存储消息(append only log),这就直接决定kafka在性能上严重依赖文件系统的本身特性.且无论任何OS下,对文件系统本身的优化是非常艰难的.文件缓存/直接内存映射等是常用的手段.因为kafka是对日志文件进行append操作,因此磁盘检索的开支是较小的;同时为了减少磁盘写入的次数,broker会将消息暂时buffer起来,当消息的个数(或尺寸)达到一定阀值时,再flush到磁盘,这样减少了磁盘IO调用的次数.对于kafka而言,较高性能的磁盘,将会带来更加直接的性能提升. 消息存储Kafka高度依赖文件系统来存储和缓存消息(AMQ的nessage是持久化到mysql数据库中的),因为一般的人认为磁盘是缓慢的,这导致人们对持久化结构具有竞争性持怀疑态度。其实,磁盘的快或者慢,这决定于我们如何使用磁盘。因为磁盘线性写的速度远远大于随机写。线性读写在大多数应用场景下是可以预测的。 每个partion(目录)相当于一个巨型文件被平均分配到多个大小相等segment(段)数据文件中。但每个段segment file消息数量不一定相等,这种特性方便old segment file快速被删除。 每个partiton只需要支持顺序读写就行了,segment文件生命周期由服务端配置参数决定。 推荐内容;大数据日志传输之Kafka实战文章来源:https://blog.csdn.net/caohao0591/article/details/80949616
《Tensorflow基础快速入门》课程的目的是帮助广大的深度学习爱好者,逐层深入,步步精通当下最流行的深度学习框架Tensorflow。该课程包含Tensorflow运行原理,Tensor上面常见的操作,常见API的使用,公式推导,Tensorboard,张量形状变换,张量上的数据操作,算术操作,矩阵操作,规约操作,序列比较和索引,共享变量,Graph图的操作,Tensorflow分布式部署,多层神经网络的搭建,神经元拟合的原理及生物智能方面得到的灵感。帮助大家一步一个脚印,把Tensorflow技术学扎实,学精通。 现在很多的深度学习技术例如CNN卷积神经网络,RNN循环神经网络,LSTM,BLSTM,GAN,聊天机器人,风格迁移,物体检测等,都可以使用Tensorflow来实现,并且在Github上可以找到很多这方面优秀的开源项目。那实际上这些项目都是像搭积木一样,使用Tensorflow里面的基本的操作搭建起来的,因此学好了Tensorflow的基础知识,可以快速的切入深度学习任意感兴趣的方向进行研究。 查看详情:https://www.roncoo.com/course/view/dbc0c38cd4fb4383b254e8983310b826
2020年09月
2020年08月
2020年07月