精华内容
下载资源
问答
  • 高并发情况下你还在用Random生成随机数?

    万次阅读 多人点赞 2021-04-05 23:42:13
    为了改进这个问题,增强随机数生成器在高并发环境中的性能,于是乎,就有了ThreadLocalRandom——一个性能强悍的高并发随机数生成器。 ThreadLocalRandom继承自Random,根据里氏代换原则,这说明ThreadLocalRandom...

    点赞再看,养成习惯,微信搜索【三太子敖丙】关注这个好像有点东西的傻瓜

    本文 GitHub https://github.com/JavaFamily 已收录,有一线大厂面试完整考点、资料、简历模板,以及我的程序人生。

    前言

    在代码中生成随机数,是一个非常常用的功能,并且JDK已经提供了一个现成的Random类来实现它,并且Random类是线程安全的。

    下面是Random.next()生成一个随机整数的实现:

        protected int next(int bits) {
            long oldseed, nextseed;
            AtomicLong seed = this.seed;
            do {
                oldseed = seed.get();
                nextseed = (oldseed * multiplier + addend) & mask;
              //CAS 有竞争是效率低下
            } while (!seed.compareAndSet(oldseed, nextseed));
            return (int)(nextseed >>> (48 - bits));
        }
    

    不难看到,上面的方法中使用CAS操作更新seed,在大量线程竞争的场景下,这个CAS操作很可能失败,失败了就会重试,而这个重试又会消耗CPU运算,从而使得性能大大下降了。

    因此,虽然Random是线程安全的,但是并不是“高并发”的。

    为了改进这个问题,增强随机数生成器在高并发环境中的性能,于是乎,就有了ThreadLocalRandom——一个性能强悍的高并发随机数生成器。

    ThreadLocalRandom继承自Random,根据里氏代换原则,这说明ThreadLocalRandom提供了和Random相同的随机数生成功能,只是实现算法略有不同。

    在Thread中的变量

    为了应对线程竞争,Java中有一个ThreadLocal类,为每一个线程分配了一个独立的,互不相干的存储空间。

    ThreadLocal的实现依赖于Thread对象中的ThreadLocal.ThreadLocalMap threadLocals成员字段。

    与之类似,为了让随机数生成器只访问本地线程数据,从而避免竞争,在Thread中,又增加了3个成员:

        /** The current seed for a ThreadLocalRandom */
        @sun.misc.Contended("tlr")
        long threadLocalRandomSeed;
        /** Probe hash value; nonzero if threadLocalRandomSeed initialized */
        @sun.misc.Contended("tlr")
        int threadLocalRandomProbe;
        /** Secondary seed isolated from public ThreadLocalRandom sequence */
        @sun.misc.Contended("tlr")
        int threadLocalRandomSecondarySeed;
    

    这3个字段作为Thread类的成员,便自然和每一个Thread对象牢牢得捆绑在一起,因此成为了名副其实的ThreadLocal变量,而依赖这几个变量实现的随机数生成器,也就成为了ThreadLocalRandom。

    消除伪共享

    不知道大家有没有注意到, 在这些变量上面,都带有一个注解@sun.misc.Contended,这个注解是干什么用的呢?要了解这个,大家得先知道一下并发编程中的一个重要问题——伪共享

    我们知道,CPU是不直接访问内存的,数据都是从高速缓存中加载到寄存器的,高速缓存又有L1,L2,L3等层级。在这里,我们先简化这些负责的层级关系,假设只有一级缓存和一个主内存。

    CPU读取和更新缓存的时候,是以行为单位进行的,也叫一个cache line,一行一般64字节,也就是8个long的长度。

    因此,问题就来了,一个缓存行可以放多个变量,如果多个线程同时访问的不同的变量,而这些不同的变量又恰好位于同一个缓存行,那会发生什么呢?

    如上图所示,X,Y为相邻2个变量,位于同一个缓存行,两个CPU core1 core2都加载了他们,core1更新X,同时,core2更新Y,由于数据的读取和更新是以缓存行为单位的,这就意味着当这2件事同时发生时,就产生了竞争,导致core1和core2有可能需要重新刷新自己的数据(缓存行被对方更新了),这就导致系统的性能大大折扣,这就是伪共享问题。

    那怎么改进呢?如下图:

    上图中,我们把X单独占用一个缓存行,Y单独占用一个缓存行,这样各自更新和读取,都不会有任何影响了。

    而上述代码中的@sun.misc.Contended(“tlr”)就会在虚拟机层面,帮助我们在变量的前后生成一些padding,使得被标注的变量位于同一个缓存行,不与其它变量冲突。

    在Thread对象中,成员变量threadLocalRandomSeed,threadLocalRandomProbe,threadLocalRandomSecondarySeed被标记为同一个组tlr,使得这3个变量放置于一个单独的缓存行,而不与其它变量发生冲突,从而提高在并发环境中的访问速度。

    反射的高效替代方案

    随机数的产生需要访问Thread的threadLocalRandomSeed等成员,但是考虑到类的封装性,这些成员却是包内可见的。

    很不幸,ThreadLocalRandom位于java.util.concurrent包,而Thread则位于java.lang包,因此,ThreadLocalRandom并没有办法访问Thread的threadLocalRandomSeed等变量。

    这时,Java老鸟们可能就会跳出来说:这算什么,看我的反射大法,不管啥都能抠出来访问一下。

    说的不错,反射是一种可以绕过封装,直接访问对象内部数据的方法,但是,反射的性能不太好,并不适合作为一个高性能的解决方案。

    有没有什么办法可以让ThreadLocalRandom访问Thread的内部成员,同时又具有远超于反射的,且无限接近于直接变量访问的方法呢?答案是肯定的,这就是使用Unsafe类。

    这里,就简单介绍一下用的两个Unsafe的方法:

    public native long    getLong(Object o, long offset);
    public native void    putLong(Object o, long offset, long x);
    

    其中getLong()方法,会读取对象o的第offset字节偏移量的一个long型数据;putLong()则会将x写入对象o的第offset个字节的偏移量中。

    这类类似C的操作方法,带来了极大的性能提升,更重要的是,由于它避开了字段名,直接使用偏移量,就可以轻松绕过成员的可见性限制了。

    性能问题解决了,那下一个问题是,我怎么知道threadLocalRandomSeed成员在Thread中的偏移位置呢,这就需要用unsafe的objectFieldOffset()方法了,请看下面的代码:

    上述这段static代码,在ThreadLocalRandom类初始化的时候,就取得了Thread成员变量threadLocalRandomSeed,threadLocalRandomProbe,threadLocalRandomSecondarySeed在对象偏移中的位置。

    因此,只要ThreadLocalRandom需要使用这些变量,都可以通过unsafe的getLong()和putLong()来进行访问(也可能是getInt()和putInt())。

    比如在生成一个随机数的时候:

        protected int next(int bits) {
            return (int)(mix64(nextSeed()) >>> (64 - bits));
        }
        final long nextSeed() {
            Thread t; long r; // read and update per-thread seed
            //在ThreadLocalRandom中,访问了Thread的threadLocalRandomSeed变量
            UNSAFE.putLong(t = Thread.currentThread(), SEED,
                           r = UNSAFE.getLong(t, SEED) + GAMMA);
            return r;
        }
    

    这种Unsafe的方法掉地能有多快呢,让我们一起看做个试验看看:

    这里,我们自己写一个ThreadTest类,使用反射和unsafe两种方法,来不停读写threadLocalRandomSeed成员变量,比较它们的性能差异,代码如下:

    上述代码中,分别使用反射方式byReflection() 和Unsafe的方式byUnsafe()来读写threadLocalRandomSeed变量1亿次,得到的测试结果如下:

    byUnsafe spend :171ms
    byReflection spend :645ms
    

    不难看到,使用Unsafe的方法远远优于反射的方法,这也是JDK内部,大量使用Unsafe来替代反射的原因之一。

    随机数种子

    我们知道,伪随机数生成都需要一个种子,threadLocalRandomSeed和threadLocalRandomSecondarySeed就是这里的种子。其中threadLocalRandomSeed是long型的,threadLocalRandomSecondarySeed是int。

    threadLocalRandomSeed是使用最广泛的大量的随机数其实都是基于threadLocalRandomSeed的。而threadLocalRandomSecondarySeed只是某些特定的JDK内部实现中有使用,使用并不广泛。

    初始种子默认使用的是系统时间:

    上述代码中完成了种子的初始化,并将初始化的种子通过UNSAFE存在SEED的位置(即threadLocalRandomSeed)。

    接着就可以使用nextInt()方法获得随机整数了:

        public int nextInt() {
            return mix32(nextSeed());
        }    
        final long nextSeed() {
            Thread t; long r; // read and update per-thread seed
            UNSAFE.putLong(t = Thread.currentThread(), SEED,
                           r = UNSAFE.getLong(t, SEED) + GAMMA);
            return r;
        }
    

    每一次调用nextInt()都会使用nextSeed()更新threadLocalRandomSeed。由于这是一个线程独有的变量,因此完全不会有竞争,也不会有CAS的重试,性能也就大大提高了。

    探针Probe的作用

    除了种子外,还有一个threadLocalRandomProbe探针变量,这个变量是用来做什么的呢?

    我们可以把threadLocalRandomProbe 理解为一个针对每个Thread的Hash值(不为0),它可以用来作为一个线程的特征值,基于这个值可以为线程在数组中找到一个特定的位置。

    static final int getProbe() {
        return UNSAFE.getInt(Thread.currentThread(), PROBE);
    }
    

    来看一个代码片段:

            CounterCell[] as; long b, s;
            if ((as = counterCells) != null ||
                !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
                CounterCell a; long v; int m;
                boolean uncontended = true;
                if (as == null || (m = as.length - 1) < 0 ||
                    // 使用probe,为每个线程找到一个在数组as中的位置
                    // 由于每个线程的probe值不一样,因此大概率 每个线程对应的数组中的元素也是不一样的
                    // 每个线程对应了不同的元素,就可以没有冲突的进行完全的并发操作
                    // 因此探针probe在这里 就起到了防止冲突的作用
                    (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
                    !(uncontended =
                      U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
    

    在具体的实现中,如果上述代码发生了冲突,那么,还可以使用ThreadLocalRandom.advanceProbe()方法来修改一个线程的探针值,这样可以进一步避免未来可能得冲突,从而减少竞争,提高并发性能。

        static final int advanceProbe(int probe) {
            //根据当前探针值,计算一个更新的探针值
            probe ^= probe << 13;   // xorshift
            probe ^= probe >>> 17;
            probe ^= probe << 5;
            //更新探针值到线程对象中 即修改了threadLocalRandomProbe变量
            UNSAFE.putInt(Thread.currentThread(), PROBE, probe);
            return probe;
        }
    

    总结

    今天,我们介绍了ThreadLocalRandom对象,这是一个高并发环境中的,高性能的随机数生成器。

    我们不但介绍了ThreadLocalRandom的功能和内部实现原理,还介绍介绍了ThreadLocalRandom对象是如何达到高性能的(比如通过伪共享,Unsafe等手段),希望大家可以将这些技术灵活运用到自己的工程中。

    小傻瓜们对这个冷门类是否有深一步的理解了?理解了可以在评论区来一波:变得更强

    我是敖丙,你知道的越多,不知道的越多,我们下期见。


    敖丙把自己的面试文章整理成了一本电子书,共 1630页!

    干货满满,字字精髓。目录如下,有我复习时总结的面试题以及简历模板,现在免费送给大家。

    链接:https://pan.baidu.com/s/1ZQEKJBgtYle3v-1LimcSwg 密码:wjk6


    文章持续更新,可以微信搜一搜「 三太子敖丙 」第一时间阅读,回复【资料】有我准备的一线大厂面试资料和简历模板,本文 GitHub https://github.com/JavaFamily 已经收录,有大厂面试完整考点,欢迎Star。

    展开全文
  • Java高并发解决方案

    万次阅读 多人点赞 2019-03-03 22:23:50
    电商的秒杀和抢购,对我们来说,都不是一个陌生的东西。然而,从技术的角度来说,这对于Web系统是一个巨大的考验。...在过去的工作中,我曾经面对过5w每秒的高并发秒杀功能,在这个过程中,整个W...

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

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

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

    1.请求接口的合理设计

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    3.重启与过载保护

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

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

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

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

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

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

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

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

    应对方案:

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

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

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

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

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

    应对方案:

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

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

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

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

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

    应对方案:

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

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

    4.火车票的抢购

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

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

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

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

    解决方案:

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

    三、高并发下的数据安全

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

    1.超发的原因

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

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

    2.悲观锁思路

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

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

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

    3.FIFO队列思路

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

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

    4.乐观锁思路

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

    5.缓存服务器

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

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

    四、小结

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

    个人整理并发解决方案。

    a.应用层面:读写分离、缓存、队列、集群、令牌、系统拆分、隔离、系统升级(可水平扩容方向)。
    b.时间换空间:降低单次请求时间,这样在单位时间内系统并发就会提升。
    c.空间换时间:拉长整体处理业务时间,换取后台系统容量空间。

     

    喜欢本文的朋友们,欢迎关注微信公众号“Java面试达人”,收看更多精彩内容

    也可关注微博 “程序员阿九”,收看更多精彩内容

     

    展开全文
  • 面试题:高并发场景下,如何保证缓存与数据库一致性? 问题分析 我们日常开发中,对于缓存用的最多的场景就像下图一样,可能仅仅是对数据进行缓存,减轻数据库压力,缩短接口响应时间。 这种方案在不需要考虑...

    面试题:高并发场景下,如何保证缓存与数据库一致性?

    问题分析

    我们日常开发中,对于缓存用的最多的场景就像下图一样,可能仅仅是对数据进行缓存,减轻数据库压力,缩短接口响应时间。

    这种方案在不需要考虑高并发得去写缓存,高并发得读写缓存时,是不会有问题,但是如果是在高并发场景下,要保证缓存和数据库的一致性,至少需要解决以下问题:

    高并发写时的数据不一致问题

    高并发读写时,请求执行各步骤的顺序是不可控的。假设此时有一个请求A,B都在在执行写流程,请求A是需要将某个数据改成1,请求B是需要将某个数据改为2,执行操作如下时就会导致数据不一致的问题:

    1.请求A执行操作1.1删除缓存。

    2.请求A执行操作1.2更新数据库,将值改为1。

    3.请求B执行操作1.1删除缓存。

    4.请求B执行操作1.2更新数据库,将值改为2

    5.假设说请求B所在服务器网络延迟比较低,请求B先更新缓存,此时缓存中的key对应的value是2。

    6.请求A更新缓存,将缓存中B更新的数据进行覆盖,将key对应的值改为1。

    此时数据库中是B修改后的数据,值为2,而缓存中的数据是1,这样在缓存过期前,用户读到的都是脏数据,与数据库不一致。

    高并发读写时的数据不一致的问题

    高并发读写时,请求执行各步骤的顺序是不可控的。假设此时有一个请求A在执行写流程,将原值由1改成2,请求B执行读流程,执行操作如下时就会导致数据不一致的问题:

    1.写请求A执行1.1操作删除缓存key,value是原值1。

    2.读请求B执行2.1操作发现缓存中没有数据,就去执行2.2操作读数据库,读到旧数据,值为1。

    3.写请求A执行1.2操作更新数据库,将数据由1改为2。

    4.写请求A执行1.3操作更新缓存,此时缓存中的数据key对应的value是2。

    5.读请求B执行2.3操作更新缓存,将之前读到的旧数据1设置到缓存中,此时缓存中的数据key对应的value是1。

    所以如果说读请求B所在服务器网络延迟比较高,去执行2.3操作比写请求A晚,就会导致写请求A更新完缓存后,读请求B使用之前读到的旧数据去更新缓存,此时缓存中数据就与数据库中的不一致。

    解决方案

    保证数据一致性,网上有很多种方案,例如:

    1.先删除缓存,再更新数据库。

    2.先更新数据库,再删除缓存。

    3.先删除缓存,再更新数据库,然后异步延迟一段时间再去删一次缓存。

    但是这些方案都是存在各种各样的问题,这里篇幅有限,只给出目前相对正确的三套方案,目前的这些方案也有自己的局限性。

    方案1.写请求串行化

    写请求

    1.写请求更新之前先获取分布式锁,获得之后才能去数据库更新这个数据,获取不到就进行等待,超时后就返回更新失败。

    2.更新完之后去刷新缓存,如果刷新失败,放到内存队列中进行重试(重试时取数据库最新数据更新缓存)。

    读请求

    读请求发现缓存中没有数据时,直接去读取数据库,读完更新缓存。

    总结

    这种技术方案通过对写请求的实现串行化来保证数据一致性,但是会导致吞吐量变低。比较适合银行相关的业务,因为对于银行项目来说,保证数据一致性比可用性更加重要,就像是去存款机存钱,取钱时,为了保证账户安全,都是会让用户执行操作后,等待一段时间才能获得反馈,这段时间其实取款机是不可用的。

    方案2.先更新数据库,异步删除缓存,删除失败后重试

    1.先更新数据库

    2.异步删除缓存(如果数据库是读写分离的,那么删除缓存时需要延迟删除,否则可能会在删除缓存时,从库还没有收到更新后的数据,其他读请求就去从库读到旧数据然后设置到缓存中。)

    3.删除缓存失败时,将删除的key放到内存队列或者是消息队列中进行异步重试

    #### 发散思考

    在更新完数据库后,我们为什么不直接更新,而是采用删除缓存呢?

    这是因为直接更新缓存的话,在高并发场景下,有多个更新请求时,难以保证后更新数据库的请求会后更新缓存,也就是上面的高并发写问题。如果采用删除缓存,可以让下次读时读取数据库,更新缓存,保证一致性。

    方案3.业务项目更新数据库,其他项目订阅binlog更新

    1.业务项目直接更新数据库。

    2.cannal项目会读取数据库的binlog,然后解析后发消息到kafka。

    3.然后缓存更新项目订阅topic,从kafka接收到更新数据库操作的消息后,更新缓存,更新缓存失败时,新建异步线程去重试或者将操作发到消息队列,后续再进行处理。

    总结:

    但是这种方案在更新数据库后,缓存中还是旧值,必须等缓存更新项目消费消息后,更新缓存,缓存中才是最新值。所以更新操作完成与更新生效之间会有一定的延迟。

    最后

    大家有了解其他的技术方案,也欢迎一起讨论!

    参考链接:

    https://www.cnblogs.com/-wenli/p/11474164.html

    https://www.cnblogs.com/rjzheng/p/9041659.html

    展开全文
  • java高并发下数据入库

    千次阅读 2021-08-24 16:03:54
    java高并发下数据批量入库 该服务利用线程池并结合缓存类来处理高并发下数据入库问题,做到实时数据存入redis和数据批量入库,使用的时候需要修改为自己的业务数据,该服务暂时适合下面两种情况: 1、达到设置的超时...

    java高并发下数据入库

    该服务利用线程池并结合缓存类来处理高并发下数据入库问题,做到实时数据存入redis和数据批量入库,使用的时候需要修改为自己的业务数据,该模块是根据下面的设置进行高并发处理。
    1、达到设置的超时时间。
    2、达到最大批次。

    package io.jack.service.impl;
    
    import com.alibaba.fastjson.JSON;
    import com.alibaba.fastjson.JSONArray;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.InitializingBean;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Component;
    
    import javax.annotation.Resource;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    /**
     * <pre>
     *   数据批量入库服务
     * </pre>
     * Created by RuiXing Hou on 2021-08-05.
     *
     * @since 1.0
     */
    @Component
    @Slf4j
    public class BatchDataStorageService implements InitializingBean
    {
    	/**
    	 * 最大批次数量
    	 */
    	@Value("${app.db.maxBatchCount:800}")
        private int maxBatchCount;
    
    	/**
    	 * 最大线程数
    	 */
        @Value("${app.db.maxBatchThreads:100}")
        private int maxBatchThreads;
    
    	/**
    	 * 超时时间
    	 */
    	@Value("${app.db.batchTimeout:3000}")
        private int batchTimeout;
    
    	/**
    	 * 批次数量
    	 */
        private int batchCount = 0;
    
    	/**
    	 * 批次号
    	 */
    	private static long batchNo = 0;
    
    	/**
    	 * 线程池定义接口
    	 */
        private ExecutorService executorService = null;
    
    	/**
    	 * 服务器缓存工具类,下面提供源码
    	 */
    	@Resource
    	private CacheService cacheService;
    
    	/**
    	 * 业务接口
    	 */
    	@Resource
    	private DeviceRealTimeService deviceRealTimeService;
    
    	/**
    	 * redis工具类
    	 */
    	@Resource
    	private RedisUtils redisUtils;
    
    	@Override
    	public void afterPropertiesSet() {
    		this.executorService = Executors.newFixedThreadPool(this.maxBatchThreads, r -> {
    			Thread thread = new Thread(r);
    			if (r instanceof BatchWorker) {
    				thread.setName("batch-worker-" + ((BatchWorker) r).batchKey);
    			}
    			return thread;
    		});
    	}
    
    	/**
    	 * 需要做高并发处理的类只需要调用该方法
    	 *
    	 * @param deviceRealTimeDTO
    	 */
    	public void saveRealTimeData(DeviceRealTimeDTO deviceRealTimeDTO) {
    		final String failedCacheKey = "device:real_time:failed_records";
    
    		try {
    
    			String durationKey = "device:real_time:batchDuration" + batchNo;
    			String batchKey = "device:real_time:batch" + batchNo;
    
    			if (!cacheService.exists(durationKey)) {
    				cacheService.put(durationKey, System.currentTimeMillis());
    				new BatchTimeoutCommitThread(batchKey, durationKey, failedCacheKey).start();
    			}
    
    			cacheService.lPush(batchKey, deviceRealTimeDTO);
    			if (++batchCount >= maxBatchCount) {
    				// 达到最大批次,执行入库逻辑
    				dataStorage(durationKey, batchKey, failedCacheKey);
    			}
    
    		} catch (Exception ex) {
    			log.warn("[DB:FAILED] 设备上报记录入批处理集合异常: " + ex.getMessage() + ", DeviceRealTimeDTO: " + JSON.toJSONString(deviceRealTimeDTO), ex);
    			cacheService.lPush(failedCacheKey, deviceRealTimeDTO);
    		} finally {
    			updateRealTimeData(deviceRealTimeDTO);
    		}
    	}
    
    	/**
    	 * 更新实时数据
    	 * @param deviceRealTimeDTO 业务POJO
    	 */
    	private void updateRealTimeData(DeviceRealTimeDTO deviceRealTimeDTO) {
    		redisUtils.set("real_time:"+deviceRealTimeDTO.getDeviceId(), JSONArray.toJSONString(deviceRealTimeDTO));
    	}
    
    	/**
    	 *
    	 * @param durationKey 		持续时间标识
    	 * @param batchKey			批次标识
    	 * @param failedCacheKey	错误标识
    	 */
    	private void dataStorage(String durationKey, String batchKey, String failedCacheKey) {
    		batchNo++;
    		batchCount = 0;
    		cacheService.del(durationKey);
    		if (batchNo >= Long.MAX_VALUE) {
    			batchNo = 0;
    		}
    		executorService.execute(new BatchWorker(batchKey, failedCacheKey));
    	}
    
    	private class BatchWorker implements Runnable
    	{
    
    		private final String failedCacheKey;
    		private final String batchKey;
    
    		public BatchWorker(String batchKey, String failedCacheKey) {
    			this.batchKey = batchKey;
    			this.failedCacheKey = failedCacheKey;
    		}
    		
    		@Override
    		public void run() {
    			final List<DeviceRealTimeDTO> deviceRealTimeDTOList = new ArrayList<>();
    			try {
    				DeviceRealTimeDTO deviceRealTimeDTO = cacheService.lPop(batchKey);
    				while(deviceRealTimeDTO != null) {
    					deviceRealTimeDTOList.add(deviceRealTimeDTO);
    					deviceRealTimeDTO = cacheService.lPop(batchKey);
    				}
    
    				long timeMillis = System.currentTimeMillis();
    
    				try {
    					List<DeviceRealTimeEntity> deviceRealTimeEntityList = ConvertUtils.sourceToTarget(deviceRealTimeDTOList, DeviceRealTimeEntity.class);
    					deviceRealTimeService.insertBatch(deviceRealTimeEntityList);
    				} finally {
    					cacheService.del(batchKey);
    					log.info("[DB:BATCH_WORKER] 批次:" + batchKey + ",保存设备上报记录数:" + deviceRealTimeDTOList.size() + ", 耗时:" + (System.currentTimeMillis() - timeMillis) + "ms");
    				}
    			} catch (Exception e) {
    				log.warn("[DB:FAILED] 设备上报记录批量入库失败:" + e.getMessage() + ", DeviceRealTimeDTO: " + deviceRealTimeDTOList.size(), e);
    				for (DeviceRealTimeDTO deviceRealTimeDTO : deviceRealTimeDTOList) {
    					cacheService.lPush(failedCacheKey, deviceRealTimeDTO);
    				}
    			}
    		}
        }
    
    	class BatchTimeoutCommitThread extends Thread {
    
    		private final String batchKey;
    		private final String durationKey;
    		private final String failedCacheKey;
    
    		public BatchTimeoutCommitThread(String batchKey, String durationKey, String failedCacheKey) {
    			this.batchKey = batchKey;
    			this.durationKey = durationKey;
    			this.failedCacheKey = failedCacheKey;
    			this.setName("batch-thread-" + batchKey);
    		}
    
    		public void run() {
    			try {
    				Thread.sleep(batchTimeout);
    			} catch (InterruptedException e) {
    				log.error("[DB] 内部错误,直接提交:" + e.getMessage());
    			}
    
    			if (cacheService.exists(durationKey)) {
    				// 达到最大批次的超时间,执行入库逻辑
    				dataStorage(durationKey, batchKey, failedCacheKey);
    			}
    		}
    
    	}
    
    }
    
    
    package io.jack.service;
    
    import org.springframework.beans.factory.InitializingBean;
    import org.springframework.context.annotation.Scope;
    import org.springframework.stereotype.Component;
    
    import java.util.HashMap;
    import java.util.LinkedList;
    import java.util.Map;
    import java.util.concurrent.ConcurrentHashMap;
    import java.util.concurrent.atomic.AtomicLong;
    
    @Component
    @Scope("singleton")
    public class CacheService implements InitializingBean {
    
        private Map<String, Object> objectCache = new ConcurrentHashMap<>();
    
        private Map<String, AtomicLong> statCache = new ConcurrentHashMap<>();
    
        @Override
        public void afterPropertiesSet() {
            statCache.put("terminals", new AtomicLong(0));
            statCache.put("connections", new AtomicLong(0));
        }
    
        public long incr(String statName) {
            if (!statCache.containsKey(statName))
                statCache.put(statName, new AtomicLong(0));
            return statCache.get(statName).incrementAndGet();
        }
    
        public long decr(String statName) {
            if (!statCache.containsKey(statName))
                statCache.put(statName, new AtomicLong(0));
            return statCache.get(statName).decrementAndGet();
        }
    
        public long stat(String statName) {
            if (!statCache.containsKey(statName))
                statCache.put(statName, new AtomicLong(0));
            return statCache.get(statName).get();
        }
    
        public <T> void put(String key, T object) {
            objectCache.put(key, object);
        }
    
        public <T> T get(String key) {
            return (T) objectCache.get(key);
        }
    
        public void remove(String key) {
            objectCache.remove(key);
        }
    
        public void hSet(String key, String subkey, Object value) {
            synchronized (objectCache) {
                HashMap<String, Object> submap = (HashMap<String, Object>) objectCache.get(key);
                if (submap == null) {
                    submap = new HashMap<>();
                    objectCache.put(key, submap);
                }
                submap.put(subkey, value);
            }
        }
    
        public <T> T hGet(String key, String subkey) {
            synchronized (objectCache) {
                HashMap<String, Object> submap = (HashMap<String, Object>) objectCache.get(key);
                if (submap != null) {
                    return (T) submap.get(subkey);
                }
                return null;
            }
        }
    
        public boolean hExists(String key, String subkey) {
            synchronized (objectCache) {
                HashMap<String, Object> submap = (HashMap<String, Object>) objectCache.get(key);
                if (submap != null) {
                    return submap.containsKey(subkey);
                }
                return false;
            }
        }
    
        public void lPush(String key, Object value) {
            synchronized (objectCache) {
                LinkedList queue = (LinkedList) objectCache.get (key);
                if (queue == null) {
                    queue = new LinkedList();
                    objectCache.put(key, queue);
                }
                queue.addLast(value);
            }
        }
    
        public <T> T lPop(String key) {
            synchronized (objectCache) {
                LinkedList queue = (LinkedList) objectCache.get (key);
                if (queue != null) {
                    if (!queue.isEmpty()) {
                        return (T)queue.removeLast();
                    }
                    objectCache.remove(key);
                }
                return null;
            }
        }
    
        public void del(String key) {
            objectCache.remove(key);
        }
    
        public boolean exists(String key) {
            return objectCache.containsKey(key);
        }
    
        public void dump() {
    
        }
    }
    
    
    展开全文
  • 解决:读写分离,对缓存,一般都是用来支撑读高并发的,写的请求是比较少的,大量的请求都是在读 一般有多个redis,其中有一个master-redis,专门接受写请求,然后把数据复制到其他的多个slave-redis中,然后所有的读请求都从...
  • 高并发下的Nginx性能优化实战

    万人学习 2019-12-24 14:44:52
    【超实用课程内容】 本课程内容包含讲解解读Nginx的基础知识,解读Nginx的核心知识、带领学员进行高并发环境下的Nginx性能优化实战,让学生能够快速将所学融合到企业应用中。 【课程如何观看?】 PC端:...
  • springboot配置线程池-高并发场景

    万次阅读 2019-03-31 12:40:58
    5.启动服务调用接口查看 快速刷新(如果效果不明显,请增加线程睡眠时间),目的是复现高并发场景,有环境可以进行压力测试   6.为了查看更多线程信息,实现VisiableThreadPoolTaskExecutor 继承...
  • 处理高并发的六种方法

    万次阅读 多人点赞 2019-03-19 12:35:02
    处理高并发的六种方法 1:系统拆分,将一个系统拆分为多个子系统,用dubbo来搞。然后每个系统连一个数据库,这样本来就一个库,现在多个数据库,这样就可以抗高并发。 2:缓存,必须得用缓存。大部分的高并发场景,...
  • 高并发与高可用知识总结

    万次阅读 2019-09-02 15:48:33
    究竟啥才是互联网架构“高并发” 一、什么是高并发 高并发(High Concurrency)是互联网分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计保证系统能够同时并行处理很多请求。 高并发相关常用的一些...
  • 什么是高并发 ,详细讲解

    万次阅读 多人点赞 2018-08-30 13:38:58
    一、什么是高并发 高并发(High Concurrency)是互联网分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计保证系统能够同时并行处理很多请求。   高并发相关常用的一些指标有响应时间(Response ...
  • 多线程和高并发介绍

    万次阅读 2021-06-06 05:00:46
    多线程和高并发介绍 文章目录多线程和高并发介绍前言一、什么是多线程?1.多线程介绍2.多线程实现原理?3.白话文解释多线程4.多线程存在的问题二、什么是高并发?1.高并发介绍2.如何提升系统的并发能力三、多线程和...
  • 甚至会出现拒绝服务的问题,第一时间想到是机器性能问题,无法满足并发如此大的场景,需要进行扩容或者服务限流。经过扩容之后平稳了一个多月之后,又一次大量请求打进来的时候出现了此问题。这时才意识到开始从各个...
  • JAVA高并发的三种实现

    万次阅读 多人点赞 2018-07-23 10:50:59
    是用它可以解决一切并发问题,但是,对于系统吞吐量要求更的话,我们这提供几个小技巧。帮助大家减小锁颗粒度,提高并发能力。 初级技巧-乐观锁 乐观锁使用的场景是,读不会冲突,写会冲突。同时读的频率远大于...
  • 分布式、多线程、高并发都不懂,拿什么去跳槽

    万次阅读 多人点赞 2019-10-09 01:03:16
    当提起这三个词的时候,是不是很多人都认为分布式=高并发=多线程?当面试官问到高并发系统可以采用哪些手段来解决,或者被问到分布式系统如何解决一致性的问题,是不是一脸懵逼?确...
  • 高并发解决方案相关面试题

    万次阅读 多人点赞 2019-09-06 20:48:16
    占用内存少,并发量强,支持多种并发连接,效率. 2.能够作为负载均衡服务器和(内部直接支持 Rails 和 PHP)代理服务器。Nginx用C编写开销和CPU占有小. 3.安装启动简单,配置简洁,bug少,一般几个月不需要重新启动...
  • 引入 中国网络上可以产生消费的活跃用户约2.4亿,互联网人数较多,基础人群大 ...于是在这个时代,高并发已经是每一家企业都要面临的。 假设高并发被解决了,在web容器的日志里你要记录些什么?分析渠道的流量的
  • 1 .从最基础的地方做起,优化我们写的代码,减少必要的...使用java中效率的类,比如ArrayList比Vector性能好。 html静态化 我们通过一个链接地址访问,通过这个链接地址,服务器对应的模块处理这个请求,转到对应
  • 高并发的解决方案

    万次阅读 多人点赞 2018-01-06 22:27:06
    刚开始的时候应用和静态资源是保存在一起的,当并发量达到一定程度的时候就需要将静态资源保存到专门的服务器中,静态资源主要包括图片、视频、js、css和一些资源文件等,这些文件因为没有状态所以分离比较简单,...
  • 高并发解决方案之一 ——负载均衡

    万次阅读 多人点赞 2018-04-15 21:52:15
    由于Nginx 超越 Apache 的高性能和稳定性,使得国内使用 Nginx 作为 Web 服务器的网站也越来越多,其中包括新浪博客、新浪播客、网易新闻、腾讯网、搜狐博客等门户网站频道等,在3w以上的高并发环境下,ngnix处理...
  • 高并发常见的面试题

    万次阅读 多人点赞 2019-02-28 23:45:06
    它沿用了与它同时期的HashMap版本的思想,底层依然由数组+链表+红黑树的方式思想,但是为了做到并发,又增加了很多复制类,例如TreeBin、Traverser等对象内部类。CAS算法实现无锁化的修改至操作,他可以大大降低锁...
  • 面试高并发,凉了!!(全程高能,建议收藏)

    千次阅读 多人点赞 2021-05-14 07:50:56
    此时,小菜突然想起来自己的好朋友——大冰,一个工作经验丰富的大神级别的人物,小菜决定要跟他学习高并发编程的知识!努力提升自己的编程技能!从此,小菜踏上了高并发学习的道路。 总结 并发编程一直是让人很...
  • UDP实现高并发其实非常简单(续集)

    千次阅读 2021-04-10 07:58:49
    上周放假时跟着小小学python,就写了一个所谓 “高并发UDP服务器” ,详见: https://blog.csdn.net/dog250/article/details/115413967 之所以这么写是因为我想让UDP服务看起来像TCP服务而已,但还是发现了新东西。 ...
  • Java 高并发解决方案(电商的秒杀和抢购)

    万次阅读 多人点赞 2018-07-25 14:08:08
    电商的秒杀和抢购,对我们来说,都不是一个陌生的东西。然而,从技术的角度来说,这对于Web系统是一个巨大的考验。当一个Web系统,在...在过去的工作中,我曾经面对过5w每秒的高并发秒杀功能,在这个过程中,整...
  • UDP实现高并发其实非常简单

    千次阅读 2021-04-03 11:15:17
    UDP越来越被重视了,人们期望它和TCP一样,但是呢?...UDP能不能像TCP那样做高并发? 内核早在3.9版本就支持REUSEPORT了,但直到2015年底,我也只能做个表面上(看起来像那么回事,但实际上就是垃圾的东西)的东西: ...
  • JAVA多线程下高并发的处理经验

    万次阅读 多人点赞 2019-02-24 17:36:59
    java线程可以通过setPriority()方法对其设定一个优先级别,优先级别的线程比低优先级别的线程有更的几率得到先执行,优先级可以用0到10的整数表示,0为最低优先级别、10为最高优先级别。当线程调度器决定那个...
  • 本文介绍高并发系统的度量指标,讲述高并发系统的设计思路,再梳理高并发的关键技术,最后结合作者的经验做一些延伸探讨。
  • 在实际开发过程中往往会出现许多高并发场场景,秒杀,强红包,抢优惠卷等; 其中: 秒杀场景的特点就是单位时间涌入用户量极大,商品数少,且要保证不可超量销售; 秒杀产品的本质就是减库存; 秒杀场景常用的...
  • 多线程与高并发编程之基础知识(上)

    万次阅读 多人点赞 2018-10-01 21:48:25
    但是,想要使用好多线程这把利刃,还需要掌握好多线程编程的基础知识,从而做到得心应手地使用多线程进行性能程序的开发! 多线程的应用场景 程序中出现需要等待的操作,比如网络操作、文件IO等,可以利用...
  • 高并发高性能服务器是如何实现的

    千次阅读 2020-11-09 08:15:00
    因为虽然线程创建开销相比进程小,但依然也是有开销的,对于动辄数万数十万的链接的高并发服务器来说,创建数万个线程会有性能问题,这包括内存占用、线程间切换,也就是调度的开销。 因此,我们需要进一步思考。 ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 903,049
精华内容 361,219
关键字:

高并发