精华内容
下载资源
问答
  • java解决高并发数据库连接池配置

    千次阅读 2019-03-19 02:34:46
    最近一直在处理高并发的问题,大致情况是这样的:大概有五六千人会在中午十二点同时访问网站,操作数据库,导致服务器崩溃。对于频繁修改数据的这种情况,例如:用户要抢商品,且抢完后要刷新看自己抢的商品,这会...

         使用的IDE是IDEA ,项目是springboot框架的项目

     

           最近一直在处理高并发的问题,大致情况是这样的:大概有五六千人会在中午十二点同时访问网站,操作数据库,导致服务器崩溃。对于频繁修改数据的这种情况,例如:用户要抢商品,且抢完后要刷新看自己抢的商品,这会造成频繁的修改数据库和查询数据库,所以对于用数据库读写分离来说并不高效,因为这涉及到频繁的查询和修改数据库,而数据库的读写分离它需要一个数据共享的过程,比如说一个数据库用来读,一个数据库用来写,但是,用户登陆进去后,要抢商品,那就要写数据,写完后还要将写完的数据共享到读的那个数据库中去,这样用户返回刷新才能看到,这样就多此一举了,而且数据共享还需要一定的时间,这样就会导致用户刷新页面后看到自己还没有抢单成功,用户体验极不好,也会让用户产生怀疑。

          这个时候加redis缓存会更效,为什么这么说呢?用户登录进来后抢单,将数据写入缓存和数据库,当用户再刷新时,用户访问的就是redis缓存里的数据了,不用再去查数据库,这样就减少了数据库的连接,减轻了数据库的负担。

         最头疼的是,加了缓存后,还是一样的崩溃。这不由得让我想起mysql数据库连接数的问题。当产生高并发时,数据库的连接数是有限的,在使用了阿里的druid连接池后,也是一样的卡死,我先说我的解决方法:

           解决方法很简单,就是修改druid的活跃连接数,我之前设置的是20,改成8后,解决了高并发的问题。

        这是什么原因呢? 通过压力测试后,发现一个规律:连接池的最大活跃连接数是根据你的服务器的配置来确定的。我一台服务器是四核的,一台是单核的,当我在双核的服务器上项目里设置的活跃连接数是20时,高并发并没有问题,但是在单核的服务器上项目里设置活跃连接数20时就会崩溃。

           由此得出结论:你设置的连接池的活跃连接数是由你的服务器配置决定的。

           也通过实验,当你的并发量很高时,连接数越小,吞吐量就越高。

         我解决五六千的并发量就是这么解决的,并发量再往上如上万上百万千万的,那就需要用到集群分布式等技术了,但对于这种小并发量的操作没有必要用到这些技术,也会增加成本。配置连接池的maxActive,并不是这个数量越大越好,相反是越小越好,最好是根据你的服务器配置来。

         我的连接池配置如下(.yml文件):

    datasource:
          url: jdbc:mysql://localhost:3306/cgcx?useUnicode=true&characterEncoding=utf-8&&autoReconnect=true&failOverReadOnly=false&useSSL=false
          username: root
          password: 123456
          driver-class-name: com.mysql.jdbc.Driver
          platform: mysql
          type: com.alibaba.druid.pool.DruidDataSource
          # 下面为连接池的补充设置,应用到上面所有数据源中
          # 初始化大小,最小,最大
          initialSize: 1
          minIdle: 2
          maxActive: 8
          # 配置获取连接等待超时的时间
          maxWait: 60000
          # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
          timeBetweenEvictionRunsMillis: 30000
          # 配置一个连接在池中最小生存的时间,单位是毫秒
          minEvictableIdleTimeMillis: 30000
          validationQuery: select 'x'
          testWhileIdle: true
          testOnBorrow: false
          testOnReturn: false
          # 打开PSCache,并且指定每个连接上PSCache的大小
          poolPreparedStatements: false
          maxPoolPreparedStatementPerConnectionSize: 20
          # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
          filters: stat,wall,slf4j
          # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
          connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
    其实高并发这个问题还没有完全的解决方案,就连伟大的阿里巴巴淘宝也没有完全的解决,我们在双十一或双十二的时候,你花了好几天精心挑选的宝贝放在购物车里,到晚上十二点抢着付款那个时候,是不是点不动啊?哈哈哈哈!!!!
    所以这个问题目前只能是尽量避免。并没有完全的解决方案。
    
    希望能帮助到各位。
    展开全文
  • JAVA高并发访问之解决方案

    千次阅读 2014-12-04 11:50:43
    java处理高并发高负载类网站中数据库的设计方法(java教程,java处理大量数据,java高负载数据) 一:高并发高负载类网站关注点之数据库 没错,首先是数据库,这是大多数应用所面临的首个SPOF。尤其是Web2.0的...
    java处理高并发高负载类网站中数据库的设计方法(java教程,java处理大量数据,java高负载数据)

    一:高并发高负载类网站关注点之数据库

    没错,首先是数据库,这是大多数应用所面临的首个SPOF。尤其是Web2.0的应用,数据库的响应是首先要解决的。
    一般来说MySQL是最常用的,可能最初是一个mysql主机,当数据增加到100万以上,那么,MySQL的效能急剧下降。常用的优化措施是M-S(主-从)方式进行同步复制,将查询和操作和分别在不同的服务器上进行操作。我推荐的是M-M-Slaves方式,2个主Mysql,多个Slaves,需要注意的是,虽然有2个Master,但是同时只有1个是Active,我们可以在一定时候切换。之所以用2个M,是保证M不会又成为系统的SPOF。
    Slaves可以进一步负载均衡,可以结合LVS,从而将select操作适当的平衡到不同的slaves上。
    以上架构可以抗衡到一定量的负载,但是随着用户进一步增加,你的用户表数据超过1千万,这时那个M变成了SPOF。你不能任意扩充Slaves,否则复制同步的开销将直线上升,怎么办?我的方法是表分区,从业务层面上进行分区。最简单的,以用户数据为例。根据一定的切分方式,比如id,切分到不同的数据库集群去。

    全局数据库用于meta数据的查询。缺点是每次查询,会增加一次,比如你要查一个用户nightsailer,你首先要到全局数据库群找到nightsailer对应的cluster id,然后再到指定的cluster找到nightsailer的实际数据。
    每个cluster可以用m-m方式,或者m-m-slaves方式。这是一个可以扩展的结构,随着负载的增加,你可以简单的增加新的mysql cluster进去。

    需要注意的是:
    1、禁用全部auto_increment的字段
    2、id需要采用通用的算法集中分配
    3、要具有比较好的方法来监控mysql主机的负载和服务的运行状态。如果你有30台以上的mysql数据库在跑就明白我的意思了。
    4、不要使用持久性链接(不要用pconnect),相反,使用sqlrelay这种第三方的数据库链接池,或者干脆自己做,因为php4中mysql的链接池经常出问题。

    二:高并发高负载网站的系统架构之HTML静态化

    其实大家都知道,效率最高、消耗最小的就是纯静态化 http://www.ablanxue.com/shtml/201207/776.shtml的html页面,所以我们尽可能使我们的网站上的页面采用静态页面来实现,这个最简单的方法其实也是 最有效的方法。但是对于大量内容并且频繁更新的网站,我们无法全部手动去挨个实现,于是出现了我们常见的信息发布系统CMS,像我们常访问的各个门户站点 的新闻频道,甚至他们的其他频道,都是通过信息发布系统来管理和实现的,信息发布系统可以实现最简单的信息录入自动生成静态页面,还能具备频道管理、权限 管理、自动抓取等功能,对于一个大型网站来说,拥有一套高效、可管理的CMS是必不可少的。
      
      除了门户和信息发布类型的网站,对于交互性要求很高的社区类型网站来说,尽可能的静态化也是提高性能的必要手段,将社区内的帖子、文章进行实时的静态化,有更新的时候再重新静态化也是大量使用的策略,像Mop的大杂烩就是使用了这样的策略,网易社区等也是如此。
      
       同时,html静态化也是某些缓存策略使用的手段,对于系统中频繁使用数据库查询但是内容更新很小的应用,可以考虑使用html静态化来实现,比如论坛 中论坛的公用设置信息,这些信息目前的主流论坛都可以进行后台管理并且存储再数据库中,这些信息其实大量被前台程序调用,但是更新频率很小,可以考虑将这 部分内容进行后台更新的时候进行静态化,这样避免了大量的数据库访问请求高并发。
      

    网站HTML静态化解决方案
    当一个Servlet资源请求到达WEB服务器之后我们会填充指定的JSP页面来响应请求:

    HTTP请求---Web服务器---Servlet--业务逻辑处理--访问数据--填充JSP--响应请求

    HTML静态化之后:

    HTTP请求---Web服务器---Servlet--HTML--响应请求

    静态访求如下

    Servlet:

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        if(request.getParameter("chapterId") != null){
            String chapterFileName = "bookChapterRead_"+request.getParameter("chapterId")+".html";
            String chapterFilePath = getServletContext().getRealPath("/") + chapterFileName;
            File chapterFile = new File(chapterFilePath);
            if(chapterFile.exists()){response.sendRedirect(chapterFileName);return;}//如果有这个文件就告诉浏览器转向 
            INovelChapterBiz novelChapterBiz = new NovelChapterBizImpl();
            NovelChapter novelChapter = novelChapterBiz.searchNovelChapterById(Integer.parseInt(request.getParameter("chapterId")));//章节信息 
            int lastPageId = novelChapterBiz.searchLastCHapterId(novelChapter.getNovelId().getId(), novelChapter.getId());
            int nextPageId = novelChapterBiz.searchNextChapterId(novelChapter.getNovelId().getId(), novelChapter.getId());
            request.setAttribute("novelChapter", novelChapter);
            request.setAttribute("lastPageId", lastPageId);
            request.setAttribute("nextPageId", nextPageId);
            new CreateStaticHTMLPage().createStaticHTMLPage(request, response, getServletContext(), 
                    chapterFileName, chapterFilePath, "/bookRead.jsp");
        }
    }
    生成HTML静态页面的类:

    package com.jb.y2t034.thefifth.web.servlet;
    import java.io.ByteArrayOutputStream;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.OutputStreamWriter;
    import java.io.PrintWriter;
    import javax.servlet.RequestDispatcher;
    import javax.servlet.ServletContext;
    import javax.servlet.ServletException;
    import javax.servlet.ServletOutputStream;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import javax.servlet.http.HttpServletResponseWrapper;
    /**
    * 创建HTML静态页面
    * 功能:创建HTML静态页面
    * 时间:2009年1011日
    * 地点:home
    * @author mavk
    *
    */
    public class CreateStaticHTMLPage {
        /**
         * 生成静态HTML页面的方法
         * @param request 请求对象
         * @param response 响应对象
         * @param servletContext Servlet上下文
         * @param fileName 文件名称
         * @param fileFullPath 文件完整路径
         * @param jspPath 需要生成静态文件的JSP路径(相对即可)
         * @throws IOException
         * @throws ServletException
         */
        public void createStaticHTMLPage(HttpServletRequest request, HttpServletResponse response,ServletContext servletContext,String fileName,String fileFullPath,String jspPath) throws ServletException, IOException{
            response.setContentType("text/html;charset=gb2312");//设置HTML结果流编码(即HTML文件编码) 
            RequestDispatcher rd = servletContext.getRequestDispatcher(jspPath);//得到JSP资源 
            final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();//用于从ServletOutputStream中接收资源 
            final ServletOutputStream servletOuputStream = new ServletOutputStream(){//用于从HttpServletResponse中接收资源 
                public void write(byte[] b, int off,int len){
                    byteArrayOutputStream.write(b, off, len);
                }
                public void write(int b){
                    byteArrayOutputStream.write(b);
                }
            };
            final PrintWriter printWriter = new PrintWriter(new OutputStreamWriter(byteArrayOutputStream));//把转换字节流转换成字符流 
            HttpServletResponse httpServletResponse = new HttpServletResponseWrapper(response){//用于从response获取结果流资源(重写了两个方法) 
                public ServletOutputStream getOutputStream(){
                    return servletOuputStream;
                }
                public PrintWriter getWriter(){
                    return printWriter;
                }
            };
            rd.include(request, httpServletResponse);//发送结果流 
            printWriter.flush();//刷新缓冲区,把缓冲区的数据输出 
            FileOutputStream fileOutputStream = new FileOutputStream(fileFullPath);
            byteArrayOutputStream.writeTo(fileOutputStream);//把byteArrayOuputStream中的资源全部写入到fileOuputStream中 
            fileOutputStream.close();//关闭输出流,并释放相关资源 
            response.sendRedirect(fileName);//发送指定文件流到客户端 
        }
    }


    三:高并发高负载类网站关注点之缓存、负载均衡、存储

    缓存是另一个大问题,我一般用memcached来做缓存集群,一般来说部署10台左右就差不多(10g内存池)。需要注意一点,千万不能用使用
    swap,最好关闭linux的swap。


    负载均衡/加速

    可能上面说缓存的时候,有人第一想的是页面静态化,所谓的静态html,我认为这是常识,不属于要点了。页面的静态化随之带来的是静态服务的
    负载均衡和加速。我认为Lighttped+Squid是最好的方式了。
    LVS <------->lighttped====>squid(s) ====lighttpd

    上面是我经常用的。注意,我没有用apache,除非特定的需求,否则我不部署apache,因为我一般用php-fastcgi配合lighttpd,
    性能比apache+mod_php要强很多。

    squid的使用可以解决文件的同步等等问题,但是需要注意,你要很好的监控缓存的命中率,尽可能的提高的90%以上。
    squid和lighttped也有很多的话题要讨论,这里不赘述。


    存储
    存储也是一个大问题,一种是小文件的存储,比如图片这类。另一种是大文件的存储,比如搜索引擎的索引,一般单文件都超过2g以上。
    小文件的存储最简单的方法是结合lighttpd来进行分布。或者干脆使用Redhat的GFS,优点是应用透明,缺点是费用较高。我是指
    你购买盘阵的问题。我的项目中,存储量是2-10Tb,我采用了分布式存储。这里要解决文件的复制和冗余。
    这样每个文件有不同的冗余,这方面可以参考google的gfs的论文。
    大文件的存储,可以参考nutch的方案,现在已经独立为hadoop子项目。(你可以google it)

    其他:
    此外,passport等也是考虑的,不过都属于比较简单的了。
    四:高并发高负载网站的系统架构之图片服务器分离
    大家知道,对于Web 服务器来说,不管是Apache、IIS还是其他容器,图片是最消耗资源的,于是我们有必要将图片与页面进行分离,这是基本上大型网站都会采用的策略,他 们都有独立的图片服务器,甚至很多台图片服务器。这样的架构可以降低提供页面访问请求的服务器系统压力,并且可以保证系统不会因为图片问题而崩溃,在应用 服务器和图片服务器上,可以进行不同的配置优化,比如apache在配置ContentType的时候可以尽量少支持,尽可能少的LoadModule, 保证更高的系统消耗和执行效率。


    利用Apache实现图片服务器的分离
    缘由:
    起步阶段的应用,都可能部署在一台服务器上(费用上的原因)
    第一个优先分离的,肯定是数据库和应用服务器。
    第二个分离的,会是什么呢?各有各的考虑,我所在的项目组重点考虑的节约带宽,服务器性能再好,带宽再高,并发来了,也容易撑不住。因此,我这篇文章的重点在这里。这里重点是介绍实践,不一定符合所有情况,供看者参考吧,
    环境介绍:
    WEB应用服务器:4CPU双核2G, 内存4G
      部署:Win2003/Apache Http Server 2.1/Tomcat6
    数据库服务器:4CPU双核2G, 内存4G
      部署:Win2003/MSSQL2000
    步骤:
    步骤一:增加2台配置为:2CPU双核2G,内存2G普通服务器,做资源服务器
      部署:Tomcat6,跑了一个图片上传的简单应用,(记得指定web.xml的<distributable/>),并指定域名为res1.***.com,res2.***.com,采用ajp协议
    步骤二:修改Apache httpd.conf配置
      原来应用的文件上传功能网址为:
       1、/fileupload.html
       2、/otherupload.html
      在httpd.conf中增加如下配置

    <VirtualHost *:80> 
      ServerAdmin webmaster@***.com 
      ProxyPass /fileupload.html balancer://rescluster/fileupload lbmethod=byrequests stickysession=JSESSIONID nofailover=Off timeout=5 maxattempts=3    
      ProxyPass /otherupload.html balancer://rescluster/otherupload.html lbmethod=byrequests stickysession=JSESSIONID nofailover=Off timeout=5 maxattempts=3    
      #<!--负载均衡--> 
      <Proxy balancer://rescluster/> 
        BalancerMember ajp://res1.***.com:8009 smax=5 max=500 ttl=120 retry=300 loadfactor=100 route=tomcat1 
        BalancerMember ajp://res2.***.com:8009 smax=5 max=500 ttl=120 retry=300 loadfactor=100 route=tomcat2 
      </Proxy> 
     
    </VirtualHost>
    步骤三,修改业务逻辑:
      所有上传文件在数据库中均采用全url的方式保存,例如产品图片路径存成:http://res1.***.com/upload/20090101/product120302005.jpg

    现在,你可以高枕无忧了,带宽不够时,增加个几十台图片服务器,只需要稍微修改一下apache的配置文件,即可。

    五:高并发高负载网站的系统架构之数据库集群和库表散列

    大型网站都有复杂的应用,这些应用必须使用数据库,那么在面对大量访问的时候,数据库的瓶颈很快就能显现出来,这时一台数据库将很快无法满足应用,于是我们需要使用数据库集群或者库表散列。
      
      在数据库集群方面,很多数据库都有自己的解决方案,Oracle、Sybase等都有很好的方案,常用的MySQL提供的Master/Slave也是类似的方案,您使用了什么样的DB,就参考相应的解决方案来实施即可。
      
       上面提到的数据库集群由于在架构、成本、扩张性方面都会受到所采用DB类型的限制,于是我们需要从应用程序的角度来考虑改善系统架构,库表散列是常用并 且最有效的解决方案。我们在应用程序中安装业务和应用或者功能模块将数据库进行分离,不同的模块对应不同的数据库或者表,再按照一定的策略对某个页面或者 功能进行更小的数据库散列,比如用户表,按照用户ID进行表散列,这样就能够低成本的提升系统的性能并且有很好的扩展性。sohu的论坛就是采用了这样的 架构,将论坛的用户、设置、帖子等信息进行数据库分离,然后对帖子、用户按照板块和ID进行散列数据库和表,最终可以在配置文件中进行简单的配置便能让系 统随时增加一台低成本的数据库进来补充系统性能。


    集群软件的分类:
    一般来讲,集群软件根据侧重的方向和试图解决的问题,分为三大类:高性能集群(High performance cluster,HPC)、负载均衡集群(Load balance cluster, LBC),高可用性集群(High availability cluster,HAC)。
    高性能集群(High performance cluster,HPC),它是利用一个集群中的多台机器共同完成同一件任务,使得完成任务的速度和可靠性都远远高于单机运行的效果。弥补了单机性能上的不足。该集群在天气预报、环境监控等数据量大,计算复杂的环境中应用比较多;
    负载均衡集群(Load balance cluster, LBC),它是利用一个集群中的多台单机,完成许多并行的小的工作。一般情况下,如果一个应用使用的人多了,那么用户请求的响应时间就会增大,机器的性能也会受到影响,如果使用负载均衡集群,那么集群中任意一台机器都能响应用户的请求,这样集群就会在用户发出服务请求之后,选择当时负载最小,能够提供最好的服务的这台机器来接受请求并相应,这样就可用用集群来增加系统的可用性和稳定性。这类集群在网站中使用较多;
    高可用性集群(High availability cluster,HAC),它是利用集群中系统 的冗余,当系统中某台机器发生损坏的时候,其他后备的机器可以迅速的接替它来启动服务,等待故障机的维修和返回。最大限度的保证集群中服务的可用性。这类系统一般在银行,电信服务这类对系统可靠性有高的要求的领域有着广泛的应用。
    2 数据库集群的现状
    数据库集群是将计算机集群技术引入到数据库中来实现的,尽管各厂商宣称自己的架构如何的完美,但是始终不能改变Oracle当先,大家追逐的事实,在集群的解决方案上Oracle RAC还是领先于包括微软在内的其它数据库厂商,它能满足客户高可用性、高性能、数据库负载均衡和方便扩展的需求。
    Oracle’s Real Application Cluster (RAC)
    Microsoft SQL Cluster Server (MSCS)
    IBM’s DB2 UDB High Availability Cluster(UDB)
    Sybase ASE High Availability Cluster (ASE)
    MySQL High Availability Cluster (MySQL CS)
    基于IO的第三方HA(高可用性)集群
    当前主要的数据库集群技术有以上六大类,有数据库厂商自己开发的;也有第三方的集群公司开发的;还有数据库厂商与第三方集群公司合作开发的,各类集群实现的功能及架构也不尽相同。
    RAC(Real Application Cluster,真正应用集群)是Oracle9i数据库中采用的一项新技术,也是Oracle数据库支持网格计算环境的核心技术。它的出现解决了传统数据库应用中面临的一个重要问题:高性能、高可伸缩性与低价格之间的矛盾。在很长一段时间里,甲骨文都以其实时应用集群技术(Real Application Cluster,RAC)统治着集群数据库市场

    六:高并发高负载网站的系统架构之缓存

    缓存一词搞技术的都接触过,很多地方用到缓存。网站架构和网站开发中的缓存也是非常重要。这里先讲述最基本的两种缓存。高级和分布式的缓存在后面讲述。
      架构方面的缓存,对Apache比较熟悉的人都能知道Apache提供了自己的缓存模块,也可以使用外加的Squid模块进行缓存,这两种方式均可以有效的提高Apache的访问响应能力。
       网站程序开发方面的缓存,Linux上提供的Memory Cache是常用的缓存接口,可以在web开发中使用,比如用Java开发的时候就可以调用MemoryCache对一些数据进行缓存和通讯共享,一些大 型社区使用了这样的架构。另外,在使用web语言开发的时候,各种语言基本都有自己的缓存模块和方法,PHP有Pear的Cache模块,Java就更多 了,.net不是很熟悉,相信也肯定有。



    Java开源缓存框架
    JBossCache/TreeCache JBossCache是一个复制的事务处理缓存,它允许你缓存企业级应用数据来更好的改善性能。缓存数据被自动复制,让你轻松进行Jboss服务器之间的集群工作。JBossCache能够通过Jboss应用服务或其他J2EE容器来运行一个Mbean服务,当然,它也能独立运行。 JBossCache包括两个模块:TreeCache和TreeCacheAOP。 TreeCache --是一个树形结构复制的事务处理缓存。 TreeCacheAOP --是一个“面向对象”缓存,它使用AOP来动态管理POJO
    OSCache OSCache标记库由OpenSymphony设计,它是一种开创性的JSP定制标记应用,提供了在现有JSP页面之内实现快速内存缓冲的功能。OSCache是个一个广泛采用的高性能的J2EE缓存框架,OSCache能用于任何Java应用程序的普通的缓存解决方案。OSCache有以下特点:缓存任何对象,你可以不受限制的缓存部分jsp页面或HTTP请求,任何java对象都可以缓存。 拥有全面的API--OSCache API给你全面的程序来控制所有的OSCache特性。 永久缓存--缓存能随意的写入硬盘,因此允许昂贵的创建(expensive-to-create)数据来保持缓存,甚至能让应用重启。 支持集群--集群缓存数据能被单个的进行参数配置,不需要修改代码。 缓存记录的过期--你可以有最大限度的控制缓存对象的过期,包括可插入式的刷新策略(如果默认性能不需要时)。
    JCACHE JCACHE是一种即将公布的标准规范(JSR 107),说明了一种对Java对象临时在内存中进行缓存的方法,包括对象的创建、共享访问、假脱机(spooling)、失效、各JVM的一致性等。它可被用于缓存JSP内最经常读取的数据,如产品目录和价格列表。利用JCACHE,多数查询的反应时间会因为有缓存的数据而加快(内部测试表明反应时间大约快15倍)。
    Ehcache Ehcache出自Hibernate,在Hibernate中使用它作为数据缓存的解决方案。
    Java Caching System JCS是Jakarta的项目Turbine的子项目。它是一个复合式的缓冲工具。可以将对象缓冲到内存、硬盘。具有缓冲对象时间过期设定。还可以通过JCS构建具有缓冲的分布式构架,以实现高性能的应用。 对于一些需要频繁访问而每访问一次都非常消耗资源的对象,可以临时存放在缓冲区中,这样可以提高服务的性能。而JCS正是一个很好的缓冲工具。缓冲工具对于读操作远远多于写操作的应用性能提高非常显著。
    SwarmCache SwarmCache是一个简单而功能强大的分布式缓存机制。它使用IP组播来有效地在缓存的实例之间进行通信。它是快速提高集群式Web应用程序的性能的理想选择。
    ShiftOne ShiftOne Object Cache这个Java库提供了基本的对象缓存能力。实现的策略有先进先出(FIFO),最近使用(LRU),最不常使用(LFU)。所有的策略可以最大化元素的大小,最大化其生存时间。
    WhirlyCache Whirlycache是一个快速的、可配置的、存在于内存中的对象的缓存。它能够通过缓存对象来加快网站或应用程序的速度,否则就必须通过查询数据库或其他代价较高的处理程序来建立。
    Jofti Jofti可对在缓存层中(支持EHCache,JBossCache和OSCache)的对象或在支持Map接口的存储结构中的对象进行索引与搜索。这个框架还为对象在索引中的增删改提供透明的功能同样也为搜索提供易于使用的查询功能。
    cache4j cache4j是一个有简单API与实现快速的Java对象缓存。它的特性包括:在内存中进行缓存,设计用于多线程环境,两种实现:同步与阻塞,多种缓存清除策略:LFU, LRU, FIFO,可使用强引用(strong reference)与软引用(soft reference)存储对象。
    Open Terracotta 一个JVM级的开源群集框架,提供:HTTP Session复制,分布式缓存,POJO群集,跨越群集的JVM来实现分布式应用程序协调(采用代码注入的方式,所以你不需要修改任何)。
    sccache SHOP.COM使用的对象缓存系统。sccache是一个in-process cache和二级、共享缓存。它将缓存对象存储到磁盘上。支持关联Key,任意大小的Key和任意大小的数据。能够自动进行垃圾收集。
    Shoal Shoal是一个基于Java可扩展的动态集群框架,能够为构建容错、可靠和可用的Java应用程序提供了基础架构支持。这个框架还可以集成到不希望绑定到特定通信协议,但需要集群和分布式系统支持的任何Java产品中。Shoal是GlassFish和JonAS应用服务器的集群引擎。
    Simple-Spring-Memcached Simple-Spring-Memcached,它封装了对MemCached的调用,使MemCached的客户端开发变得超乎寻常的简单。
    展开全文
  • Java高并发秒杀API(四)之高并发优化

    万次阅读 多人点赞 2017-10-06 17:07:54
    Java高并发秒杀API(四)之高并发优化1. 高并发优化分析 关于并发 并发性上不去是因为当多个线程同时访问一行数据时,产生了事务,因此产生写锁,每当一个获取了事务的线程把锁释放,另一个排队线程才能拿到写锁,QPS...

    Java高并发秒杀API(四)之高并发优化

    1. 高并发优化分析

    关于并发

    并发性上不去是因为当多个线程同时访问一行数据时,产生了事务,因此产生写锁,每当一个获取了事务的线程把锁释放,另一个排队线程才能拿到写锁,QPS(Query Per Second每秒查询率)和事务执行的时间有密切关系,事务执行时间越短,并发性越高,这也是要将费时的I/O操作移出事务的原因。

    在本项目中高并发发生在哪?

    高并发发生的地方

    在上图中,红色的部分就表示会发生高并发的地方,绿色部分表示对于高并发没有影响。

    为什么需要单独获取系统时间?

    这是为了我们的秒杀系统的优化做铺垫。比如在秒杀还未开始的时候,用户大量刷新秒杀商品详情页面是很正常的情况,这时候秒杀还未开始,大量的请求发送到服务器会造成不必要的负担。

    我们将这个详情页放置到CDN中,这样用户在访问该页面时就不需要访问我们的服务器了,起到了降低服务器压力的作用。而CDN中存储的是静态化的详情页和一些静态资源(css,js等),这样我们就拿不到系统的时间来进行秒杀时段的控制,所以我们需要单独设计一个请求来获取我们服务器的系统时间。

    详情页

    CDN(Content Delivery Network)的理解

    CDN

    获取系统时间不需要优化

    因为Java访问一次内存(Cacheline)大约10ns,1s=10亿ns,也就是如果不考虑GC,这个操作1s可以做1亿次。

    秒杀地址接口分析

    • 无法使用CDN缓存,因为CDN适合请求对应的资源不变化的,比如静态资源、JavaScript;秒杀地址返回的数据是变化的,不适合放在CDN缓存;
    • 适合服务端缓存:Redis等,1秒钟可以承受10万qps。多个Redis组成集群,可以到100w个qps. 所以后端缓存可以用业务系统控制。

    秒杀地址接口优化

    秒杀地址接口优化

    秒杀操作优化分析

    • 无法使用cdn缓存
    • 后端缓存困难: 库存问题
    • 一行数据竞争:热点商品

    大部分写的操作和核心操作无法使用CDN,也不可能在缓存中减库存。你在Redis中减库存,那么用户也可能通过缓存来减库存,这样库存会不一致,所以要通过mysql的事务来保证一致性。

    比如一个热点商品所有人都在抢,那么会在同一时间对数据表中的一行数据进行大量的update set操作。

    行级锁在commit之后才释放,所以优化方向是减少行级锁的持有时间。

    延迟问题很关键

    • 同城机房网络(0.5ms~2ms),最高并发性是1000qps。
    • Update后JVM -GC(垃圾回收机制)大约50ms,最高并发性是20qps。并发性越高,GC就越可能发生,虽然不一定每次都会发生,但一定会发生。
    • 异地机房,比如北京到上海之间的网络延迟,进过计算大概13~20ms。

      网络延迟计算

    如何判断update更新库存成功?

    有两个条件:

    1. update自身没报错;
    2. 客户端确认update影响记录数

    优化思路:

    • 把客户端逻辑放到MySQL服务端,避免网络延迟和GC影响

    如何把客户端逻辑放到MySQL服务端

    有两种方案:

    1. 定制SQL方案,在每次update后都会自动提交,但需要修改MySQL源码,成本很高,不是大公司(BAT等)一般不会使用这种方法。
    2. 使用存储过程:整个事务在MySQL端完成,用存储过程写业务逻辑,服务端负责调用。

    接下来先分析第一种方案

    秒杀方案1

    秒杀方案1成本分析

    根据上图的成本分析,我们的秒杀系统采用第二种方案,即使用存储过程。

    优化总结

    • 前端控制

    暴露接口,按钮防重复(点击一次按钮后就变成灰色,禁止重复点击按钮)

    • 动静态数据分离

    CDN缓存,后端缓存

    • 事务竞争优化

    减少事务行级锁的持有时间

    2. Redis后端缓存优化编码

    关于CDN的说明

    由于不同公司提供的CDN的接口暴露不同,不同的公司租用的机房调用的API也不相同,所以慕课网的视频中并没有对CDN的使用过程进行讲解。

    2.1 下载安装Redis

    前往官网下载安装Stable版本的Redis,安装后可以将安装目录添加到系统变量Path里以方便使用,我使用的是Windows系统的Redis,懒得去官网下载的可以点这里下载

    安装后,运行redis-server.exe启动服务器成功,接着运行redis-cli.exe启动客户端连接服务器成功,说明Redis已经安装成功了。

    为什么使用Redis

    Redis属于NoSQL,即非关系型数据库,它是key-value型数据库,是直接在内存中进行存取数据的,所以有着很高的性能。

    利用Redis可以减轻MySQL服务器的压力,减少了跟数据库服务器的通信次数。秒杀的瓶颈就在于跟数据库服务器的通信速度(MySQL本身的主键查询非常快)

    2.2 在pom.xml中配置Redis客户端

    <!--添加Redis依赖 -->
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>2.7.3</version>
    </dependency>
    

    Jedis

    Redis有很多客户端,我们的项目是用Java语言写的,自然选择对应Java语言的客户端,而官网最推荐我们的Java客户端是Jedis,在pom.xml里配置了Jedis依赖就可以使用它了,记得要先开启Redis的服务器,Jedis才能连接到服务器。

    由于Jedis并没有实现内部序列化操作,而Java内置的序列化机制性能又不高,我们是一个秒杀系统,需要考虑高并发优化,在这里我们采用开源社区提供的更高性能的自定义序列化工具protostuff。

    2.3 在pom.xml中配置protostuff依赖

    <!--prostuff序列化依赖 -->
    <dependency>
        <groupId>com.dyuproject.protostuff</groupId>
        <artifactId>protostuff-core</artifactId>
        <version>1.0.8</version>
    </dependency>
    <dependency>
        <groupId>com.dyuproject.protostuff</groupId>
        <artifactId>protostuff-runtime</artifactId>
        <version>1.0.8</version>
    </dependency>
    

    关于序列化和反序列化

    序列化是处理对象流的机制,就是将对象的内容进行流化,可以对流化后的对象进行读写操作,也可以将流化后的对象在网络间传输。反序列化就是将流化后的对象重新转化成原来的对象。

    在Java中内置了序列化机制,通过implements Serializable来标识一个对象实现了序列化接口,不过其性能并不高。

    2.4 使用Redis优化地址暴露接口

    原本查询秒杀商品时是通过主键直接去数据库查询的,选择将数据缓存在Redis,在查询秒杀商品时先去Redis缓存中查询,以此降低数据库的压力。如果在缓存中查询不到数据再去数据库中查询,再将查询到的数据放入Redis缓存中,这样下次就可以直接去缓存中直接查询到。

    以上属于数据访问层的逻辑(DAO层),所以我们需要在dao包下新建一个cache目录,在该目录下新建RedisDao.java,用来存取缓存。

    RedisDao

    public class RedisDao {
        private final JedisPool jedisPool;
    
        public RedisDao(String ip, int port) {
            jedisPool = new JedisPool(ip, port);
        }
    
        private RuntimeSchema<Seckill> schema = RuntimeSchema.createFrom(Seckill.class);
    
        public Seckill getSeckill(long seckillId) {
            // redis操作逻辑
            try {
                Jedis jedis = jedisPool.getResource();
                try {
                    String key = "seckill:" + seckillId;
                    // 并没有实现哪部序列化操作
                    // 采用自定义序列化
                    // protostuff: pojo.
                    byte[] bytes = jedis.get(key.getBytes());
                    // 缓存重获取到
                    if (bytes != null) {
                        Seckill seckill = schema.newMessage();
                        ProtostuffIOUtil.mergeFrom(bytes, seckill, schema);
                        // seckill被反序列化
    
                        return seckill;
                    }
                } finally {
                    jedis.close();
                }
            } catch (Exception e) {
    
            }
            return null;
        }
    
        public String putSeckill(Seckill seckill) {
            try {
                Jedis jedis = jedisPool.getResource();
                try {
                    String key = "seckill:" + seckill.getSeckillId();
                    byte[] bytes = ProtostuffIOUtil.toByteArray(seckill, schema,
                            LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE));
                    // 超时缓存
                    int timeout = 60 * 60;// 1小时
                    String result = jedis.setex(key.getBytes(), timeout, bytes);
    
                    return result;
                } finally {
                    jedis.close();
                }
            } catch (Exception e) {
    
            }
    
            return null;
        }
    }
    

    注意

    使用protostuff序列化工具时,被序列化的对象必须是pojo对象(具备setter/getter)

    在spring-dao.xml中手动注入RedisDao

    由于RedisDao和MyBatis的DAO没有关系,MyBatis不会帮我们自动实现该接口,所以我们需要在spring-dao.xml中手动注入RedisDao。由于我们在RedisDao是通过构造方法来注入ip和port两个参数的,所以需要配置,如果不配置这个标签,我们需要为ip和port提供各自的setter和getter(注入时可以没有getter)。

    在这里我们直接把value的值写死在标签里边了,实际开发中需要把ip和port参数的值写到配置文件里,通过读取配置文件的方式读取它们的值。

    <!--redisDao -->
    <bean id="redisDao" class="com.lewis.dao.cache.RedisDao">
        <constructor-arg index="0" value="localhost" />
        <constructor-arg index="1" value="6379" />
    </bean>
    

    修改SeckillServiceImpl

    使用注解注入RedisDao属性

     @Autowired
     private RedisDao redisDao;
    

    修改exportSeckillURI()

    public Exposer exportSeckillUrl(long seckillId) {
        // 优化点:缓存优化:超时的基础上维护一致性
        // 1.访问redis
    
        Seckill seckill = redisDao.getSeckill(seckillId);
        if (seckill == null) {
            // 2.访问数据库
            seckill = seckillDao.queryById(seckillId);
            if (seckill == null) {// 说明查不到这个秒杀产品的记录
                return new Exposer(false, seckillId);
            } else {
                // 3.放入redis
                redisDao.putSeckill(seckill);
            }
        }
    
        // 若是秒杀未开启
        Date startTime = seckill.getStartTime();
        Date endTime = seckill.getEndTime();
        // 系统当前时间
        Date nowTime = new Date();
        if (startTime.getTime() > nowTime.getTime() || endTime.getTime() < nowTime.getTime()) {
            return new Exposer(false, seckillId, nowTime.getTime(), startTime.getTime(), endTime.getTime());
        }
    
        // 秒杀开启,返回秒杀商品的id、用给接口加密的md5
        String md5 = getMD5(seckillId);
        return new Exposer(true, md5, seckillId);
    }
    

    2.5 测试类RedisDaoTest

    通过IDE工具快速生成测试类RedisDaoTest,新写一个testSeckill(),对getSeckill和putSeckill方法进行全局测试。

    @RunWith(SpringJUnit4ClassRunner.class)
    // 告诉junit spring的配置文件
    @ContextConfiguration({ "classpath:spring/spring-dao.xml" })
    public class RedisDaoTest {
        private final Logger logger = LoggerFactory.getLogger(this.getClass());
    
        private long id = 1001;
    
        @Autowired
        private RedisDao redisDao;
    
        @Autowired
        private SeckillDao seckillDao;
    
        @Test
        public void testSeckill() {
    
            Seckill seckill = redisDao.getSeckill(id);
            if (seckill == null) {
                seckill = seckillDao.queryById(id);
                if (seckill != null) {
                    String result = redisDao.putSeckill(seckill);
                    logger.info("result={}", result);
                    seckill = redisDao.getSeckill(id);
                    logger.info("seckill={}", seckill);
                }
            }
        }
    
    }
    

    如果测试通过了,会输出result={}OK以及id为1001的商品信息,如果输出的都是null,那说明你没有开启Redis服务器,所以在内存中没有存取到缓存。

    为什么不用Redis的hash来存储对象?

    第一:通过Jedis储存对象的方式有大概三种

    1. 本项目采用的方式:将对象序列化成byte字节,最终存byte字节;
    2. 对象转hashmap,也就是你想表达的hash的形式,最终存map;
    3. 对象转json,最终存json,其实也就是字符串

    第二:其实如果你是平常的项目,并发不高,三个选择都可以,这种情况下以hash的形式更加灵活,可以对象的单个属性,但是问题来了,在秒杀的场景下,三者的效率差别很大。

    第三:结果如下

    10w数据 时间 内存占用
    存json 10s 14M
    存byte 6s 6M
    存jsonMap 10s 20M
    存byteMap 4s 4M
    取json 7s
    取byte 4s
    取jsonmap 7s
    取bytemap 4s

    第四:你该说了,bytemap最快啊,为啥不用啊,因为项目用了超级强悍的序列化工具啊,以上测试是基于java的序列化,如果改了序列化工具,你可以测试下。

    以上问答源自慕课网的一道问答

    教学视频中张老师对于Redis暴露接口地址的补充

    1. redis事务与RDBMS事务有本质区别,详情见http://redis.io/topics/transactions
    2. 关于spring整合redis。原生Jedis API已经足够清晰。笔者所在的团队不使用任何spring-data整合API,而是直接对接原生Client并做二次开发调优,如Jedis,Hbase等。
    3. 这里使用redis缓存方法用于暴露秒杀地址场景,该方法存在瞬时压力,为了降低DB的primary key QPS,且没有使用库存字段所以不做一致性维护。
    4. 跨数据源的严格一致性需要2PC支持,性能不尽如人意。线上产品一般使用最终一致性去解决,这块相关知识较多,所以没有讲。
    5. 本课程的重点其实不是SSM,只是一个快速开发的方式。重点根据业务场景分析通信成本,瓶颈点的过程和优化思路。
    6. 初学者不要纠结于事务。事务可以降低一致性维护难度,但扩展性灵活性存在不足。技术是死的,人是活的。比如京东抢购使用Redis+LUA+MQ方案,就是一种技术反思。

    3. 秒杀操作——并发优化

    3.1 简单优化

    回顾事务执行

    回顾事务执行

    sql语句的简单优化

    简单优化

    优化SeckillServiceImpl的executeSeckill()

    用户的秒杀操作分为两步:减库存、插入购买明细,我们在这里进行简单的优化,就是将原本先update(减库存)再进行insert(插入购买明细)的步骤改成:先insert再update。

    public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException,
            RepeatKillException, SeckillCloseException {
    
        if (md5 == null || !md5.equals(getMD5(seckillId))) {
            throw new SeckillException("seckill data rewrite");// 秒杀数据被重写了
        }
        // 执行秒杀逻辑:减库存+增加购买明细
        Date nowTime = new Date();
    
        try {
    
            // 否则更新了库存,秒杀成功,增加明细
            int insertCount = successKilledDao.insertSuccessKilled(seckillId, userPhone);
            // 看是否该明细被重复插入,即用户是否重复秒杀
            if (insertCount <= 0) {
                throw new RepeatKillException("seckill repeated");
            } else {
    
                // 减库存,热点商品竞争
                int updateCount = seckillDao.reduceNumber(seckillId, nowTime);
                if (updateCount <= 0) {
                    // 没有更新库存记录,说明秒杀结束 rollback
                    throw new SeckillCloseException("seckill is closed");
                } else {
                    // 秒杀成功,得到成功插入的明细记录,并返回成功秒杀的信息 commit
                    SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
                    return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, successKilled);
                }
            }
        } catch (SeckillCloseException e1) {
            throw e1;
        } catch (RepeatKillException e2) {
            throw e2;
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
            // 将编译期异常转化为运行期异常
            throw new SeckillException("seckill inner error :" + e.getMessage());
        }
    
    }
    

    为什么要先insert再update

    首先是在更新操作的时候给行加锁,插入并不会加锁,如果更新操作在前,那么就需要执行完更新和插入以后事务提交或回滚才释放锁。而如果插入在前,更新在后,那么只有在更新时才会加行锁,之后在更新完以后事务提交或回滚释放锁。

    在这里,插入是可以并行的,而更新由于会加行级锁是串行的。

    也就是说是更新在前加锁和释放锁之间两次的网络延迟和GC,如果插入在前则加锁和释放锁之间只有一次的网络延迟和GC,也就是减少的持有锁的时间。

    这里先insert并不是忽略了库存不足的情况,而是因为insert和update是在同一个事务里,光是insert并不一定会提交,只有在update成功才会提交,所以并不会造成过量插入秒杀成功记录。

    3.2 深度优化

    前边通过调整insert和update的执行顺序来实现简单优化,但依然存在着Java客户端和服务器通信时的网络延迟和GC影响,我们可以将执行秒杀操作时的insert和update放到MySQL服务端的存储过程里,而Java客户端直接调用这个存储过程,这样就可以避免网络延迟和可能发生的GC影响。另外,由于我们使用了存储过程,也就使用不到Spring的事务管理了,因为在存储过程里我们会直接启用一个事务。

    3.2.1 写一个存储过程procedure,然后在MySQL控制台里执行它

    -- 秒杀执行储存过程
    DELIMITER $$ -- 将定界符从;转换为$$
    -- 定义储存过程
    -- 参数: in输入参数   out输出参数
    -- row_count() 返回上一条修改类型sql(delete,insert,update)的影响行数
    -- row_count:0:未修改数据 ; >0:表示修改的行数; <0:sql错误
    CREATE PROCEDURE `seckill`.`execute_seckill`
      (IN v_seckill_id BIGINT, IN v_phone BIGINT,
       IN v_kill_time  TIMESTAMP, OUT r_result INT)
      BEGIN
        DECLARE insert_count INT DEFAULT 0;
        START TRANSACTION;
        INSERT IGNORE INTO success_killed
        (seckill_id, user_phone, state)
        VALUES (v_seckill_id, v_phone, 0);
        SELECT row_count() INTO insert_count;
        IF (insert_count = 0) THEN
          ROLLBACK;
          SET r_result = -1;
        ELSEIF (insert_count < 0) THEN
            ROLLBACK;
            SET r_result = -2;
        ELSE
          UPDATE seckill
          SET number = number - 1
          WHERE seckill_id = v_seckill_id
                AND end_time > v_kill_time
                AND start_time < v_kill_time
                AND number > 0;
          SELECT row_count() INTO insert_count;
          IF (insert_count = 0) THEN
            ROLLBACK;
            SET r_result = 0;
          ELSEIF (insert_count < 0) THEN
              ROLLBACK;
              SET r_result = -2;
          ELSE
            COMMIT;
            SET r_result = 1;
          END IF;
        END IF;
      END;
    $$
    -- 储存过程定义结束
    -- 将定界符重新改为;
    DELIMITER ;
    
    -- 定义一个用户变量r_result
    SET @r_result = -3;
    -- 执行储存过程
    CALL execute_seckill(1003, 13502178891, now(), @r_result);
    -- 获取结果
    SELECT @r_result;
    

    注意点

    CREATE PROCEDURE `seckill`.`execute_seckill`
    

    上边这句语句的意思是为一个名为seckill的数据库定义一个名为execute_seckill的存储过程,如果你在连接数据库后使用了这个数据库(即use seckill;),那么这里的定义句子就不能这样写了,会报错(因为存储过程是依赖于数据库的),改成下边这样:

    CREATE PROCEDURE `execute_seckill`
    

    row_count()

    存储过程中,row_count()函数用来返回上一条sql(delete,insert,update)影响的行数。

    根据row_count()返回值,可以进行接下来的流程判断:

    0:未修改数据;

    >0: 表示修改的行数;

    <0: 表示SQL错误或未执行修改SQL

    3.2.2 修改源码以调用存储过程

    SeckillDao里添加调用存储过程的方法声明

    /**
     *  使用储存过程执行秒杀
     * @param paramMap
     */
    void killByProcedure(Map<String,Object> paramMap);
    

    接着在SeckillDao.xml里添加该方法对应的sql语句

    <!--调用储存过程 -->
    <select id="killByProcedure" statementType="CALLABLE">
        CALL execute_seckill(
            #{seckillId,jdbcType=BIGINT,mode=IN},
            #{phone,jdbcType=BIGINT,mode=IN},
            #{killTime,jdbcType=TIMESTAMP,mode=IN},
            #{result,jdbcType=INTEGER,mode=OUT}
        )
    </select>
    

    SeckillService接口里添加一个方法声明

    /**
     * 调用存储过程来执行秒杀操作,不需要抛出异常
     * 
     * @param seckillId 秒杀的商品ID
     * @param userPhone 手机号码
     * @param md5 md5加密值
     * @return 根据不同的结果返回不同的实体信息
     */
    SeckillExecution executeSeckillProcedure(long seckillId,long userPhone,String md5);
    

    为什么这个方法不需要抛出异常?

    原本没有调用存储过程的执行秒杀操作之所以要抛出RuntimException,是为了让Spring事务管理器能够在秒杀不成功的时候进行回滚操作。而现在我们使用了存储过程,有关事务的提交或回滚已经在procedure里完成了,前面也解释了不需要再使用到Spring的事务了,既然如此,我们也就不需要在这个方法里抛出异常来让Spring帮我们回滚了。

    SeckillServiceImpl里实现这个方法

    我们需要使用到第三方工具类,所以在pom.xml里导入commons-collections工具类

    <!--导入apache工具类-->
    <dependency>
        <groupId>commons-collections</groupId>
        <artifactId>commons-collections</artifactId>
        <version>3.2.2</version>
    </dependency>
    

    在接口的实现类里对executeSeckillProcedure进行实现

    @Override
    public SeckillExecution executeSeckillProcedure(long seckillId, long userPhone, String md5) {
        if (md5 == null || !md5.equals(getMD5(seckillId))) {
            return new SeckillExecution(seckillId, SeckillStatEnum.DATE_REWRITE);
        }
        Date killTime = new Date();
        Map<String, Object> map = new HashMap<>();
        map.put("seckillId", seckillId);
        map.put("phone", userPhone);
        map.put("killTime", killTime);
        map.put("result", null);
        // 执行储存过程,result被复制
        seckillDao.killByProcedure(map);
        // 获取result
        int result = MapUtils.getInteger(map, "result", -2);
        if (result == 1) {
            SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
            return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, successKilled);
        } else {
            return new SeckillExecution(seckillId, SeckillStatEnum.stateOf(result));
        }
    }
    

    接着对该方法进行测试,在原本的SeckillServiceTest测试类里添加测试方法

    @Test
    public void executeSeckillProcedure(){
        long seckillId = 1001;
        long phone = 13680115101L;
        Exposer exposer = seckillService.exportSeckillUrl(seckillId);
        if (exposer.isExposed()) {
            String md5 = exposer.getMd5();
            SeckillExecution execution = seckillService.executeSeckillProcedure(seckillId, phone, md5);
            logger.info("execution={}", execution);
        }
    }
    

    经过测试,发现没有问题,测试通过。然后我们需要把Controller里的执行秒杀操作改成调用存储过程的方法。

        @RequestMapping(value = "/{seckillId}/{md5}/execution",
                method = RequestMethod.POST,
                produces = {"application/json;charset=UTF-8"})
        @ResponseBody
        public SeckillResult<SeckillExecution> execute(@PathVariable("seckillId") Long seckillId,
                                                       @PathVariable("md5") String md5,
                                                       @CookieValue(value = "userPhone",required = false) Long userPhone)
        {
            if (userPhone==null)
            {
                return new SeckillResult<SeckillExecution>(false,"未注册");
            }
    
            try {
                //这里改为调用存储过程
    //            SeckillExecution execution = seckillService.executeSeckill(seckillId, userPhone, md5);
                SeckillExecution execution = seckillService.executeSeckillProcedure(seckillId, userPhone, md5);
                return new SeckillResult<SeckillExecution>(true, execution);
            }catch (RepeatKillException e1)
            {
                SeckillExecution execution=new SeckillExecution(seckillId, SeckillStatEnum.REPEAT_KILL);
                return new SeckillResult<SeckillExecution>(true,execution);
            }catch (SeckillCloseException e2)
            {
                SeckillExecution execution=new SeckillExecution(seckillId, SeckillStatEnum.END);
                return new SeckillResult<SeckillExecution>(true,execution);
            }
            catch (Exception e)
            {
                SeckillExecution execution=new SeckillExecution(seckillId, SeckillStatEnum.INNER_ERROR);
                return new SeckillResult<SeckillExecution>(true,execution);
            }
        }
    

    存储过程优化总结

    1. 存储过程优化:事务行级锁持有的时间
    2. 不要过度依赖存储过程
    3. 简单的逻辑依赖存储过程
    4. QPS:一个秒杀单6000/qps

    经过简单优化和深度优化之后,本项目大概能达到一个秒杀单6000qps(慕课网视频中张老师说的),这个数据对于一个秒杀商品来说其实已经挺ok了,注意这里是指同一个秒杀商品6000qps,如果是不同商品不存在行级锁竞争的问题。

    3.3 系统部署架构

    系统可能用到的服务

    CDN:放置一些静态化资源,或者可以将动态数据分离。一些js依赖直接用公网的CDN,自己开发的一些页面也做静态化处理推送到CDN。用户在CDN获取到的数据不需要再访问我们的服务器,动静态分离可以降低服务器请求量。比如秒杀详情页,做成HTML放在cdn上,动态数据可以通过ajax请求后台获取。

    Nginx:作为http服务器,响应客户请求,为后端的servlet容器做反向代理,以达到负载均衡的效果。

    Redis:用来做服务器端的缓存,通过Jedis提供的API来达到热点数据的一个快速存取的过程,减少数据库的请求量。

    MySQL:保证秒杀过程的数据一致性与完整性。

    智能DNS解析+智能CDN加速+Nginx并发+Redis缓存+MySQL分库分表

    大型系统部署架构

    大型系统部署架构,逻辑集群就是开发的部分。

    1. Nginx做负载均衡
    2. 分库分表:在秒杀系统中,一般通过关键的秒杀商品id取模进行分库分表,以512为一张表,1024为一张表。分库分表一般采用开源架构,如阿里巴巴的tddl分库分表框架。
    3. 统计分析:一般使用hadoop等架构进行分析

    在这样一个架构中,可能参与的角色如下:

    项目角色

    本节结语

    至此,关于该SSM实战项目——Java高并发秒杀API已经全部完成,感谢观看本文。

    项目笔记相关链接

    项目源码

    项目视频教程链接

    这是慕课网上的一个免费项目教学视频,名为Java高并发秒杀API,一共有如下四节课程,附带视频传送门(在视频中老师是用IDEA,本文用的是Eclipse)

    高并发的相关推荐

    1. java系统高并发解决方案(转载)
    展开全文
  • 为什么要分库分表(设计高并发系统的时候,数据库层面该如何设计)?用过哪些分库分表中间件?不同的分库分表中间件都有什么优点和缺点?你们具体是如何对数据库如何进行垂直拆分或水平拆分的? 面试题剖析 为什么...

    1. 为什么要分库分表(设计高并发系统的时候,数据库层面该如何设计)?用过哪些分库分表中间件?不同的分库分表中间件都有什么优点和缺点?你们具体是如何对数据库如何进行垂直拆分或水平拆分的?

    面试题剖析

    为什么要分库分表?(设计高并发系统的时候,数据库层面该如何设计?)

    说白了,分库分表是两回事儿,大家可别搞混了,可能是光分库不分表,也可能是光分表不分库,都有可能。

    我先给大家抛出来一个场景。

    假如我们现在是一个小创业公司(或者是一个 BAT 公司刚兴起的一个新部门),现在注册用户就 20 万,每天活跃用户就 1 万,每天单表数据量就 1000,然后高峰期每秒钟并发请求最多就 10。天,就这种系统,随便找一个有几年工作经验的,然后带几个刚培训出来的,随便干干都可以。

    结果没想到我们运气居然这么好,碰上个 CEO 带着我们走上了康庄大道,业务发展迅猛,过了几个月,注册用户数达到了 2000 万!每天活跃用户数 100 万!每天单表数据量 10 万条!高峰期每秒最大请求达到 1000!同时公司还顺带着融资了两轮,进账了几个亿人民币啊!公司估值达到了惊人的几亿美金!这是小独角兽的节奏!

    好吧,没事,现在大家感觉压力已经有点大了,为啥呢?因为每天多 10 万条数据,一个月就多 300 万条数据,现在咱们单表已经几百万数据了,马上就破千万了。但是勉强还能撑着。高峰期请求现在是 1000,咱们线上部署了几台机器,负载均衡搞了一下,数据库撑 1000QPS 也还凑合。但是大家现在开始感觉有点担心了,接下来咋整呢…

    再接下来几个月,我的天,CEO 太牛逼了,公司用户数已经达到 1 亿,公司继续融资几十亿人民币啊!公司估值达到了惊人的几十亿美金,成为了国内今年最牛逼的明星创业公司!天,我们太幸运了。

    但是我们同时也是不幸的,因为此时每天活跃用户数上千万,每天单表新增数据多达 50 万,目前一个表总数据量都已经达到了两三千万了!扛不住啊!数据库磁盘容量不断消耗掉!高峰期并发达到惊人的 5000~8000!别开玩笑了,哥。我跟你保证,你的系统支撑不到现在,已经挂掉了!

    好吧,所以你看到这里差不多就理解分库分表是怎么回事儿了,实际上这是跟着你的公司业务发展走的,你公司业务发展越好,用户就越多,数据量越大,请求量越大,那你单个数据库一定扛不住。

    分表

    比如你单表都几千万数据了,你确定你能扛住么?绝对不行,单表数据量太大,会极大影响你的 sql 执行的性能,到了后面你的 sql 可能就跑的很慢了。一般来说,就以我的经验来看,单表到几百万的时候,性能就会相对差一些了,你就得分表了。

    分表是啥意思?就是把一个表的数据放到多个表中,然后查询的时候你就查一个表。比如按照用户 id 来分表,将一个用户的数据就放在一个表中。然后操作的时候你对一个用户就操作那个表就好了。这样可以控制每个表的数据量在可控的范围内,比如每个表就固定在 200 万以内。

    分库

    分库是啥意思?就是你一个库一般我们经验而言,最多支撑到并发 2000,一定要扩容了,而且一个健康的单库并发值你最好保持在每秒 1000 左右,不要太大。那么你可以将一个库的数据拆分到多个库中,访问的时候就访问一个库好了。

    这就是所谓的分库分表,为啥要分库分表?你明白了吧。

    # 分库分表前 分库分表后
    并发支撑情况 MySQL 单机部署,扛不住高并发 MySQL从单机到多机,能承受的并发增加了多倍
    磁盘使用情况 MySQL 单机磁盘容量几乎撑满 拆分为多个库,数据库服务器磁盘使用率大大降低
    SQL 执行性能 单表数据量太大,SQL 越跑越慢 单表数据量减少,SQL 执行效率明显提升

    用过哪些分库分表中间件?不同的分库分表中间件都有什么优点和缺点?

    这个其实就是看看你了解哪些分库分表的中间件,各个中间件的优缺点是啥?然后你用过哪些分库分表的中间件。

    比较常见的包括:

    • cobar
    • TDDL
    • atlas
    • sharding-jdbc
    • mycat

    cobar

    阿里 b2b 团队开发和开源的,属于 proxy 层方案。早些年还可以用,但是最近几年都没更新了,基本没啥人用,差不多算是被抛弃的状态吧。而且不支持读写分离、存储过程、跨库 join 和分页等操作。

    TDDL

    淘宝团队开发的,属于 client 层方案。支持基本的 crud 语法和读写分离,但不支持 join、多表查询等语法。目前使用的也不多,因为还依赖淘宝的 diamond 配置管理系统。

    atlas

    360 开源的,属于 proxy 层方案,以前是有一些公司在用的,但是确实有一个很大的问题就是社区最新的维护都在 5 年前了。所以,现在用的公司基本也很少了。

    sharding-jdbc

    当当开源的,属于 client 层方案。确实之前用的还比较多一些,因为 SQL 语法支持也比较多,没有太多限制,而且目前推出到了 2.0 版本,支持分库分表、读写分离、分布式 id 生成、柔性事务(最大努力送达型事务、TCC 事务)。而且确实之前使用的公司会比较多一些(这个在官网有登记使用的公司,可以看到从 2017 年一直到现在,是有不少公司在用的),目前社区也还一直在开发和维护,还算是比较活跃,个人认为算是一个现在也可以选择的方案

    mycat

    基于 cobar 改造的,属于 proxy 层方案,支持的功能非常完善,而且目前应该是非常火的而且不断流行的数据库中间件,社区很活跃,也有一些公司开始在用了。但是确实相比于 sharding jdbc 来说,年轻一些,经历的锤炼少一些。

    总结

    综上,现在其实建议考量的,就是 sharding-jdbc 和 mycat,这两个都可以去考虑使用。

    sharding-jdbc 这种 client 层方案的优点在于不用部署,运维成本低,不需要代理层的二次转发请求,性能很高,但是如果遇到升级啥的需要各个系统都重新升级版本再发布,各个系统都需要耦合 sharding-jdbc 的依赖;

    mycat 这种 proxy 层方案的缺点在于需要部署,自己运维一套中间件,运维成本高,但是好处在于对于各个项目是透明的,如果遇到升级之类的都是自己中间件那里搞就行了。

    通常来说,这两个方案其实都可以选用,但是我个人建议中小型公司选用 sharding-jdbc,client 层方案轻便,而且维护成本低,不需要额外增派人手,而且中小型公司系统复杂度会低一些,项目也没那么多;但是中大型公司最好还是选用 mycat 这类 proxy 层方案,因为可能大公司系统和项目非常多,团队很大,人员充足,那么最好是专门弄个人来研究和维护 mycat,然后大量项目直接透明使用即可。

    你们具体是如何对数据库如何进行垂直拆分或水平拆分的?

    水平拆分的意思,就是把一个表的数据给弄到多个库的多个表里去,但是每个库的表结构都一样,只不过每个库表放的数据是不同的,所有库表的数据加起来就是全部数据。水平拆分的意义,就是将数据均匀放更多的库里,然后用多个库来抗更高的并发,还有就是用多个库的存储容量来进行扩容。

    database-split-horizon

    垂直拆分的意思,就是把一个有很多字段的表给拆分成多个表或者是多个库上去。每个库表的结构都不一样,每个库表都包含部分字段。一般来说,会将较少的访问频率很高的字段放到一个表里去,然后将较多的访问频率很低的字段放到另外一个表里去。因为数据库是有缓存的,你访问频率高的行字段越少,就可以在缓存里缓存更多的行,性能就越好。这个一般在表层面做的较多一些。

    database-split-vertically

    这个其实挺常见的,不一定我说,大家很多同学可能自己都做过,把一个大表拆开,订单表、订单支付表、订单商品表。

    还有表层面的拆分,就是分表,将一个表变成 N 个表,就是让每个表的数据量控制在一定范围内,保证 SQL 的性能。否则单表数据量越大,SQL 性能就越差。一般是 200 万行左右,不要太多,但是也得看具体你怎么操作,也可能是 500 万,或者是 100 万。你的SQL越复杂,就最好让单表行数越少。

    好了,无论分库还是分表,上面说的那些数据库中间件都是可以支持的。就是基本上那些中间件可以做到你分库分表之后,中间件可以根据你指定的某个字段值,比如说 userid,自动路由到对应的库上去,然后再自动路由到对应的表里去

    你就得考虑一下,你的项目里该如何分库分表?一般来说,垂直拆分,你可以在表层面来做,对一些字段特别多的表做一下拆分;水平拆分,你可以说是并发承载不了,或者是数据量太大,容量承载不了,你给拆了,按什么字段来拆,你自己想好;分表,你考虑一下,你如果哪怕是拆到每个库里去,并发和容量都ok了,但是每个库的表还是太大了,那么你就分表,将这个表分开,保证每个表的数据量并不是很大。

    而且这儿还有两种分库分表的方式

    • 一种是按照 range 来分,就是每个库一段连续的数据,这个一般是按比如时间范围来的,但是这种一般较少用,因为很容易产生热点问题,大量的流量都打在最新的数据上了。
    • 或者是按照某个字段hash一下均匀分散,这个较为常用。

    range 来分,好处在于说,扩容的时候很简单,因为你只要预备好,给每个月都准备一个库就可以了,到了一个新的月份的时候,自然而然,就会写新的库了;缺点,但是大部分的请求,都是访问最新的数据。实际生产用 range,要看场景。

    hash 分发,好处在于说,可以平均分配每个库的数据量和请求压力;坏处在于说扩容起来比较麻烦,会有一个数据迁移的过程,之前的数据需要重新计算 hash 值重新分配到不同的库或表。

    2. 现在有一个未分库分表的系统,未来要分库分表,如何设计才可以让系统从未分库分表动态切换到分库分表上?

    面试题剖析

    这个其实从 low 到高大上有好几种方案,我们都玩儿过,我都给你说一下。

    停机迁移方案

    我先给你说一个最 low 的方案,就是很简单,大家伙儿凌晨 12 点开始运维,网站或者 app 挂个公告,说 0 点到早上 6 点进行运维,无法访问。

    接着到 0 点停机,系统停掉,没有流量写入了,此时老的单库单表数据库静止了。然后你之前得写好一个导数的一次性工具,此时直接跑起来,然后将单库单表的数据哗哗哗读出来,写到分库分表里面去。

    导数完了之后,就 ok 了,修改系统的数据库连接配置啥的,包括可能代码和 SQL 也许有修改,那你就用最新的代码,然后直接启动连到新的分库分表上去。

    验证一下,ok了,完美,大家伸个懒腰,看看看凌晨 4 点钟的北京夜景,打个滴滴回家吧。

    但是这个方案比较 low,谁都能干,我们来看看高大上一点的方案。

    database-shard-method-1

    双写迁移方案

    这个是我们常用的一种迁移方案,比较靠谱一些,不用停机,不用看北京凌晨 4 点的风景。

    简单来说,就是在线上系统里面,之前所有写库的地方,增删改操作,除了对老库增删改,都加上对新库的增删改,这就是所谓的双写,同时写俩库,老库和新库。

    然后系统部署之后,新库数据差太远,用之前说的导数工具,跑起来读老库数据写新库,写的时候要根据 gmt_modified 这类字段判断这条数据最后修改的时间,除非是读出来的数据在新库里没有,或者是比新库的数据新才会写。简单来说,就是不允许用老数据覆盖新数据。

    导完一轮之后,有可能数据还是存在不一致,那么就程序自动做一轮校验,比对新老库每个表的每条数据,接着如果有不一样的,就针对那些不一样的,从老库读数据再次写。反复循环,直到两个库每个表的数据都完全一致为止。

    接着当数据完全一致了,就 ok 了,基于仅仅使用分库分表的最新代码,重新部署一次,不就仅仅基于分库分表在操作了么,还没有几个小时的停机时间,很稳。所以现在基本玩儿数据迁移之类的,都是这么干的。
    database-shard-method-2

    3. 如何设计可以动态扩容缩容的分库分表方案?

    考点分析

    对于分库分表来说,主要是面对以下问题:

    • 选择一个数据库中间件,调研、学习、测试;
    • 设计你的分库分表的一个方案,你要分成多少个库,每个库分成多少个表,比如 3 个库,每个库 4 个表;
    • 基于选择好的数据库中间件,以及在测试环境建立好的分库分表的环境,然后测试一下能否正常进行分库分表的读写;
    • 完成单库单表到分库分表的迁移,双写方案;
    • 线上系统开始基于分库分表对外提供服务;
    • 扩容了,扩容成 6 个库,每个库需要 12 个表,你怎么来增加更多库和表呢?

    这个是你必须面对的一个事儿,就是你已经弄好分库分表方案了,然后一堆库和表都建好了,基于分库分表中间件的代码开发啥的都好了,测试都 ok 了,数据能均匀分布到各个库和各个表里去,而且接着你还通过双写的方案咔嚓一下上了系统,已经直接基于分库分表方案在搞了。

    那么现在问题来了,你现在这些库和表又支撑不住了,要继续扩容咋办?这个可能就是说你的每个库的容量又快满了,或者是你的表数据量又太大了,也可能是你每个库的写并发太高了,你得继续扩容。

    这都是玩儿分库分表线上必须经历的事儿。

    面试题剖析

    停机扩容(不推荐)

    这个方案就跟停机迁移一样,步骤几乎一致,唯一的一点就是那个导数的工具,是把现有库表的数据抽出来慢慢倒入到新的库和表里去。但是最好别这么玩儿,有点不太靠谱,因为既然分库分表就说明数据量实在是太大了,可能多达几亿条,甚至几十亿,你这么玩儿,可能会出问题。

    从单库单表迁移到分库分表的时候,数据量并不是很大,单表最大也就两三千万。那么你写个工具,多弄几台机器并行跑,1小时数据就导完了。这没有问题。

    如果 3 个库 + 12 个表,跑了一段时间了,数据量都 1~2 亿了。光是导 2 亿数据,都要导个几个小时,6 点,刚刚导完数据,还要搞后续的修改配置,重启系统,测试验证,10 点才可以搞完。所以不能这么搞。

    优化后的方案

    一开始上来就是 32 个库,每个库 32 个表,那么总共是 1024 张表。

    我可以告诉各位同学,这个分法,第一,基本上国内的互联网肯定都是够用了,第二,无论是并发支撑还是数据量支撑都没问题。

    每个库正常承载的写入并发量是 1000,那么 32 个库就可以承载32 * 1000 = 32000 的写并发,如果每个库承载 1500 的写并发,32 * 1500 = 48000 的写并发,接近 5万/s 的写入并发,前面再加一个MQ,削峰,每秒写入 MQ 8 万条数据,每秒消费 5 万条数据。

    有些除非是国内排名非常靠前的这些公司,他们的最核心的系统的数据库,可能会出现几百台数据库的这么一个规模,128个库,256个库,512个库。

    1024 张表,假设每个表放 500 万数据,在 MySQL 里可以放 50 亿条数据。

    每秒的 5 万写并发,总共 50 亿条数据,对于国内大部分的互联网公司来说,其实一般来说都够了。

    谈分库分表的扩容,第一次分库分表,就一次性给他分个够,32 个库,1024 张表,可能对大部分的中小型互联网公司来说,已经可以支撑好几年了。

    一个实践是利用 32 * 32 来分库分表,即分为 32 个库,每个库里一个表分为 32 张表。一共就是 1024 张表。根据某个 id 先根据 32 取模路由到库,再根据 32 取模路由到库里的表。

    orderId id % 32 (库) id / 32 % 32 (表)
    259 3 8
    1189 5 5
    352 0 11
    4593 17 15

    刚开始的时候,这个库可能就是逻辑库,建在一个数据库上的,就是一个mysql服务器可能建了 n 个库,比如 32 个库。后面如果要拆分,就是不断在库和 mysql 服务器之间做迁移就可以了。然后系统配合改一下配置即可。

    比如说最多可以扩展到32个数据库服务器,每个数据库服务器是一个库。如果还是不够?最多可以扩展到 1024 个数据库服务器,每个数据库服务器上面一个库一个表。因为最多是1024个表。

    这么搞,是不用自己写代码做数据迁移的,都交给 dba 来搞好了,但是 dba 确实是需要做一些库表迁移的工作,但是总比你自己写代码,然后抽数据导数据来的效率高得多吧。

    哪怕是要减少库的数量,也很简单,其实说白了就是按倍数缩容就可以了,然后修改一下路由规则。

    这里对步骤做一个总结:

    1. 设定好几台数据库服务器,每台服务器上几个库,每个库多少个表,推荐是 32库 * 32表,对于大部分公司来说,可能几年都够了。
    2. 路由的规则,orderId 模 32 = 库,orderId / 32 模 32 = 表
    3. 扩容的时候,申请增加更多的数据库服务器,装好 mysql,呈倍数扩容,4 台服务器,扩到 8 台服务器,再到 16 台服务器。
    4. 由 dba 负责将原先数据库服务器的库,迁移到新的数据库服务器上去,库迁移是有一些便捷的工具的。
    5. 我们这边就是修改一下配置,调整迁移的库所在数据库服务器的地址。
    6. 重新发布系统,上线,原先的路由规则变都不用变,直接可以基于 n 倍的数据库服务器的资源,继续进行线上系统的提供服务。

    4. 分库分表之后,id 主键如何处理?

    考点分析

    其实这是分库分表之后你必然要面对的一个问题,就是 id 咋生成?因为要是分成多个表之后,每个表都是从 1 开始累加,那肯定不对啊,需要一个全局唯一的 id 来支持。所以这都是你实际生产环境中必须考虑的问题。

    面试题剖析

    基于数据库的实现方案

    数据库自增 id

    这个就是说你的系统里每次得到一个 id,都是往一个库的一个表里插入一条没什么业务含义的数据,然后获取一个数据库自增的一个 id。拿到这个 id 之后再往对应的分库分表里去写入。

    这个方案的好处就是方便简单,谁都会用;缺点就是单库生成自增 id,要是高并发的话,就会有瓶颈的;如果你硬是要改进一下,那么就专门开一个服务出来,这个服务每次就拿到当前 id 最大值,然后自己递增几个 id,一次性返回一批 id,然后再把当前最大 id 值修改成递增几个 id 之后的一个值;但是无论如何都是基于单个数据库

    适合的场景:你分库分表就俩原因,要不就是单库并发太高,要不就是单库数据量太大;除非是你并发不高,但是数据量太大导致的分库分表扩容,你可以用这个方案,因为可能每秒最高并发最多就几百,那么就走单独的一个库和表生成自增主键即可。

    设置数据库 sequence 或者表自增字段步长

    可以通过设置数据库 sequence 或者表的自增字段步长来进行水平伸缩。

    比如说,现在有 8 个服务节点,每个服务节点使用一个 sequence 功能来产生 ID,每个 sequence 的起始 ID 不同,并且依次递增,步长都是 8。

    database-id-sequence-step

    适合的场景:在用户防止产生的 ID 重复时,这种方案实现起来比较简单,也能达到性能目标。但是服务节点固定,步长也固定,将来如果还要增加服务节点,就不好搞了。

    UUID

    好处就是本地生成,不要基于数据库来了;不好之处就是,UUID 太长了、占用空间大,作为主键性能太差了;更重要的是,UUID 不具有有序性,会导致 B+ 树索引在写的时候有过多的随机写操作(连续的 ID 可以产生部分顺序写),还有,由于在写的时候不能产生有顺序的 append 操作,而需要进行 insert 操作,将会读取整个 B+ 树节点到内存,在插入这条记录后会将整个节点写回磁盘,这种操作在记录占用空间比较大的情况下,性能下降明显。

    适合的场景:如果你是要随机生成个什么文件名、编号之类的,你可以用 UUID,但是作为主键是不能用 UUID 的。

    UUID.randomUUID().toString().replace(-, “”) -> sfsdf23423rr234sfdaf
    

    获取系统当前时间

    这个就是获取当前时间即可,但是问题是,并发很高的时候,比如一秒并发几千,会有重复的情况,这个是肯定不合适的。基本就不用考虑了。

    适合的场景:一般如果用这个方案,是将当前时间跟很多其他的业务字段拼接起来,作为一个 id,如果业务上你觉得可以接受,那么也是可以的。你可以将别的业务字段值跟当前时间拼接起来,组成一个全局唯一的编号。

    snowflake 算法

    snowflake 算法是 twitter 开源的分布式 id 生成算法,采用 Scala 语言实现,是把一个 64 位的 long 型的 id,1 个 bit 是不用的,用其中的 41 bit 作为毫秒数,用 10 bit 作为工作机器 id,12 bit 作为序列号。

    • 1 bit:不用,为啥呢?因为二进制里第一个 bit 为如果是 1,那么都是负数,但是我们生成的 id 都是正数,所以第一个 bit 统一都是 0。
    • 41 bit:表示的是时间戳,单位是毫秒。41 bit 可以表示的数字多达 2^41 - 1,也就是可以标识 2^41 - 1 个毫秒值,换算成年就是表示69年的时间。
    • 10 bit:记录工作机器 id,代表的是这个服务最多可以部署在 2^10台机器上哪,也就是1024台机器。但是 10 bit 里 5 个 bit 代表机房 id,5 个 bit 代表机器 id。意思就是最多代表 2^5个机房(32个机房),每个机房里可以代表 2^5 个机器(32台机器)。
    • 12 bit:这个是用来记录同一个毫秒内产生的不同 id,12 bit 可以代表的最大正整数是 2^12 - 1 = 4096,也就是说可以用这个 12 bit 代表的数字来区分同一个毫秒内的 4096 个不同的 id。
    0 | 0001100 10100010 10111110 10001001 01011100 00 | 10001 | 1 1001 | 0000 00000000
    
    public class IdWorker {
    
        private long workerId;
        private long datacenterId;
        private long sequence;
    
        public IdWorker(long workerId, long datacenterId, long sequence) {
            // sanity check for workerId
            // 这儿不就检查了一下,要求就是你传递进来的机房id和机器id不能超过32,不能小于0
            if (workerId > maxWorkerId || workerId < 0) {
                throw new IllegalArgumentException(
                        String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
            }
            if (datacenterId > maxDatacenterId || datacenterId < 0) {
                throw new IllegalArgumentException(
                        String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
            }
            System.out.printf(
                    "worker starting. timestamp left shift %d, datacenter id bits %d, worker id bits %d, sequence bits %d, workerid %d",
                    timestampLeftShift, datacenterIdBits, workerIdBits, sequenceBits, workerId);
    
            this.workerId = workerId;
            this.datacenterId = datacenterId;
            this.sequence = sequence;
        }
    
        private long twepoch = 1288834974657L;
    
        private long workerIdBits = 5L;
        private long datacenterIdBits = 5L;
    
        // 这个是二进制运算,就是 5 bit最多只能有31个数字,也就是说机器id最多只能是32以内
        private long maxWorkerId = -1L ^ (-1L << workerIdBits);
    
        // 这个是一个意思,就是 5 bit最多只能有31个数字,机房id最多只能是32以内
        private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
        private long sequenceBits = 12L;
    
        private long workerIdShift = sequenceBits;
        private long datacenterIdShift = sequenceBits + workerIdBits;
        private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
        private long sequenceMask = -1L ^ (-1L << sequenceBits);
    
        private long lastTimestamp = -1L;
    
        public long getWorkerId() {
            return workerId;
        }
    
        public long getDatacenterId() {
            return datacenterId;
        }
    
        public long getTimestamp() {
            return System.currentTimeMillis();
        }
    
        public synchronized long nextId() {
            // 这儿就是获取当前时间戳,单位是毫秒
            long timestamp = timeGen();
    
            if (timestamp < lastTimestamp) {
                System.err.printf("clock is moving backwards.  Rejecting requests until %d.", lastTimestamp);
                throw new RuntimeException(String.format(
                        "Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
            }
    
            if (lastTimestamp == timestamp) {
                // 这个意思是说一个毫秒内最多只能有4096个数字
                // 无论你传递多少进来,这个位运算保证始终就是在4096这个范围内,避免你自己传递个sequence超过了4096这个范围
                sequence = (sequence + 1) & sequenceMask;
                if (sequence == 0) {
                    timestamp = tilNextMillis(lastTimestamp);
                }
            } else {
                sequence = 0;
            }
    
            // 这儿记录一下最近一次生成id的时间戳,单位是毫秒
            lastTimestamp = timestamp;
    
            // 这儿就是将时间戳左移,放到 41 bit那儿;
            // 将机房 id左移放到 5 bit那儿;
            // 将机器id左移放到5 bit那儿;将序号放最后12 bit;
            // 最后拼接起来成一个 64 bit的二进制数字,转换成 10 进制就是个 long 型
            return ((timestamp - twepoch) << timestampLeftShift) | (datacenterId << datacenterIdShift)
                    | (workerId << workerIdShift) | sequence;
        }
    
        private long tilNextMillis(long lastTimestamp) {
            long timestamp = timeGen();
            while (timestamp <= lastTimestamp) {
                timestamp = timeGen();
            }
            return timestamp;
        }
    
        private long timeGen() {
            return System.currentTimeMillis();
        }
    
        // ---------------测试---------------
        public static void main(String[] args) {
            IdWorker worker = new IdWorker(1, 1, 1);
            for (int i = 0; i < 30; i++) {
                System.out.println(worker.nextId());
            }
        }
    
    }
    
    

    怎么说呢,大概这个意思吧,就是说 41 bit 是当前毫秒单位的一个时间戳,就这意思;然后 5 bit 是你传递进来的一个机房 id(但是最大只能是 32 以内),另外 5 bit 是你传递进来的机器 id(但是最大只能是 32 以内),剩下的那个 12 bit序列号,就是如果跟你上次生成 id 的时间还在一个毫秒内,那么会把顺序给你累加,最多在 4096 个序号以内。

    所以你自己利用这个工具类,自己搞一个服务,然后对每个机房的每个机器都初始化这么一个东西,刚开始这个机房的这个机器的序号就是 0。然后每次接收到一个请求,说这个机房的这个机器要生成一个 id,你就找到对应的 Worker 生成。

    利用这个 snowflake 算法,你可以开发自己公司的服务,甚至对于机房 id 和机器 id,反正给你预留了 5 bit + 5 bit,你换成别的有业务含义的东西也可以的。

    这个 snowflake 算法相对来说还是比较靠谱的,所以你要真是搞分布式 id 生成,如果是高并发啥的,那么用这个应该性能比较好,一般每秒几万并发的场景,也足够你用了。

    关注我!这里只有干货!
    本文原创地址:https://jsbintask.cn/2019/02/17/interview/interview-db-shard/,转载请注明出处。

    展开全文
  • 并发编程篇:java 高并发面试题

    万次阅读 多人点赞 2018-02-28 21:43:18
    3、java thread状态 NEW 状态是指线程刚创建, 尚未启动 RUNNABLE Java线程中将就绪(ready)和运行中(running)两种状态笼统的成为“运行”。线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。...
  • java高并发解决思路

    千次阅读 2017-03-10 14:07:54
    java高并发解决方案
  • 数据库学习:高并发数据库设计

    万次阅读 2017-11-13 13:21:19
    数据库学习:高并发数据库设计 随着乐视硬件抢购的不断升级,乐视集团支付面临的请求压力百倍乃至千倍的暴增。作为商品购买的最后一环,保证用户快速稳定的完成支付尤为重要。所以在15年11月,我们对整个支付...
  • java高并发详解

    千次阅读 2018-02-28 23:59:11
    对于开发的网站,如果网站的访问量非常大,那么我们应该考虑相关的、并发访问问题,并发是绝大部分程序员头疼的问题; 为了更好的理解并发和同步,先明白两个重要的概念:异步和同步; &nbsp;1、同步和异...
  • memcache解决高并发数据库瓶颈问题

    千次阅读 2011-05-03 18:35:00
    高并发面临的问题  对于高并发访问的Web应用程序来说,数据库存取瓶颈一直是个令人头疼的问题。特别当你的程序架构还是建立在单数据库模式,而一个数据池连接数峰值已经达到500的时候,那你的程序运行...
  • 读多写少,高并发,资源冲突(短时间内突发性高并发请求) 3 应对策略 读多写少 缓存:把热点数据丢到缓存中,浏览器缓存,本地缓存等 高并发 限流:延迟处理策略,拒绝访问 负载均衡:使用nginx实现反向代理和...
  • 【java】总结java高并发的处理

    千次阅读 热门讨论 2019-01-18 10:32:36
    什么是高并发高并发(High Concurrency)是互联网分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计保证系统能够同时并行处理很多请求。 高并发相关常用的一些指标有响应时间(Response Time),...
  • java高并发处理

    万次阅读 多人点赞 2017-02-08 09:53:13
    java web项目开发者,最难解决的是高并发问题,我为搞并发解决方案,想出了一个解决方案。  a.应用层面:读写分离、缓存、队列、集群、令牌、系统拆分、隔离、系统升级(可水平扩容方向)。  b.时间换空间:...
  • java高并发

    千次阅读 多人点赞 2018-02-02 18:11:32
    并发
  • 对于我们开发的网站,如果网站的访问量非常大的话,我们就需要考虑相关的并发访问问题了。而且并发问题也是中高级工程师面试中必问的问题,今天我们就来系统学习一下。 为了更好的理解并发和同步,我们先学习两个...
  • 高并发数据库设计

    千次阅读 2018-09-17 20:12:58
    所以数据库的快速恢复成了数据库高可用的重中之重,试想一下如果我们能在数据库出故障的1秒之内完成数据库恢复,修复不一致的数据和成本也会大大降低。 下图是一个最经典的主从结构: 上图中有1台web服务器...
  • java高并发实际处理简介

    千次阅读 2018-06-05 09:57:20
    java高并发简介,这里我讲点干货,带水分的都除掉啊。如果喜欢,请点赞,给予编写动力。秒杀锁定图平时项目中,如果多个客户同时需要修改或者审批同一个业务数据的时候,这个时候我们需要考虑脏数据和数据不可重复读...
  • Java高并发系统的限流策略

    万次阅读 2018-05-12 23:03:33
    概要在大数据量高并发访问时,经常会出现服务或接口面对暴涨的请求而不可用的情况,甚至引发连锁反映导致整个系统崩溃。此时你需要使用的技术手段之一就是限流,当请求达到一定的并发数或速率,就进行等待、排队、...
  • Java高并发的方式解决

    千次阅读 2018-03-19 16:55:17
    对于我们开发的网站,如果网站的访问量非常大的话,那么我们就需要考虑相关的并发访问问题了。而并发问题是绝大部分的程序员头疼的问题, 为了更好的理解并发和同步,我们需要先明白两个重要的概念:同步和异步 1...
  • java高并发解决方案

    万次阅读 2016-07-20 17:53:31
    高并发的时候是有很多用户在访问,导致出现系统数据不正确、丢失数据现象,所以想到 的是用队列解决,其实队列解决的方式也可以处理,比如我们在竞拍商品、转发评论微博或者是秒杀商品等,同一时间访问量特别大,...
  • Java 高并发缓存与Guava Cache

    万次阅读 2014-11-15 15:23:05
    今天我们介绍的是本地缓存缓存,我们这边采用java.util.concurrent....我们先不考虑内存元素回收或者在保存数据会出现内存溢出的情况,我们用ConcurrentHashMap模拟本地缓存,当在高并发环境一下,会出现一些什么问题?
  • 关于java高并发问题总结

    千次阅读 2017-01-23 22:26:02
    关于java高并发问题总结文章是参考了很多优秀博客的内容,算作自己的一个心得笔记java代码层面来控制多线程并发的问题这一部分在学习java基础时就已经了解,主要是围绕着synchronized关键字来对公共资源进行锁定,...
  • Java解决多用户高并发访问

    千次阅读 2017-02-27 17:07:51
    Java集群--大型网站是怎样解决多用户高并发访问的  时间过得真快,再次登录博客园来写博,才发现距离上次的写博时间已经过去了一个月了,虽然是因为自己找了实习,但这也说明自己对时间的掌控能力还是没...
  • Java处理高并发访问的处理总结

    万次阅读 2017-08-29 23:49:11
    结合之前做的一个网站,项目中分了几个子项目,主要用到Redis,service(server)层和control层分离,有做了缓存,页面也是进行静态化(htm和freemarker),仔细想想,整个项目基本吻合高并发,负载均衡的处理。...
  • Java高并发优化之页面缓存

    千次阅读 2018-07-18 17:50:20
    页面缓存是将动态页面直接生成静态的页面放在服务器端,用户调取相同页面时,静态页面将直接下载到客户端,不再需要通过程序的运行和数据库访问,大大节约了服务器的负载。每次访问页面时,会检测相应的缓存页面...
  • 对于我们所研发的网站,若网站的访问量非常大,那么我们必须考虑相关的并发访问问题,而并发问题是绝大部分的程序员头疼的问题。 本 Chat 带你领略一下相关概念和解决方案。本 Chat 文章部分观点来自网站整理,介意...
  • java高并发:CAS无锁原理及广泛应用

    万次阅读 多人点赞 2016-12-20 12:53:39
    应对高并发需要在各个技术层面进行合理的设计和技术选型才可以。本文只讲述微观层面是如何应对多线程高并发的,介绍著名的CAS原理以及其广泛应用。 本文中jdk版本使用的是jdk1.7.0_55. 不同版本实现可能稍有差异. ...
  • 前边我们讲述了:Java高并发——了解并行世界、Java高并发——多线程基础、Java高并发——多线程协作,同步控制 。从1,线程是什么?为什么需要多线程?2,Java对多线程的基础操作:线程的状态扭转,线程的创建、...
  • Java高并发解决方案

    千次阅读 2014-12-20 22:33:15
    一:高并发高负载类网站关注点之数据库 没错,首先是数据库,这是大多数应用所面临的首个SPOF。尤其是Web2.0的应用,数据库的响应是首先要解决的。 一 般来说MySQL是最常用的,可能最初是一个mysql主机,当数据...
  • Java高并发秒杀系统(二)

    千次阅读 2017-11-24 23:03:56
    本文主要对秒杀系统在大并发的场景下性能瓶颈的做一个分析,以及秒杀系统的优化实现。秒杀系统的业务分析和系统实现,可以参考上一篇文章 Java高并发秒杀系统(一)

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 151,471
精华内容 60,588
关键字:

java高并发访问数据库

java 订阅