作者: chenyl

  • 腾讯:@Bean 与 @Component 用在同一个类上,会怎么样?

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

    来源:cnblogs.com/youzhibing/p/15354706.html

    • 疑虑背景
      • 疑虑描述
      • Spring Boot 版本
    • 结果验证
    • 源码解析
    • Spring 升级优化
    • 总结
      • 补充

    疑虑背景

    疑虑描述

    最近,在进行开发的过程中,发现之前的一个写法,类似如下

    图片

    以我的理解,@Configuration@Bean 会创建一个 userName 不为 null 的 UserManager 对象,而 @Component 也会创建一个 userName 为 null 的 UserManager 对象

    那么我们在其他对象中注入 UserManager 对象时,到底注入的是哪个对象?

    因为项目已经上线了很长一段时间了,所以这种写法没有编译报错,运行也没有出问题

    后面去找同事了解下,实际是想让

    图片

    生效,而实际也确实是它生效了

    那么问题来了:Spring 容器中到底有几个 UserManager 类型的对象?

    Spring Boot 版本

    项目中用的 Spring Boot 版本是:2.0.3.RELEASE

    对象的 scope 是默认值,也就是 singleton

    结果验证

    验证方式有很多,可以 debug 跟源码,看看 Spring 容器中到底有几个 UserManager 对象,也可以直接从 UserManager 构造方法下手,看看哪几个构造方法被调用,等等

    我们从构造方法下手,看看 UserManager 到底实例化了几次

    图片

    只有有参构造方法被调用了,无参构造方法岿然不动(根本没被调用)

    既然 UserManager 构造方法只被调用了一次,那么前面的问题:到底注入的是哪个对象

    答案也就清晰了,没得选了呀,只能是 @Configuration@Bean 创建的 userName 不为 null 的 UserManager 对象

    问题又来了:为什么不是 @Component 创建的 userName 为 null 的 UserManager 对象?

    源码解析

    @Configuration@Component 关系很紧密

    图片

    所以@Configuration 能够被 component scan

    其中 ConfigurationClassPostProcessor@Configuration 息息相关,其类继承结构图如下:

    图片

    它实现了 BeanFactoryPostProcessor 接口和 PriorityOrdered 接口,关于 BeanFactoryPostProcessor

    那么我们从 AbstractApplicationContext 的 refresh 方法调用的 invokeBeanFactoryPostProcessors(beanFactory)开始,来跟下源码

    图片

    此时完成了 com.lee.qsl 包下的 component scancom.lee.qsl 包及子包下的 UserConfig 、 UserController 和 UserManager 都被扫描出来

    注意,此刻@Bean 的处理还未开始, UserManager 是通过@Component 而被扫描出来的;此时 Spring 容器中 beanDefinitionMap 中的 UserManager 是这样的

    图片

    接下来一步很重要,与我们想要的答案息息相关

    图片

    图片

    循环递归处理 UserConfig 、 UserController 和 UserManager ,把它们都封装成 ConfigurationClass ,递归扫描 BeanDefinition

    循环完之后,我们来看看 configClasses

    图片

    UserConfig bean 定义信息中 beanMethods 中有一个元素 [BeanMethod:name=userManager,declaringClass=com.lee.qsl.config.UserConfig]

    然后我们接着往下走,来仔细看看答案出现的环节

    是不是有什么发现?@Component 修饰的 UserManager 定义直接被覆盖成了 @Configuration + @Bean 修饰的 UserManager 定义

    Bean 定义类型也由 ScannedGenericBeanDefinition 替换成了 ConfigurationClassBeanDefinition

    后续通过 BeanDefinition 创建实例的时候,创建的自然就是 @Configuration + @Bean 修饰的 UserManager ,也就是会反射调用 UserManager 的有参构造方法

    自此,答案也就清楚了

    Spring 其实给出了提示

    2021-10-03 20:37:33.697  INFO 13600 --- [           
    main] o.s.b.f.s.DefaultListableBeanFactory     : Overriding bean definition for bean 'userManager' with a different definition: replacing [Generic bean: class [com.lee.qsl.manager.UserManager]; scope=singleton; abstract=false; lazyInit=false; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null; defined in file [D:qsl-projectspring-boot-bean-componenttargetclassescomleeqslmanagerUserManager.class]] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=userConfig; factoryMethodName=userManager; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [com/lee/qsl/config/UserConfig.class]]

    只是日志级别是 info ,太不显眼了

    Spring 升级优化

    可能 Spring 团队意识到了 info 级别太不显眼的问题,或者说意识到了直接覆盖的处理方式不太合理

    所以在 Spring 5.1.2.RELEASE (Spring Boot 则是 2.1.0.RELEASE )做出了优化处理

    我们来具体看看

    图片

    启动直接报错,Spring 也给出了提示

    The bean 'userManager', defined in class path resource [com/lee/qsl/config/UserConfig.class], could not be registered. A bean with that name has already been defined in file [D:qsl-projectspring-boot-bean-componenttargetclassescomleeqslmanagerUserManager.class] and overriding is disabled.

    我们来跟下源码,主要看看与 Spring 5.0.7.RELEASE 的区别

    新增了配置项 allowBeanDefinitionOverriding 来控制是否允许 BeanDefinition 覆盖,默认情况下是不允许的

    我们可以在配置文件中配置:spring.main.allow-bean-definition-overriding=true ,允许 BeanDefinition 覆盖

    这种处理方式是更优的,将选择权交给开发人员,而不是自己偷偷的处理,已达到开发者想要的效果

    总结

    Spring 5.0.7.RELEASESpring Boot 2.0.3.RELEASE ) 支持@Configuration + @Bean@Component 同时作用于同一个类

    启动时会给 info 级别的日志提示,同时会将@Configuration + @Bean 修饰的 BeanDefinition 覆盖掉@Component 修饰的 BeanDefinition

    也许 Spring 团队意识到了上述处理不太合适,于是在 Spring 5.1.2.RELEASE 做出了优化处理

    增加了配置项:allowBeanDefinitionOverriding ,将主动权交给了开发者,由开发者自己决定是否允许覆盖

    补充

    关于 allowBeanDefinitionOverriding ,前面讲的不对,后面特意去翻了下源码,补充如下

    Spring 1.2 引进 DefaultListableBeanFactory 的时候就有了 private boolean allowBeanDefinitionOverriding = true;,默认是允许 BeanDefinition 覆盖

    图片

    Spring 4.1.2 引进了 isAllowBeanDefinitionOverriding()方法

    图片

    Spring 自始至终默认都是允许 BeanDefinition 覆盖的,变的是 Spring Boot , Spring Boot 2.1.0 之前没有覆盖 Spring 的 allowBeanDefinitionOverriding 默认值,仍是允许 BeanDefinition 覆盖的

    Spring Boot 2.1.0 中 SpringApplication 定义了私有属性:allowBeanDefinitionOverriding

    图片

    没有显示的指定值,那么默认值就是 false ,之后在 Spring Boot 启动过程中,会用此值覆盖掉 Spring 中的 allowBeanDefinitionOverriding 的默认值

    关于 allowBeanDefinitionOverriding ,我想大家应该已经清楚了

    
    

  • 为什么 Nginx 比 Apache 更牛叉?

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

    来源:codebay.cn/post/8557.html

    • 一、Apache三种工作模式
      • 1、prefork的工作原理
      • 2、worker的工作原理
      • 3、event 基于事件机制的特性
    • 二、如何提高Web服务器的并发连接处理能力
    • 三、Nginx优异之处
    • 四、Nginx 工作原理
    • 五、Nginx 的诞生主要解决C10K问题
      • 1、select模型:(apache使用,由于受模块等限制,用的不多);
      • 2、poll:poll是unix沿用select自己重新实现了一遍,唯一解决的问题是poll 没有最大文件描述符数量的限制;
      • 3、epoll模型:(Nginx使用)

    Nginx才短短几年,就拿下了Web服务器大壁江山,众所周知,Nginx在处理大并发静态请求方面,效率明显高于Httpd,甚至能轻松解决C10K问题。

    在高并发连接的情况下,Nginx是Apache服务器不错的替代品。Nginx同时也可以作为7层负载均衡服务器来使用。根据我的测试结果,Nginx + PHP(FastCGI) 可以承受3万以上的并发连接数,相当于同等环境下Apache的10倍。

    一般来说,4GB内存的服务器+Apache(prefork模式)一般只能处理3000个并发连接,因为它们将占用3GB以上的内存,还得为系统预留1GB的内存。我曾经就有两台Apache服务器,因为在配置文件中设置的MaxClients为4000,当Apache并发连接数达到3800时,导致服务器内存和Swap空间用满而崩溃。

    而这台 Nginx + PHP(FastCGI) 服务器在3万并发连接下,开启的10个Nginx进程消耗150M内存(15M*10=150M),开启的64个php-cgi进程消耗1280M内存(20M*64=1280M),加上系统自身消耗的内存,总共消耗不到2GB内存。如果服务器内存较小,完全可以只开启25个php-cgi进程,这样php-cgi消耗的总内存数才500M。

    在3万并发连接下,访问Nginx+ PHP(FastCGI) 服务器的PHP程序,仍然速度飞快。

    为什么Nginx在处理高并发方面要优于httpd,我们先从两种web服务器的工作原理以及工作模式说起。

    一、Apache三种工作模式

    我们都知道Apache有三种工作模块,分别为:prefork、worker、event。

    • prefork: 多进程,每个请求用一个进程响应,这个过程会用到select机制来通知。
    • worker: 多线程,一个进程可以生成多个线程,每个线程响应一个请求,但通知机制还是select不过可以接受更多的请求。
    • event: 基于异步I/O模型,一个进程或线程,每个进程或线程响应多个用户请求,它是基于事件驱动(也就是epoll机制)实现的。

    1、prefork的工作原理

    如果不用“–with-mpm”显式指定某种MPM,prefork就是Unix平台上缺省的MPM。它所采用的预派生子进程方式也是 Apache1.3中采用的模式。prefork本身并没有使用到线程,2.0版使用它是为了与1.3版保持兼容性;另一方面,prefork用单独的子进程来处理不同的请求,进程之间是彼此独立的,这也使其成为最稳定的MPM之一。

    2、worker的工作原理

    相对于prefork,worker是2.0版中全新的支持多线程和多进程混合模型的MPM。由于使用线程来处理,所以可以处理相对海量的请求,而系统资源的开销要小于基于进程的服务器。但是,worker也使用了多进程,每个进程又生成多个线程,以获得基于进程服务器的稳定性,这种MPM的工作方 式将是Apache2.0的发展趋势。

    3、event 基于事件机制的特性

    一个进程响应多个用户请求,利用callback机制,让套接字复用,请求过来后进程并不处理请求,而是直接交由其他机制来处理,通过epoll机制来通知请求是否完成;在这个过程中,进程本身一直处于空闲状态,可以一直接收用户请求。可以实现一个进程程响应多个用户请求。支持持海量并发连接数,消耗更少的资源。

    二、如何提高Web服务器的并发连接处理能力

    有几个基本条件:

    1、基于线程,即一个进程生成多个线程,每个线程响应用户的每个请求。

    2、基于事件的模型,一个进程处理多个请求,并且通过epoll机制来通知用户请求完成。

    3、基于磁盘的AIO(异步I/O)

    4、支持mmap内存映射,mmap传统的web服务器,进行页面输入时,都是将磁盘的页面先输入到内核缓存中,再由内核缓存中复制一份到web服务器上,mmap机制就是让内核缓存与磁盘进行映射,web服务器,直接复制页面内容即可。不需要先把磁盘的上的页面先输入到内核缓存去。

    刚好,Nginx 支持以上所有特性。所以Nginx官网上说,Nginx支持50000并发,是有依据的。

    三、Nginx优异之处

    传统上基于进程或线程模型架构的Web服务通过每进程或每线程处理并发连接请求,这势必会在网络和I/O操作时产生阻塞,其另一个必然结果则是对内存或CPU的利用率低下。

    生成一个新的进程/线程需要事先备好其运行时环境,这包括为其分配堆内存和栈内存,以及为其创建新的执行上下文等。这些操作都需要占用CPU,而且过多的进程/线程还会带来线程抖动或频繁的上下文切换,系统性能也会由此进一步下降。

    另一种高性能web服务器/Web服务器反向代理:Nginx,Nginx的主要着眼点就是其高性能以及对物理计算资源的高密度利用,因此其采用了不同的架构模型。受启发于多种操作系统设计中基于“事件”的高级处理机制,Nginx采用了模块化、事件驱动、异步、单线程及非阻塞的架构,并大量采用了多路复用及事件通知机制。

    在Nginx中,连接请求由为数不多的几个仅包含一个线程的进程Worker以高效的回环(run-loop)机制进行处理,而每个Worker可以并行处理数千个的并发连接及请求。

    四、Nginx 工作原理

    Nginx会按需同时运行多个进程:一个主进程(master)和几个工作进程(worker),配置了缓存时还会有缓存加载器进程(cache loader)和缓存管理器进程(cache manager)等。所有进程均是仅含有一个线程,并主要通过“共享内存”的机制实现进程间通信。主进程以root用户身份运行,而worker、cache loader和cache manager均应以非特权用户身份运行。

    在高连接并发的情况下,Nginx是Apache服务器不错的替代品。

    Nginx 安装非常的简单 , 配置文件非常简洁(还能够支持perl语法),Bugs 非常少的服务器: Nginx 启动特别容易, 并且几乎可以做到7*24不间断运行,即使运行数个月也不需要重新启动. 你还能够 不间断服务的情况下进行软件版本的升级 。

    五、Nginx 的诞生主要解决C10K问题

    最后我们从各自使用的多路复用IO模型来分析:

    1、select模型:(apache使用,由于受模块等限制,用的不多);

    单个进程能够 监视的文件描述符的数量存在最大限制;

    select()所维护的 存储大量文件描述符的数据结构 ,随着文件描述符数量的增长,其在用户态和内核的地址空间的复制所引发的开销也会线性增长;

    由于网络响应时间的延迟使得大量TCP连接处于非活跃状态,但调用select()还是会对 所有的socket进行一次线性扫描 ,会造成一定的开销;

    2、poll:poll是unix沿用select自己重新实现了一遍,唯一解决的问题是poll 没有最大文件描述符数量的限制;

    3、epoll模型:(Nginx使用)

    epoll带来了两个优势,大幅度提升了性能:

    (1)基于事件的就绪通知方式 ,select/poll方式,进程只有在调用一定的方法后,内核才会对所有监视的文件描述符进行扫描,而epoll事件通过epoll_ctl()注册一个文件描述符,一旦某个文件描述符就绪时,内核会采用类似call back的回调机制,迅速激活这个文件描述符,epoll_wait()便会得到通知

    (2)调用一次epoll_wait()获得就绪文件描述符时,返回的并不是实际的描述符,而是一个代表就绪描述符数量的值,拿到这些值去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里使用内存映射(mmap)技术, 避免了复制大量文件描述符带来的开销

    (3)当然epoll也有一定的局限性, epoll只有Linux2.6才有实现 ,而其他平台都没有,这和apache这种优秀的跨平台服务器,显然是有些背道而驰了。

    (4)简单来说epoll是select的升级版,单进程管理的文件描述符没有最大限制。但epoll只有linux平台可使用。作为跨平台的Apache没有使用

    
    

  • “12306” 是如何支撑百万 QPS 的?

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

    文章来源:https://juejin.cn/post/6844903949632274445


    目录

    • 12306抢票,极限并发带来的思考?
    • 1. 大型高并发系统架构
    • 2.秒杀抢购系统选型
    • 3. 扣库存的艺术
    • 4. 代码演示
    • 5.总结回顾



    12306抢票,极限并发带来的思考?





    每到节假日期间,一二线城市返乡、外出游玩的人们几乎都面临着一个问题:抢火车票!虽然现在大多数情况下都能订到票,但是放票瞬间即无票的场景,相信大家都深有体会。尤其是春节期间,大家不仅使用12306,还会考虑“智行”和其他的抢票软件,全国上下几亿人在这段时间都在抢票。

    “12306服务”承受着这个世界上任何秒杀系统都无法超越的QPS,上百万的并发再正常不过了!笔者专门研究了一下“12306”的服务端架构,学习到了其系统设计上很多亮点,在这里和大家分享一下并模拟一个例子:如何在100万人同时抢1万张火车票时,系统提供正常、稳定的服务。


    github代码地址:https://github.com/GuoZhaoran/spikeSystem




    1. 大型高并发系统架构





    高并发的系统架构都会采用分布式集群部署,服务上层有着层层负载均衡,并提供各种容灾手段(双火机房、节点容错、服务器灾备等)保证系统的高可用,流量也会根据不同的负载能力和配置策略均衡到不同的服务器上。下边是一个简单的示意图:


    1.1 负载均衡简介

    上图中描述了用户请求到服务器经历了三层的负载均衡,下边分别简单介绍一下这三种负载均衡:

    (1)OSPF(开放式最短链路优先)是一个内部网关协议(Interior Gateway Protocol,简称IGP)。OSPF通过路由器之间通告网络接口的状态来建立链路状态数据库,生成最短路径树,OSPF会自动计算路由接口上的Cost值,但也可以通过手工指定该接口的Cost值,手工指定的优先于自动计算的值。OSPF计算的Cost,同样是和接口带宽成反比,带宽越高,Cost值越小。到达目标相同Cost值的路径,可以执行负载均衡,最多6条链路同时执行负载均衡。

    (2)LVS (Linux VirtualServer),它是一种集群(Cluster)技术,采用IP负载均衡技术和基于内容请求分发技术。调度器具有很好的吞吐率,将请求均衡地转移到不同的服务器上执行,且调度器自动屏蔽掉服务器的故障,从而将一组服务器构成一个高性能的、高可用的虚拟服务器。

    (3)Nginx想必大家都很熟悉了,是一款非常高性能的http代理/反向代理服务器,服务开发中也经常使用它来做负载均衡。Nginx实现负载均衡的方式主要有三种:轮询、加权轮询、ip hash轮询,下面我们就针对Nginx的加权轮询做专门的配置和测试

    1.2 Nginx加权轮询的演示

    Nginx实现负载均衡通过upstream模块实现,其中加权轮询的配置是可以给相关的服务加上一个权重值,配置的时候可能根据服务器的性能、负载能力设置相应的负载。下面是一个加权轮询负载的配置,我将在本地的监听3001-3004端口,分别配置1,2,3,4的权重:

    #配置负载均衡
        upstream load_rule {
           server 127.0.0.1:3001 weight=1;
           server 127.0.0.1:3002 weight=2;
           server 127.0.0.1:3003 weight=3;
           server 127.0.0.1:3004 weight=4;
        }
        ...
        server {
        listen       80;
        server_name  load_balance.com www.load_balance.com;
        location / {
           proxy_pass http://load_rule;
        }
    }

    我在本地/etc/hosts目录下配置了 www.load_balance.com的虚拟域名地址,接下来使用Go语言开启四个http端口监听服务,下面是监听在3001端口的Go程序,其他几个只需要修改端口即可:

    package main

    import (
     "net/http"
     "os"
     "strings"
    )


    func main() 
    {
     http.HandleFunc("/buy/ticket", handleReq)
     http.ListenAndServe(":3001", nil)
    }

    //处理请求函数,根据请求将响应结果信息写入日志
    func handleReq(w http.ResponseWriter, r *http.Request) {
     failedMsg :=  "handle in port:"
     writeLog(failedMsg, "./stat.log")
    }

    //写入日志
    func writeLog(msg string, logPath string) {
     fd, _ := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
     defer fd.Close()
     content := strings.Join([]string{msg, "rn"}, "3001")
     buf := []byte(content)
     fd.Write(buf)
    }

    我将请求的端口日志信息写到了./stat.log文件当中,然后使用ab压测工具做压测:

    ab -n 1000 -c 100 http://www.load_balance.com/buy/ticket

    统计日志中的结果,3001-3004端口分别得到了100、200、300、400的请求量,这和我在nginx中配置的权重占比很好的吻合在了一起,并且负载后的流量非常的均匀、随机。



    2.秒杀抢购系统选型





    回到我们最初提到的问题中来:火车票秒杀系统如何在高并发情况下提供正常、稳定的服务呢?


    从上面的介绍我们知道用户秒杀流量通过层层的负载均衡,均匀到了不同的服务器上,即使如此,集群中的单机所承受的QPS也是非常高的。如何将单机性能优化到极致呢?要解决这个问题,我们就要想明白一件事:通常订票系统要处理生成订单、减扣库存、用户支付这三个基本的阶段,我们系统要做的事情是要保证火车票订单不超卖、不少卖 ,每张售卖的车票都必须支付才有效,还要保证系统承受极高的并发。这三个阶段的先后顺序改怎么分配才更加合理呢?我们来分析一下:

    2.1 下单减库存

    当用户并发请求到达服务端时,首先创建订单,然后扣除库存,等待用户支付。这种顺序是我们一般人首先会想到的解决方案,这种情况下也能保证订单不会超卖,因为创建订单之后就会减库存,这是一个原子操作。但是这样也会产生一些问题,第一就是在极限并发情况下,任何一个内存操作的细节都至关影响性能,尤其像创建订单这种逻辑,一般都需要存储到磁盘数据库的,对数据库的压力是可想而知的;第二是如果用户存在恶意下单的情况,只下单不支付这样库存就会变少,会少卖很多订单,虽然服务端可以限制IP和用户的购买订单数量,这也不算是一个好方法。

    2.2 支付减库存

    如果等待用户支付了订单在减库存,第一感觉就是不会少卖。但是这是并发架构的大忌,因为在极限并发情况下,用户可能会创建很多订单,当库存减为零的时候很多用户发现抢到的订单支付不了了,这也就是所谓的“超卖”。也不能避免并发操作数据库磁盘IO

    2.3 预扣库存

    从上边两种方案的考虑,我们可以得出结论:只要创建订单,就要频繁操作数据库IO。那么有没有一种不需要直接操作数据库IO的方案呢,这就是预扣库存。先扣除了库存,保证不超卖,然后异步生成用户订单,这样响应给用户的速度就会快很多;那么怎么保证不少卖呢?用户拿到了订单,不支付怎么办?

    我们都知道现在订单都有有效期,比如说用户五分钟内不支付,订单就失效了,订单一旦失效,就会加入新的库存,这也是现在很多网上零售企业保证商品不少卖采用的方案。订单的生成是异步的,一般都会放到MQ、kafka这样的即时消费队列中处理,订单量比较少的情况下,生成订单非常快,用户几乎不用排队。



    3. 扣库存的艺术





    从上面的分析可知,显然预扣库存的方案最合理。我们进一步分析扣库存的细节,这里还有很大的优化空间,库存存在哪里?怎样保证高并发下,正确的扣库存,还能快速的响应用户请求?

    在单机低并发情况下,我们实现扣库存通常是这样的:

    为了保证扣库存和生成订单的原子性,需要采用事务处理,然后取库存判断、减库存,最后提交事务,整个流程有很多IO,对数据库的操作又是阻塞的。这种方式根本不适合高并发的秒杀系统。

    接下来我们对单机扣库存的方案做优化:本地扣库存 。我们把一定的库存量分配到本地机器,直接在内存中减库存,然后按照之前的逻辑异步创建订单。改进过之后的单机系统是这样的:

    这样就避免了对数据库频繁的IO操作,只在内存中做运算,极大的提高了单机抗并发的能力。但是百万的用户请求量单机是无论如何也抗不住的,虽然nginx处理网络请求使用epoll模型,c10k的问题在业界早已得到了解决。但是linux系统下,一切资源皆文件,网络请求也是这样,大量的文件描述符会使操作系统瞬间失去响应。

    上面我们提到了nginx的加权均衡策略,我们不妨假设将100W的用户请求量平均均衡到100台服务器上,这样单机所承受的并发量就小了很多。然后我们每台机器本地库存100张火车票,100台服务器上的总库存还是1万,这样保证了库存订单不超卖,下面是我们描述的集群架构:

    问题接踵而至,在高并发情况下,现在我们还无法保证系统的高可用,假如这100台服务器上有两三台机器因为扛不住并发的流量或者其他的原因宕机了。那么这些服务器上的订单就卖不出去了,这就造成了订单的少卖。

    要解决这个问题,我们需要对总订单量做统一的管理,这就是接下来的容错方案。服务器不仅要在本地减库存,另外要远程统一减库存 。有了远程统一减库存的操作,我们就可以根据机器负载情况,为每台机器分配一些多余的“buffer库存”用来防止机器中有机器宕机的情况。我们结合下面架构图具体分析一下:

    我们采用Redis存储统一库存,因为Redis的性能非常高,号称单机QPS能抗10W的并发。在本地减库存以后,如果本地有订单,我们再去请求redis远程减库存,本地减库存和远程减库存都成功了,才返回给用户抢票成功的提示,这样也能有效的保证订单不会超卖。

    当机器中有机器宕机时,因为每个机器上有预留的buffer余票,所以宕机机器上的余票依然能够在其他机器上得到弥补,保证了不少卖。buffer余票设置多少合适呢,理论上buffer设置的越多,系统容忍宕机的机器数量就越多,但是buffer设置的太大也会对redis造成一定的影响。

    虽然redis内存数据库抗并发能力非常高,请求依然会走一次网络IO,其实抢票过程中对redis的请求次数是本地库存和buffer库存的总量,因为当本地库存不足时,系统直接返回用户“已售罄”的信息提示,就不会再走统一扣库存的逻辑,这在一定程度上也避免了巨大的网络请求量把redis压跨,所以buffer值设置多少,需要架构师对系统的负载能力做认真的考量。



    4. 代码演示





    Go语言原生为并发设计,我采用go语言给大家演示一下单机抢票的具体流程。

    4.1 初始化工作

    go包中的init函数先于main函数执行,在这个阶段主要做一些准备性工作。我们系统需要做的准备工作有:初始化本地库存、初始化远程redis存储统一库存的hash键值、初始化redis连接池;另外还需要初始化一个大小为1的int类型chan,目的是实现分布式锁的功能,也可以直接使用读写锁或者使用redis等其他的方式避免资源竞争,但使用channel更加高效,这就是go语言的哲学:不要通过共享内存来通信,而要通过通信来共享内存 。redis库使用的是redigo,下面是代码实现:

    ...
    //localSpike包结构体定义
    package localSpike

    type LocalSpike struct {
     LocalInStock     int64
     LocalSalesVolume int64
    }
    ...
    //remoteSpike对hash结构的定义和redis连接池
    package remoteSpike
    //远程订单存储健值
    type RemoteSpikeKeys struct {
     SpikeOrderHashKey string //redis中秒杀订单hash结构key
     TotalInventoryKey string //hash结构中总订单库存key
     QuantityOfOrderKey string //hash结构中已有订单数量key
    }

    //初始化redis连接池
    func NewPool() *redis.Pool {
     return &redis.Pool{
      MaxIdle:   10000,
      MaxActive: 12000// max number of connections
      Dial: func() (redis.Conn, error) {
       c, err := redis.Dial("tcp"":6379")
       if err != nil {
        panic(err.Error())
       }
       return c, err
      },
     }
    }
    ...
    func init() {
     localSpike = localSpike2.LocalSpike{
      LocalInStock:     150,
      LocalSalesVolume: 0,
     }
     remoteSpike = remoteSpike2.RemoteSpikeKeys{
      SpikeOrderHashKey:  "ticket_hash_key",
      TotalInventoryKey:  "ticket_total_nums",
      QuantityOfOrderKey: "ticket_sold_nums",
     }
     redisPool = remoteSpike2.NewPool()
     done = make(chan int1)
     done 1
    }

    4.2 本地扣库存和统一扣库存

    本地扣库存逻辑非常简单,用户请求过来,添加销量,然后对比销量是否大于本地库存,返回bool值:

    package localSpike
    //本地扣库存,返回bool值
    func (spike *LocalSpike) LocalDeductionStock() bool{
     spike.LocalSalesVolume = spike.LocalSalesVolume + 1
     return spike.LocalSalesVolume }

    注意这里对共享数据LocalSalesVolume的操作是要使用锁来实现的,但是因为本地扣库存和统一扣库存是一个原子性操作,所以在最上层使用channel来实现,这块后边会讲。统一扣库存操作redis,因为redis是单线程的,而我们要实现从中取数据,写数据并计算一些列步骤,我们要配合lua脚本打包命令,保证操作的原子性:

    package remoteSpike
    ......
    const LuaScript = `
            local ticket_key = KEYS[1]
            local ticket_total_key = ARGV[1]
            local ticket_sold_key = ARGV[2]
            local ticket_total_nums = tonumber(redis.call('HGET', ticket_key, ticket_total_key))
            local ticket_sold_nums = tonumber(redis.call('HGET', ticket_key, ticket_sold_key))
      -- 查看是否还有余票,增加订单数量,返回结果值
           if(ticket_total_nums >= ticket_sold_nums) then
                return redis.call('HINCRBY', ticket_key, ticket_sold_key, 1)
            end
            return 0
    `
    //远端统一扣库存
    func (RemoteSpikeKeys *RemoteSpikeKeys) RemoteDeductionStock(conn redis.Conn) bool {
     lua := redis.NewScript(1, LuaScript)
     result, err := redis.Int(lua.Do(conn, RemoteSpikeKeys.SpikeOrderHashKey, RemoteSpikeKeys.TotalInventoryKey, RemoteSpikeKeys.QuantityOfOrderKey))
     if err != nil {
      return false
     }
     return result != 0
    }

    我们使用hash结构存储总库存和总销量的信息,用户请求过来时,判断总销量是否大于库存,然后返回相关的bool值。在启动服务之前,我们需要初始化redis的初始库存信息:

     hmset ticket_hash_key "ticket_total_nums" 10000 "ticket_sold_nums" 0

    4.3 响应用户信息

    我们开启一个http服务,监听在一个端口上:

    package main
    ...
    func main() {
     http.HandleFunc("/buy/ticket", handleReq)
     http.ListenAndServe(":3005", nil)
    }

    上面我们做完了所有的初始化工作,接下来handleReq的逻辑非常清晰,判断是否抢票成功,返回给用户信息就可以了。

    package main
    //处理请求函数,根据请求将响应结果信息写入日志
    func handleReq(w http.ResponseWriter, r *http.Request) {
     redisConn := redisPool.Get()
     LogMsg := ""
      //全局读写锁
     if localSpike.LocalDeductionStock() && remoteSpike.RemoteDeductionStock(redisConn) {
      util.RespJson(w, 1,  "抢票成功", nil)
      LogMsg = LogMsg + "result:1,localSales:" + strconv.FormatInt(localSpike.LocalSalesVolume, 10)
     } else {
      util.RespJson(w, -1"已售罄", nil)
      LogMsg = LogMsg + "result:0,localSales:" + strconv.FormatInt(localSpike.LocalSalesVolume, 10)
     }
     done 1

     //将抢票状态写入到log中
     writeLog(LogMsg, "./stat.log")
    }

    func writeLog(msg string, logPath string) {
     fd, _ := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
     defer fd.Close()
     content := strings.Join([]string{msg, "rn"}, "")
     buf := []byte(content)
     fd.Write(buf)
    }

    前边提到我们扣库存时要考虑竞态条件,我们这里是使用channel避免并发的读写,保证了请求的高效顺序执行。我们将接口的返回信息写入到了./stat.log文件方便做压测统计。

    4.4 单机服务压测

    开启服务,我们使用ab压测工具进行测试:

    ab -n 10000 -c 100 http://127.0.0.1:3005/buy/ticket

    下面是我本地低配mac的压测信息

    This is ApacheBench, Version 2.3 1826891 $>
    Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
    Licensed to The Apache Software Foundation, http://www.apache.org/

    Benchmarking 127.0.0.1 (be patient)
    Completed 1000 requests
    Completed 2000 requests
    Completed 3000 requests
    Completed 4000 requests
    Completed 5000 requests
    Completed 6000 requests
    Completed 7000 requests
    Completed 8000 requests
    Completed 9000 requests
    Completed 10000 requests
    Finished 10000 requests


    Server Software:
    Server Hostname:        127.0.0.1
    Server Port:            3005

    Document Path:          /buy/ticket
    Document Length:        29 bytes

    Concurrency Level:      100
    Time taken for tests:   2.339 seconds
    Complete requests:      10000
    Failed requests:        0
    Total transferred:      1370000 bytes
    HTML transferred:       290000 bytes
    Requests per second:    4275.96 [#/sec] (mean)
    Time per request:       23.387 [ms] (mean)
    Time per request:       0.234 [ms] (mean, across all concurrent requests)
    Transfer rate:          572.08 [Kbytes/sec] received

    Connection Times (ms)
                  min  mean[+/-sd] median   max
    Connect:        0    8  14.7      6     223
    Processing:     2   15  17.6     11     232
    Waiting:        1   11  13.5      8     225
    Total:          7   23  22.8     18     239

    Percentage of the requests served within a certain time (ms)
      50%     18
      66%     24
      75%     26
      80%     28
      90%     33
      95%     39
      98%     45
      99%     54
     100%    239 (longest request)

    根据指标显示,我单机每秒就能处理4000+的请求,正常服务器都是多核配置,处理1W+的请求根本没有问题。而且查看日志发现整个服务过程中,请求都很正常,流量均匀,redis也很正常:

    //stat.log
    ...
    result:1,localSales:145
    result:1,localSales:146
    result:1,localSales:147
    result:1,localSales:148
    result:1,localSales:149
    result:1,localSales:150
    result:0,localSales:151
    result:0,localSales:152
    result:0,localSales:153
    result:0,localSales:154
    result:0,localSales:156
    ...


    5.总结回顾





    总体来说,秒杀系统是非常复杂的。我们这里只是简单介绍模拟了一下单机如何优化到高性能,集群如何避免单点故障,保证订单不超卖、不少卖的一些策略,完整的订单系统还有订单进度的查看,每台服务器上都有一个任务,定时的从总库存同步余票和库存信息展示给用户,还有用户在订单有效期内不支付,释放订单,补充到库存等等。

    我们实现了高并发抢票的核心逻辑,可以说系统设计的非常的巧妙,巧妙的避开了对DB数据库IO的操作,对Redis网络IO的高并发请求,几乎所有的计算都是在内存中完成的,而且有效的保证了不超卖、不少卖,还能够容忍部分机器的宕机。我觉得其中有两点特别值得学习总结:

    (1)负载均衡,分而治之。通过负载均衡,将不同的流量划分到不同的机器上,每台机器处理好自己的请求,将自己的性能发挥到极致,这样系统的整体也就能承受极高的并发了,就像工作的的一个团队,每个人都将自己的价值发挥到了极致,团队成长自然是很大的。

    (2)合理的使用并发和异步。自epoll网络架构模型解决了c10k问题以来,异步越来被服务端开发人员所接受,能够用异步来做的工作,就用异步来做,在功能拆解上能达到意想不到的效果,这点在nginx、node.js、redis上都能体现,他们处理网络请求使用的epoll模型,用实践告诉了我们单线程依然可以发挥强大的威力。服务器已经进入了多核时代,go语言这种天生为并发而生的语言,完美的发挥了服务器多核优势,很多可以并发处理的任务都可以使用并发来解决,比如go处理http请求时每个请求都会在一个goroutine中执行,总之:怎样合理的压榨CPU,让其发挥出应有的价值,是我们一直需要探索学习的方向。

  • 太牛了,前后端完全开源,快速搭建简约美观的在线可互动的教室,星标4.5k!

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

    Agora Flat开源教室是一款高效实用且专业的互动课堂软件,Agora Flat开源教室能够帮助用户快速搭建简约的互动教室,实现网上学习和授课等功能,提高教学效率。

    Agora Flat开源教室客户端支持大小课班和一对一的模式,用户可以多情景灵便转换,进行优质课堂教学。

    特性

    • 实时交互:多功能互动白板,实时音视频(RTC)通讯,即时消息(RTM)聊天
    • 登录方式:微信,GitHub
    • 房间管理:加入、创建、预定房间,支持周期性房间
    • 课堂录制回放:白板信令回放,音视频云录制回放,群聊信令回放
    • 多媒体课件云盘
    • 屏幕共享

    功能

    轻松创建丰富多样的在线教室

    • 大班课

    适用于大型课堂授课,知识传递效率更高。学生可举手发言参与在线互动。

    • 小班课

    适用于 1 名教师和 2~16 名学生进行在线教学,互动性更及时,趣味性更高,提高教学效率的同时保证质量。

    • 一对一

    适用于 1 名老师对 1 名学生,沉浸式的个性化课堂教学,学习效果更加明显。

    支持自定义主题和 UI

    场景插件:无限的组合,无限的可能

    安装运行

    快速上手

    你可以在没有服务端的情况下构建并运行 Flat 客户端。此仓库包含以下项目:

    Flat Electron 客户端

    https://github.com/netless-io/flat/tree/main/desktop

    Flat Web 客户端

    https://github.com/netless-io/flat/tree/main/web

    安装

    如果你还没有安装 pnpm:

    npm i -g pnpm

    Clone 或者 fork 这个项目,在根目录执行:

    pnpm i

    构建并运行 Flat Electron 客户端

    在仓库根目录运行以下命令:

    pnpm start

    你可以运行以下命令将项目打包成可执行文件:

    • 项目根目录执行 pnpm ship 将根据当前系统打包。
    • 或者项目根目录执行 pnpm ship:macpnpm ship:win 可针对指定的系统打包。

    如果你因为网络问题导致无法下载 electron,可在项目目录新建: .npmrc 文件,并写入 ELECTRON_MIRROR=" https://npmmirror.com/mirrors/electron/" 内容。重新执行 pnpm i 即可。

    构建并运行 Flat Web 客户端

    在仓库根目录运行以下任意一个命令:

    pnpm start:web
    cd ./web/flat-web/ && pnpm start

    在 Flat 中 UI 逻辑与业务逻辑分开开发。可以通过 Storybook 快速查看与开发部分 UI。在项目根目录执行 pnpm storybook 在本地运行 Storybook。

    相关项目

    Flat 安卓客户端

    • https://github.com/netless-io/flat-android

    Flat 服务端

    • https://github.com/netless-io/flat-server

    Flat 主页

    • https://flat.whiteboard.agora.io/#download

    传送门

    • 开源地址:https://github.com/netless-io/flat

    整理:骑猪看星星

    来源:开源技术专栏

  • 牛逼哄哄的 BitMap,到底牛逼在哪?

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

    文章来源:https://www.cnblogs.com/cjsblog/p/11613708.html


    BitMap


    Bit-map的基本思想就是用一个bit位来标记某个元素对应的Value,而Key即是该元素。由于采用了Bit为单位来存储数据,因此在存储空间方面,可以大大节省。(PS:划重点 节省存储空间 )

    假设有这样一个需求:在20亿个随机整数中找出某个数m是否存在其中,并假设32位操作系统,4G内存

    在Java中,int占4字节,1字节=8位(1 byte = 8 bit)

    如果每个数字用int存储,那就是20亿个int,因而占用的空间约为 (2000000000*4/1024/1024/1024)≈7.45 G

    如果按位存储就不一样了,20亿个数就是20亿位,占用空间约为 (2000000000/8/1024/1024/1024)≈0.233 G

    高下立判,无需多言

    那么,问题来了,如何表示一个数呢?

    刚才说了,每一位表示一个数,0表示不存在,1表示存在,这正符合二进制

    这样我们可以很容易表示{1,2,4,6}这几个数:

    计算机内存分配的最小单位是字节,也就是8位,那如果要表示{12,13,15}怎么办呢?

    当然是在另一个8位上表示了:

    图片

    这样的话,好像变成一个二维数组了

    1个int占32位,那么我们只需要申请一个int数组长度为 int tmp[1+N/32] 即可存储,其中N表示要存储的这些数中的最大值,于是乎:

    tmp[0]:可以表示0~31

    tmp[1]:可以表示32~63

    tmp[2]:可以表示64~95

    。。。

    如此一来,给定任意整数M,那么M/32就得到下标,M%32就知道它在此下标的哪个位置


    添加


    这里有个问题,我们怎么把一个数放进去呢?例如,想把5这个数字放进去,怎么做呢?

    首先,5/32=0,5%32=5,也是说它应该在tmp[0]的第5个位置,那我们把1向左移动5位,然后按位或

    换成二进制就是

    这就相当于 86 | 32 = 118

    86 | (1

    b[0] = b[0] | (1

    也就是说,要想插入一个数,将1左移带代表该数字的那一位,然后与原数进行按位或操作

    化简一下,就是 86 + (5/8) | (1

    因此,公式可以概括为:p + (i/8)|(1


    清除


    以上是添加,那如果要清除该怎么做呢?

    还是上面的例子,假设我们要6移除,该怎么做呢?

    从图上看,只需将该数所在的位置为0即可

    1左移6位,就到达6这个数字所代表的位,然后按位取反,最后与原数按位与,这样就把该位置为0了

    b[0] = b[0] & (~(1

    b[0] = b[0] & (~(1


    查找


    前面我们也说了,每一位代表一个数字,1表示有(或者说存在),0表示无(或者说不存在)。通过把该为置为1或者0来达到添加和清除的小伙,那么判断一个数存不存在就是判断该数所在的位是0还是1

    假设,我们想知道3在不在,那么只需判断 b[0] & (1


    Bitmap有什么用


    量数据的快速排序、查找、去重



    快速排序


    假设我们要对0-7内的5个元素(4,7,2,5,3)排序(这里假设这些元素没有重复),我们就可以采用Bit-map的方法来达到排序的目的。

    要表示8个数,我们就只需要8个Bit(1Bytes),首先我们开辟1Byte的空间,将这些空间的所有Bit位都置为0,然后将对应位置为1。

    最后,遍历一遍Bit区域,将该位是一的位的编号输出(2,3,4,5,7),这样就达到了排序的目的,时间复杂度O(n)。

    优点:

    • 运算效率高,不需要进行比较和移位;
    • 占用内存少,比如N=10000000;只需占用内存为N/8=1250000Byte=1.25M

    缺点:

    • 所有的数据不能重复。即不可对重复的数据进行排序和查找。
    • 只有当数据比较密集时才有优势

    快速去重


    20亿个整数中找出不重复的整数的个数,内存不足以容纳这20亿个整数。

    首先,根据“内存空间不足以容纳这05亿个整数”我们可以快速的联想到Bit-map。下边关键的问题就是怎么设计我们的Bit-map来表示这20亿个数字的状态了。其实这个问题很简单,一个数字的状态只有三种,分别为不存在,只有一个,有重复。因此,我们只需要2bits就可以对一个数字的状态进行存储了,假设我们设定一个数字不存在为00,存在一次01,存在两次及其以上为11。那我们大概需要存储空间2G左右。

    接下来的任务就是把这20亿个数字放进去(存储),如果对应的状态位为00,则将其变为01,表示存在一次;如果对应的状态位为01,则将其变为11,表示已经有一个了,即出现多次;如果为11,则对应的状态位保持不变,仍表示出现多次。

    最后,统计状态位为01的个数,就得到了不重复的数字个数,时间复杂度为O(n)。

    快速查找


    这就是我们前面所说的了,int数组中的一个元素是4字节占32位,那么除以32就知道元素的下标,对32求余数(%32)就知道它在哪一位,如果该位是1,则表示存在。



    小结&回顾


    Bitmap主要用于快速检索关键字状态,通常要求关键字是一个连续的序列(或者关键字是一个连续序列中的大部分), 最基本的情况,使用1bit表示一个关键字的状态(可标示两种状态),但根据需要也可以使用2bit(表示4种状态),3bit(表示8种状态)。

    Bitmap的主要应用场合:表示连续(或接近连续,即大部分会出现)的关键字序列的状态(状态数/关键字个数 越小越好)。

    32位机器上,对于一个整型数,比如int a=1 在内存中占32bit位,这是为了方便计算机的运算。但是对于某些应用场景而言,这属于一种巨大的浪费,因为我们可以用对应的32bit位对应存储十进制的0-31个数,而这就是Bit-map的基本思想。Bit-map算法利用这种思想处理大量数据的排序、查询以及去重。

    补充1


    在数字没有溢出的前提下,对于正数和负数,左移一位都相当于乘以2的1次方,左移n位就相当于乘以2的n次方,右移一位相当于除2,右移n位相当于除以2的n次方。

    >> 右移,相当于除以2的n次方,例如:64>>3 相当于64÷8=8

    ^ 异或,相当于求余数,例如:48^32 相当于 48%32=16

    补充2


    不使用第三方变量,交换两个变量的值

    // 方式一
    a = a + b;
    b = a - b;
    a = a - b;

    // 方式二
    a = a ^ b;
    b = a ^ b;
    a = a ^ b;


    BitSet


    BitSet实现了一个位向量,它可以根据需要增长。每一位都有一个布尔值。一个BitSet的位可以被非负整数索引(PS:意思就是每一位都可以表示一个非负整数)。可以查找、设置、清除某一位。通过逻辑运算符可以修改另一个BitSet的内容。默认情况下,所有的位都有一个默认值false。

    图片
    图片
    图片
    图片
    图片

    可以看到,跟我们前面想的差不多

    用一个long数组来存储,初始长度64,set值的时候首先右移6位(相当于除以64)计算在数组的什么位置,然后更改状态位

    别的看不懂不要紧,看懂这两句就够了:

    int wordIndex = wordIndex(bitIndex);
    words[wordIndex] |= (1L 


    Bloom Filters


    图片

    Bloom filter 是一个数据结构,它可以用来判断某个元素是否在集合内,具有运行快速,内存占用小的特点。

    而高效插入和查询的代价就是,Bloom Filter 是一个基于概率的数据结构:它只能告诉我们一个元素绝对不在集合内或可能在集合内。

    Bloom filter 的基础数据结构是一个 比特向量(可理解为数组)。

    主要应用于大规模数据下不需要精确过滤的场景,如检查垃圾邮件地址,爬虫URL地址去重,解决缓存穿透问题等

    如果想判断一个元素是不是在一个集合里,一般想到的是将集合中所有元素保存起来,然后通过比较确定。链表、树、散列表(哈希表)等等数据结构都是这种思路,但是随着集合中元素的增加,需要的存储空间越来越大;同时检索速度也越来越慢,检索时间复杂度分别是O(n)、O(log n)、O(1)。

    布隆过滤器的原理是,当一个元素被加入集合时,通过 K 个散列函数将这个元素映射成一个位数组(Bit array)中的 K 个点,把它们置为 1 。检索时,只要看看这些点是不是都是1就知道元素是否在集合中;如果这些点有任何一个 0,则被检元素一定不在;如果都是1,则被检元素很可能在(之所以说“可能”是误差的存在)。


    BloomFilter 流程

    1、 首先需要 k 个 hash 函数,每个函数可以把 key 散列成为 1 个整数;

    2、初始化时,需要一个长度为 n 比特的数组,每个比特位初始化为 0;

    3、某个 key 加入集合时,用 k 个 hash 函数计算出 k 个散列值,并把数组中对应的比特位置为 1;

    4、判断某个 key 是否在集合时,用 k 个 hash 函数计算出 k 个散列值,并查询数组中对应的比特位,如果所有的比特位都是1,认为在集合中。

    dependency>
        groupId>com.google.guavagroupId>
        artifactId>guavaartifactId>
        version>28.1-jreversion>
    dependency>

  • 还在只用 RedisTemplate 访问 Redis 吗?这几种方式给你安排上!

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

    • 开始准备
    • RedisTemplate
    • JPA Repository
    • Cache
    • 总结

    开始准备

    开始之前我们需要有Redis安装,我们采用本机Docker运行Redis,主要命令如下

    docker pull redis
    docker run --name my_redis -d -p 6379:6379 redis
    docker exec -it my_redis bash
    redis-cli

    前面两个命令是启动redis docker,后两个是连接到docker,在使用redis-cli 去查看redis里面的内容,主要查看我们存在redis里面的数据。

    RedisTemplate

    我们先从RedisTemplate开始,这个是最好理解的一种方式,我之前在工作中也使用过这种方式,先看代码示例 我们先定义一个POJO类

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public class Book implements Serializable {
        private Long id;
        private String name;
        private String author;
    }

    一个很简单的BOOK类,三个字段:idnameauthor。再来一个RedisTemplate的Bean

    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate template = new RedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

    再定义一个使用这个RedisTemplate的Service类

    public Optional findOneBook(String name) {
        HashOperations hashOperations = redisTemplate.opsForHash();
        if (redisTemplate.hasKey(CACHE) && hashOperations.hasKey(CACHE, name)) {
            log.info("Get book {} from Redis.", name);
            return Optional.of(hashOperations.get(CACHE, name));
        }

        Optional book = bookRepository.getBook(name);
        log.info("Book Found: {}", book);
        if (book.isPresent()) {
            log.info("Put book {} to Redis.", name);
            hashOperations.put(CACHE, name, book.get());
            redisTemplate.expire(CACHE, 10, TimeUnit.MINUTES);
        }
        return book;
    }

    我们使用Hash来存储这个Book信息,在上面的方法中查找书名存不存在Redis中,如果存在就直接返回,如果不存在就去持久化存储中找,找到就再通过Template写入到Redis中, 这是缓存的通用做法。使用起来感觉很方便。

    我们这里为了简单没有使用持久化存储,就硬编码了几条数据,代码如下

    @Repository
    public class BookRepository {
        Map bookMap = new HashMap();
        public BookRepository(){
            bookMap.put("apache kafka", Book.builder()
                    .name("apache kafka").id(1L).author("zhangsan")
                    .build());
            bookMap.put("python", Book.builder()
                    .name("python").id(2L).author("lisi")
                    .build());
        }

        public Optional getBook(String name){
            if(bookMap.containsKey(name)){
                return Optional.of(bookMap.get(name));
            }
            else{
                return Optional.empty();
            }
        }
    }

    我们调用 bookService.findOneBook("python")bookService.findOneBook("apache kafka"); 来把数据写入到换存中

    我们来看下存储在Redis的数据长什么样子。

    127.0.0.1:6379> keys *
    1"xacxedx00x05tx00x04book"
    127.0.0.1:6379> type "xacxedx00x05tx00x04book"
    hash
    127.0.0.1:6379> hgetall "xacxedx00x05tx00x04book"
    1"xacxedx00x05tx00x06python"
    2"xacxedx00x05srx00&com.ken.redistemplatesample.model.Book=x19x96xfbx7fx7fxdaxbex02x00x03Lx00x06authortx00x12Ljava/lang/String;Lx00x02idtx00x10Ljava/lang/Long;Lx00x04nameqx00~x00x01xptx00x04lisisrx00x0ejava.lang.Long;x8bxe4x90xccx8f#xdfx02x00x01Jx00x05valuexrx00x10java.lang.Numberx86xacx95x1dx0bx94xe0x8bx02x00x00xpx00x00x00x00x00x00x00x02tx00x06python"
    3"xacxedx00x05tx00x0capache kafka"
    4"xacxedx00x05srx00&com.ken.redistemplatesample.model.Book=x19x96xfbx7fx7fxdaxbex02x00x03Lx00x06authortx00x12Ljava/lang/String;Lx00x02idtx00x10Ljava/lang/Long;Lx00x04nameqx00~x00x01xptx00bzhangsansrx00x0ejava.lang.Long;x8bxe4x90xccx8f#xdfx02x00x01Jx00x05valuexrx00x10java.lang.Numberx86xacx95x1dx0bx94xe0x8bx02x00x00xpx00x00x00x00x00x00x00x01tx00x0capache kafka"

    我们可以看到数据被存在了key是“xacxedx00x05tx00x04book”的一个Hash表中, Hash里面有两条记录。大家发现一个问题没有?

    就是这个key不是我们想象的用“book”做key,而是多了一串16进制的码, 这是因为RedisTemplate使用了默认的JdkSerializationRedisSerializer 去序列化我们的key和value,如果大家都用Java语言那没有问题, 如果有人用Java语言写,有人用别的语言读,那就有问题,就像我开始的时候用hgetall “book”始终拿不到数据那样。

    RedisTemplate也提供了StringRedisTemplate来方便大家需要使用String来序列化redis里面的数据。简单看下代码

    @Bean
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory)
    {
        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

    public Optional getBookString(String name){
        HashOperations hashOperations = stringRedisTemplate.opsForHash();
        if (stringRedisTemplate.hasKey(STRINGCACHE) && hashOperations.hasKey(STRINGCACHE, name)) {
            log.info("Get book {} from Redis.", name);
            return Optional.of(hashOperations.get(STRINGCACHE, name));
        }

        Optional book = bookRepository.getBook(name);
        log.info("Book Found: {}", book);
        if (book.isPresent()) {
            log.info("Put book {} to Redis.", name);
            hashOperations.put(STRINGCACHE, name, book.get().getAuthor());
            stringRedisTemplate.expire(STRINGCACHE, 10, TimeUnit.MINUTES);
            return Optional.of(book.get().getAuthor());
        }
        return Optional.empty();
    }

    使用上就没有那么方便,你就得自己写需要存的是哪个字段,读出来是哪个字段。

    127.0.0.1:6379> keys *
    1) "string_book"
    127.0.0.1:6379> hgetall string_book
    1) "python"
    2) "lisi"
    3) "apache kafka"
    4) "zhangsan"

    如上图所示,使用客户端读出来看起来就比较清爽一些。也可以看到占用的Size会小很多,我们这个例子相差7倍,如果是数据量大,这个还是比较大的浪费。

    127.0.0.1:6379> keys *
    1) "xacxedx00x05tx00x04book"
    2) "string_book"
    127.0.0.1:6379> memory usage "string_book"
    (integer) 104
    127.0.0.1:6379> memory usage "xacxedx00x05tx00x04book"
    (integer) 712

    JPA Repository

    我们知道使用JPA Repository来访问DataBase的时候,增删改查那样的操作能够很方便的实现,基本就是定义个接口,代码都不用写,Spring就帮我们完成了大部分的工作,那么访问Redis是不是也可以这样呢?答案是肯定的,我们来看代码 首先我们还是定义一个POJO

    @RedisHash(value = "cache-book", timeToLive = 600)
    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public class CacheBook implements Serializable {

        @Id
        private Long userId;

        @Indexed
        private String name;

        private String author;
    }

    这个类与我们上面template上面的类的区别就是我们加了两个注解, 在类开头加了 @RedisHash(value = "cache-book", timeToLive = 600) 在字段上面加了@Id@Indexed 定义一个Repository的接口

    public interface CacheBookRepository extends CrudRepositoryCacheBookLong> {
        Optional findOneByName(String name);
    }

    再定义一个service和上面那个例子template一样,缓存中有就到缓存中拿,没有就到持久化存储中找,并写入缓存

    @Slf4j
    @Service
    public class BookService {
        private static final String CACHE = "repository-book";
        @Autowired
        private CacheBookRepository cacheRepository;

        @Autowired
        private BookRepository bookRepository;

        public Optional findOneBook(String name) {
            Optional optionalCacheBook = cacheRepository.findOneByName(name);
            if(!optionalCacheBook.isPresent())
            {
                Optional book = bookRepository.getBook(name);
                log.info("Book Found: {}", book);
                if (book.isPresent()) {
                    log.info("Put book {} to Redis.", name);
                    cacheRepository.save(book.get());
                }
                return book;
            }
            return optionalCacheBook;
        }
    }

    代码很简单,简单到不敢相信是真的。还是一样,调用这个方法,我们来看存在Redis里面的数据

    127.0.0.1:6379> keys *
    1) "repository-book:2"
    2) "repository-book:2:idx"
    3) "repository-book"
    4) "repository-book:name:apache kafka"
    5) "repository-book:name:python"
    6) "repository-book:1:idx"
    7) "repository-book:1"

    哇,感觉存的内容有些多, 不用怕我们来看下各自存什么数据 首先看最短的一个

    127.0.0.1:6379> smembers repository-book
    1) "1"
    2) "2"

    它里面存的是我们的id所有的value,可以用来判断id是否存在 再来看

    127.0.0.1:6379> hgetall repository-book:2
    1) "_class"
    2) "com.ken.redisrepositorysample.model.CacheBook"
    3) "author"
    4) "lisi"
    5) "name"
    6) "python"
    7) "userId"
    8) "2"

    这个是我们数据存放的地方

    127.0.0.1:6379> smembers repository-book:1:idx
    1) "repository-book:name:apache kafka"
    127.0.0.1:6379> smembers "repository-book:name:apache kafka"
    1) "1"

    另外两个都是set, 存在在它们里面的数据是索引信息。由此可以看出通过JPA Repository 的方式,代码很少,而且存储的数据也很通用,个人觉得是比较理想的访问方法。

    Cache

    我们已经看了两种方式,在访问的时候遵循这样的模式:缓存中有就从缓存中返回,没有就从持久化存储中找,然后写入缓存,这部分代码我也不想自己写,那么Cache就是你的救星。

    我们先看代码 我们这次使用内存数据库H2作为持久化存储, 放一个schema.sql在resouces下面

    drop table t_book if exists;


    create table t_book (
        id bigint auto_increment,
        create_time timestamp,
        update_time timestamp,
        name varchar(255),
        author varchar(200),
        primary key (id)
    );

    insert into t_book (name, author, create_time, update_time) values ('python''zhangsan'now(), now());
    insert into t_book (name, author, create_time, update_time) values ('hadoop''lisi'now(), now());
    insert into t_book (name, author, create_time, update_time) values ('java''wangwu'now(), now());

    然后定义POJO

    @Entity
    @Table(name = "T_BOOK")
    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public class CacheBook implements Serializable {

        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;

        private String name;

        private String author;

        @Column(updatable = false)
        @CreationTimestamp
        private Date createTime;
        @UpdateTimestamp
        private Date updateTime;
    }

    完全是和数据库绑定的代码,和缓存没有任何关系 一个Repository来访问数据库

    public interface BookRepository extends JpaRepositoryCacheBookLong> {
    }

    定义一个service来调用它

    @Slf4j
    @Service
    @CacheConfig(cacheNames = "cache-book")
    public class BookService {

        @Autowired
        private BookRepository bookRepository;

        @Cacheable
        public List findAllCoffee() {
            return bookRepository.findAll();
        }

        @CacheEvict
        public void reloadCoffee() {
        }
    }

    这里就比较关键了,在类上加上了注解 @CacheConfig(cacheNames = "cache-book") 在方法上面加上了Cacheable和CacheEvict, Cacheable这个方法就是用来实现逻辑,有就从缓存中拿,没有就从数据库拿的,CacheEvict是调用这个方法的时候清除缓存。

    然后再启动入口程序的地方加上注解 @EnableJpaRepositories @EnableCaching(proxyTargetClass = true) 在配置文件application.properties中加上

    spring.jpa.hibernate.ddl-auto=none
    spring.jpa.properties.hibernate.show_sql=true
    spring.jpa.properties.hibernate.format_sql=true

    management.endpoints.web.exposure.include=*

    spring.cache.type=redis
    spring.cache.cache-names=cache-book
    spring.cache.redis.time-to-live=600000
    spring.cache.redis.cache-null-values=false

    spring.redis.host=localhost

    这样就可以了, 感觉就是通过配置下就把缓存给完成了,非常的简单 我们来看Redis中是怎么存的

    127.0.0.1:6379> keys *
    1) "cache-book::SimpleKey []"
    127.0.0.1:6379> get "cache-book::SimpleKey []"
    "xacxedx00x05srx00x13java.util.ArrayListxx81xd2x1dx99xc7ax9dx03x00x01Ix00x04sizexpx00x00x00x03wx04x00x00x00x03srx00(com.ken.rediscachesample.model.CacheBookxecxcbR=xe1Ux9bxf7x02x00x05Lx00x06authortx00x12Ljava/lang/String;Lx00ncreateTimetx00x10Ljava/util/Date;Lx00x02idtx00x10Ljava/lang/Long;Lx00x04nameqx00~x00x03Lx00nupdateTimeqx00~x00x04xptx00bzhangsansrx00x12java.sql.Timestamp&x18xd5xc8x01Sxbfex02x00x01Ix00x05nanosxrx00x0ejava.util.Datehjx81x01KYtx19x03x00x00xpwbx00x00x01x84xff]x85xb0xb-x81x80srx00x0ejava.lang.Long;x8bxe4x90xccx8f#xdfx02x00x01Jx00x05valuexrx00x10java.lang.Numberx86xacx95x1dx0bx94xe0x8bx02x00x00xpx00x00x00x00x00x00x00x01tx00x06pythonsqx00~x00bwbx00x00x01x84xff]x85xb0xb-x81x80sqx00~x00x02tx00x04lisisqx00~x00bwbx00x00x01x84xff]x85xb0xb

    看到没有,就是当成Redis里面的String来存的, 如果数据量比较小,那是非常的方便,如果数据量大,这种方式就有些问题了。

    总结

    我们看了这三种方式,这里仅仅是做了个入门,每个里面都有很多细节的地方需要去研究和使用,整体的感觉是要想使用的简单,那么存储在Redis中的数据就要量少,量大后,就需要自己来定制了,那基本上要用RedisTemplate来做一些工作。

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

    来源:https://juejin.cn/post/7180290294481420348

    
    

  • 快速交付神器:阿里巴巴官方低代码引擎开源了!

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

    来源:开源技术专栏(ID:GitHubKY)p

    LowCodeEngine是由阿里巴巴钉钉团队开源的低代码引擎, 该引擎全面遵循《阿里巴巴中后端前端基础构建协议规范》和《阿里巴巴中后端前端素材协议规范》。兼容主流浏览器:Chrome >= 80Edge >= 80safari 和 firefox 最近 2 个 版本

    特性

    • 🌈 提炼自企业级低代码平台的面向扩展设计的内核引擎,奉行最小内核,最强生态的设计理念
    • 📦 开箱即用的高质量生态元素,包括 物料体系、设置器、插件 等
    • ⚙️ 完善的工具链,支持 物料体系、设置器、插件 等生态元素的全链路研发周期
    • 🔌 强大的扩展能力,已支撑 100+ 个各种类型低代码平台
    • 🛡 使用 TypeScript 开发,提供完整的类型定义文件

    擎协议

    引擎完整实现了《低代码引擎搭建协议规范》 和《低代码引擎物料协议规范》 ,协议栈是低代码领域的物料能否流通的关键部分。

    使用示例

    npm install @alilc/lowcode-engine --save-dev

    TIPS:仅支持 cdn 方式引入,npm 包用于提供 typings 等代码提示能力

    import { init, skeleton } from '@alilc/lowcode-engine';  
      
    skeleton.add({  
      area'topArea',  
      type'Widget',  
      name'logo',  
      content: YourFantaticLogo,  
      contentProps: {  
        logo:  
          'https://img.alicdn.com/tfs/TB1_SocGkT2gK0jSZFkXXcIQFXa-66-66.png',  
        href'/',  
      },  
      props: {  
        align'left',  
        width100,  
      },  
    });  
      
    init(document.getElementById('lce'));

    工程化配置:

    {  
      "externals": {  
        "@alilc/lowcode-engine""var window.AliLowCodeEngine",  
        "@alilc/lowcode-engine-ext""var window.AliLowCodeEngineExt"  
      }  
    }

    cdn 可选方式:

    方式 1(推荐):alifd cdn

    https://alifd.alicdn.com/npm/@alilc/lowcode-engine@1.0.0/dist/js/engine-core.js  
      
    https://alifd.alicdn.com/npm/@alilc/lowcode-react-simulator-renderer@1.0.0/dist/js/react-simulator-renderer.js

    方式 2:unpkg

    https://unpkg.com/@alilc/lowcode-engine@1.0.0/dist/js/engine-core.js  
      
    https://unpkg.com/@alilc/lowcode-react-simulator-renderer@1.0.0/dist/js/react-simulator-renderer.js

    方式 3:jsdelivr

    https://cdn.jsdelivr.net/npm/@alilc/lowcode-engine@1.0.0/dist/js/engine-core.js  
      
    https://cdn.jsdelivr.net/npm/@alilc/lowcode-react-simulator-renderer@1.0.0/dist/js/react-simulator-renderer.js

    方式 4:使用自有 cdn

    将源码中 packages/engine/dist 和 packages/(react|rax)-simulator-renderer/dist 下的文件传至你的 cdn 提供商

    界面功能

    低代码编辑器中的区块主要包含这些功能点:

    物料面板

    可以查找组件,并在此拖动组件到编辑器画布中:

    大纲面板

    可以调整页面内的组件树结构:

    可以在这里打开或者关闭模态浮层的展现:

    源码面板

    可以编辑页面级别的 JavaScript 代码和 CSS 配置:

    Schema 编辑

    【开发者专属】可以编辑页面的底层 Schema 数据:

    搭配顶部操作区的“保存到本地”和“重置页面”功能,可以实验各种 schema 对低代码页面的改变。

    编辑画布区域

    点击组件在右侧面板中能够显示出对应组件的属性配置选项:

    拖拽修改组件的排列顺序:

    将组件拖拽到容器类型的组件中,注意拖拽时会在右侧提示当前的组件树:

    属性

    组件的基础属性值设置:

    样式

    组件的样式配置,如文字:

    事件

    绑定组件对外暴露的事件:

    高级

    循环、条件渲染与 key 设置:

    案例

    钉钉宜搭是阿里巴巴自研的低代码应用开发平台

    Parts造物是阿里巴巴自研的低代码物料管理、物料集成、物料研发的产品

    传送门

    开源地址:

    • https://github.com/alibaba/lowcode-engine
    
    

  • MySQL 数据同步到 Redis 缓存, so Easy !

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

    来源:dongshao.blog.csdn.net/article/details/107190925

    本文介绍MySQL与Redis缓存的同步的两种方案


    • 方案1:通过MySQL自动同步刷新Redis,MySQL触发器+UDF函数实现

    • 方案2:解析MySQL的binlog实现,将数据库中的数据同步到Redis

    一、方案1(UDF)


    • 场景分析: 当我们对MySQL数据库进行数据操作时,同时将相应的数据同步到Redis中,同步到Redis之后,查询的操作就从Redis中查找
    • 过程大致如下:
      • 在MySQL中对要操作的数据设置触发器Trigger,监听操作
      • 客户端(NodeServer)向MySQL中写入数据时,触发器会被触发,触发之后调用MySQL的UDF函数
      • UDF函数可以把数据写入到Redis中,从而达到同步的效果

    • 方案分析:
      • 这种方案适合于读多写少,并且不存并发写的场景
      • 因为MySQL触发器本身就会造成效率的降低,如果一个表经常被操作,这种方案显示是不合适的

    演示案例

    • 下面是MySQL的表

    • 下面是UDF的解析代码

    • 定义对应的触发器


    二、方案2(解析binlog)


    • 在介绍方案2之前我们先来介绍一下MySQL复制的原理,如下图所示:
      • 主服务器操作数据,并将数据写入Bin log
      • 从服务器调用I/O线程读取主服务器的Bin log,并且写入到自己的Relay log中,再调用SQL线程从Relay log中解析数据,从而同步到自己的数据库中

    • 方案2就是:
      • 上面MySQL的整个复制流程可以总结为一句话,那就是:从服务器读取主服务器Bin log中的数据,从而同步到自己的数据库中
      • 我们方案2也是如此,就是在概念上把主服务器改为MySQL,把从服务器改为Redis而已(如下图所示),当MySQL中有数据写入时,我们就解析MySQL的Bin log,然后将解析出来的数据写入到Redis中,从而达到同步的效果

    • 例如下面是一个云数据库实例分析:
      • 云数据库与本地数据库是主从关系。云数据库作为主数据库主要提供写,本地数据库作为从数据库从主数据库中读取数据
      • 本地数据库读取到数据之后,解析Bin log,然后将数据写入写入同步到Redis中,然后客户端从Redis读数据

    • 这个技术方案的难点就在于: 如何解析MySQL的Bin Log。但是这需要对binlog文件以及MySQL有非常深入的理解,同时由于binlog存在Statement/Row/Mixedlevel多种形式,分析binlog实现同步的工作量是非常大的

    Canal开源技术

    • canal是阿里巴巴旗下的一款开源项目,纯Java开发。基于数据库增量日志解析,提供增量数据订阅&消费,目前主要支持了MySQL(也支持mariaDB)
    • 开源参考地址有:https://github.com/liukelin/canal_mysql_nosql_sync
    • 工作原理(模仿MySQL复制):
      • canal模拟mysql slave的交互协议,伪装自己为mysql slave,向mysql master发送dump协议
      • mysql master收到dump请求,开始推送binary log给slave(也就是canal)
      • canal解析binary log对象(原始为byte流)

    • 架构:
      • eventParser (数据源接入,模拟slave协议和master进行交互,协议解析)
      • eventSink (Parser和Store链接器,进行数据过滤,加工,分发的工作)
      • eventStore (数据存储)
      • metaManager (增量订阅&消费信息管理器)
      • server代表一个canal运行实例,对应于一个jvm
      • instance对应于一个数据队列 (1个server对应1..n个instance)
      • instance模块:

    • 大致的解析过程如下:
      • parse解析MySQL的Bin log,然后将数据放入到sink中
      • sink对数据进行过滤,加工,分发
      • store从sink中读取解析好的数据存储起来
      • 然后自己用设计代码将store中的数据同步写入Redis中就可以了
      • 其中parse/sink是框架封装好的,我们做的是store的数据读取那一步

    • 更多关于Cancl可以百度搜索
    • 下面是运行拓扑图

    • MySQL表的同步,采用责任链模式,每张表对应一个Filter 。例如zvsync中要用到的类设计如下:

    • 下面是具体化的zvsync中要用到的类 ,每当新增或者删除表时,直接进行增删就可以了


    三、附加


    本文上面所介绍的都是从MySQL中同步到缓存中。

    但是在实际开发中可能有人会用下面的方案:

      • 1、客户端有数据来了之后,先将其保存到Redis中,然后再同步到MySQL中

      • 2、这种方案本身也是不安全/不可靠的,因此如果Redis存在短暂的宕机或失效,那么会丢失数据


  • 美团:为什么 MySQL 不推荐使用 join?

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

    来源:cnblogs.com/liboware/p/12740901.html

    • 一、应用层关联的优势
    • 二、应用层关联的使用场景
    • 三、不推荐使用join的原因
    • 四、不使用join的解决方案
    • 五、join查询的优势

    1.对于mysql,不推荐使用子查询和join是因为本身join的效率就是硬伤,一旦数据量很大效率就很难保证,强烈推荐分别根据索引单表取数据,然后在程序里面做join,merge数据。

    2.子查询就更别用了,效率太差,执行子查询时,MYSQL需要创建临时表,查询完毕后再删除这些临时表,所以,子查询的速度会受到一定的影响,这里多了一个创建和销毁临时表的过程。

    3.如果是JOIN的话,它是走嵌套查询的。小表驱动大表,且通过索引字段进行关联。如果表记录比较少的话,还是OK的。大的话业务逻辑中可以控制处理。

    4.数据库是最底层的,瓶颈往往是数据库。建议数据库只是作为数据store的工具,而不要添加业务上去。

    一、应用层关联的优势

    让缓存的效率更高。许多应用程序可以方便地缓存单表查询对应的结果对象。如果关联中的某个表发生了变化,那么就无法使用查询缓存了,而拆分后,如果某个表很少改变,那么基于该表的查询就可以重复利用查询缓存结果了。

    • 将查询分解后,执行单个查询可以减少锁的竞争。
    • 在应用层做关联,可以更容易对数据库进行拆分,更容易做到高性能和可扩展。
    • 查询本身效率也可能会有所提升。查询id集的时候,使用IN()代替关联查询,可以让MySQL按照ID顺序进行查询,这可能比随机的关联要更高效。
    • 可以减少冗余记录的查询。在应用层做关联查询,意味着对于某条记录应用只需要查询一次,而在数据库中做关联查询,则可能需
    • 要重复地访问一部分数据。从这点看,这样的重构还可能会减少网络和内存的消艳。
    • 更进一步,这样做相当于在应用中实现了哈希关联,而不是使用MySQL的嵌套循环关联。某些场景哈希关联的效率要高很多。

    二、应用层关联的使用场景

    • 当应用能够方便地缓存单个查询的结果的时候
    • 当可以将数据分布到不同的MySQL服务器上的时候
    • 当能够使用IN()的方式代替关联查询的时候
    • 并发场景多,DB查询频繁,需要分库分表

    三、不推荐使用join的原因

    1.DB承担的业务压力大,能减少负担就减少。当表处于百万级别后,join导致性能下降;

    2.分布式的分库分表。这种时候是不建议跨库join的。目前mysql的分布式中间件,跨库join表现不良。

    3.修改表的schema,单表查询的修改比较容易,join写的sql语句要修改,不容易发现,成本比较大,当系统比较大时,不好维护。

    四、不使用join的解决方案

    在业务层,单表查询出数据后,作为条件给下一个单表查询。也就是子查询。会担心子查询出来的结果集太多。mysql对in的数量没有限制,但是mysql限制整条sql语句的大小。

    通过调整参数max_allowed_packet ,可以修改一条sql的最大值。建议在业务上做好处理,限制一次查询出来的结果集是能接受的。

    五、join查询的优势

    关联查询的好处是可以做分页,可以用副表的字段做查询条件,在查询的时候,将副表匹配到的字段作为结果集,用主表去in它。

    但是问题来了,如果匹配到的数据量太大就不行了,也会导致返回的分页记录跟实际的不一样,解决的方法可以交给前端,一次性查询,让前端分批显示就可以了,这种解决方案的前提是数据量不太,因为sql本身长度有限。

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

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

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

    1. 基本概念

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

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

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

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

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

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

    2. 相关配置

    2.1 添加依赖


        1.8
        1.2.11



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

    2.2 配置属性

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

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

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

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

    2.3 配置Filter

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

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

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

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

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

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

    3 监控页面

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

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

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

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

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

    图片
    1. SQL防火墙页面

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

    1. Session监控页面

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

    1. JSONAPI页面

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

    4. sql监控

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

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

    5. 慢sql记录

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

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

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

    6. spring 监控

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



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

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

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

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

    7. 去广告(Ad)

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

    图片

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

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

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

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

    // this.buildFooter();

    common.js的位置:

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

    2. 使用过滤器过滤

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

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

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

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

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

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

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

                @Override
                public void destroy() {}
            };

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

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

    8. 获取 Druid 的监控数据

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

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

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

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