并发编程redis爱编程爱挑战

高并发秒杀系统的优化

2017-02-06  本文已影响4399人  不知名的蛋挞

一、什么是高并发

高并发是指在同一个时间点,有大量用户同时访问URL地址,比如淘宝双11、定时领取红包就会产生高并发;又比如贴吧的爆吧,就是恶意的高并发请求,也就是DDOS攻击(通过大量合法的请求占用大量网络资源,以达到瘫痪网络的目的)。

二、高并发带来的后果

三、并发下的处理

<!-- 数据库连接池 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">    
     <!-- 配置连接池属性 -->
     <property name="driverClass" value="${driver}"/>
     <property name="jdbcUrl" value="${url}"/>
     <property name="user" value="${username}"/>
     <property name="password" value="${password}"/>

     <!-- c3p0连接池的私有属性 -->
     <property name="maxPoolSize" value="30"/>
     <property name="minPoolSize" value="10"/>
     <property name="autoCommitOnClose" value="false"/>
     <!-- 获取连接超时时间 -->
     <property name="checkoutTimeOut" value="1000"/>
     <!-- 当获取连接失败重试次数 -->
     <property name="acquireRetryAttempsts" value="2"/>
</bean>
//当我们用UPDLOCK来读取记录时可以对取到的记录加上更新锁
//从而加上锁的记录在其它的线程中是不能更改的只能等本线程的事务结束后才能更改
update commodity with (updlock) set count = count-1 where id=?;

③ 如果要实现这样一个需求:cache里面的数据必须每天9点更新一次,其他时间点缓存每小时更新一次。并且到9点的时候,凡是已经打开页面的用户会自动刷新页面。
  这里面包含的用户触发缓存更新的逻辑:用户刷新页面,当缓存存在的时候,会获取到最后一次缓存更新的时间。如果当前时间>9点,并且最后缓存时间在9点之前,则会从数据库中重新获取数据保存到cache中。如果大量用户在9点之前已经打开了页面,而且在9点之后还未关闭页面,那么就会导致在9点的时候会有很多并发请求过来,数据库服务器压力暴增。
  要解决这个问题,最好就是只有一个请求去数据库获取,其他都是从缓存中获取数据。此时,我们就可以用锁来解决:从数据读取到缓存那段代码前面加上锁,这样在并发的情况下只会有一个请求是从数据库里获取数据,其他都是从缓存中获取。

但是不是所有的方法都需要加事务,比如读操作。

用户大量刷新→CDN(detail页静态化,静态资源js、css等)→高并发系统

原子计数器:主要是高并发的统计的时候要用到。比如:
increment() 和 decrement() 操作是原子的读-修改-写操作。为了安全实现计数器,必须使用当前值,并为其添加一个值,或写出新值,所有这些均视为一项操作,其他线程不能打断它。

解决方案
①MySQL源码层的修改方案:在update后面加上这样一句话:/+[auto_commit]/,当你执行完这条update的时候它会自动回滚。回滚的条件是:update影响的记录数是1就可以commit,如果为0就会rollback。也就是不给java客户端和MySQL之间网络延迟,然后再由java客户端其控制commit和rollback,而是直接通过语句发过去你就告诉我commit和rollback。这个成本比较高,需要修改MySQL源码
②使用存储过程:存储过程的本质就是让一组sql组成一组事务,然后再MySQL端完成,避免客户端完成事务造成性能的干扰。一般情况下,spring声明事务和手动控制事务都是客户端控制事务。这些事务在行级锁没有那么高的竞争情况下是完全OK的,但是秒杀是一个特殊的应用场景,它会在同一行中产生热点,大家都竞争同一行,这个时候存储过程就能够发挥作用了,他把整个sql执行过程放在MySQL端完成,MySQL执行sql的效率非常高。*简单的逻辑我们可以使用存储过程,太过复杂的就不要依赖了。

-- 秒杀执行存储过程
DELIMITER $$ -- onsole ; 转换为 $$
-- 定义存储过程
-- 参数:in 输入参数; out 输出参数
-- row_count():返回上一条修改类型sql(delete,insert,upodate)的影响行数
-- row_count: 0:未修改数据; >0:表示修改的行数; <0:sql错误/未执行修改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, create_time)
        VALUES(v_seckill_id, v_phone, v_kill_time);
        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 ;

SET @r_result = -3;
-- 执行存储过程
call execute_seckill(1001, 13631231234, now(), @r_result);
-- 获取结果
SELECT @r_result;

③通常我们的操作是:减库存(rowLock)→插入购买明细→commit/rollback(freeLock)。我们可以在这个基础上进行一些简单的优化,调换操作的顺序:插入购买明细→减库存(rowLock)→commit/rollback(freeLock),我这样们的延迟就只会发生在update语句这个点上。

nodejs就是一个具有高并发能力的编程语言,它使用单线程异步时间机制,不会因为数据逻辑处理问题导致服务器资源被占用而导致服务器宕机,我们可以使用NodeJs写web接口。

apache模式,以下简称A模式。一共有三个点餐窗口,三位服务人员,三位厨师(请自行脑补画面,但是别乱想)。顾客在任一窗口点餐[所谓多线程],点完后服务员传达厨师,等待厨师出餐,服务员返给顾客[同步返回响应结果]。顾客本次购物结束。服务员进行下一位顾客的点餐[接收下一个请求]。
  nodejs模式,以下简称N模式。一共只有一个点餐窗口一位服务员[单线程],一位厨师[CPU]。顾客在窗口点餐,点完后服务员传达厨师,厨师进行出餐,而服务员不必等待[不必等待当前请求返回结果],直接进行下一位顾客的点餐,然后继续传达下一个顾客的订单给厨师。厨师挨个完成后抛出给出餐窗口[异步返回响应结果],顾客到出餐窗口取餐,本次购物结束。

比如要统计用户通过各种方式(如点击图片/链接)进入到商品详情的行为次数,如果同时有1w个用户同时在线访问页面,一次拉动滚动条屏幕页面展示10件商品,这样就会有10w个请求过来,服务端需要把请求的次数数据入库,这样服务器分分钟给跪了。
  要解决这些访问量大的数据统计接口的问题,我们可以通过nodejs写一个数据处理接口,把统计数据先存到redis的list中,然后再使用nodejs写一个脚本,脚本的功能就是从redis里取出数据保存到mysql数据库中。这个脚本会一直运行,当redis没有数据需求要同步到数据库中的时候,sleep,然后再进行数据同步操作。

NoSQL
├redis
│├主从分离
│└集群
├mongodb
│├主从分离
│└集群
├memcache
│├主从分离
│└集群
└...
CDN
├html
├css
├js
└image

高并发情境中,更新用户相关缓存需要分布式存储,比如使用用户ID进行hash分组,把用户分不到不用的缓存中,这样一个缓存集合的总量不会很大,不会影响查询效率。

上一篇下一篇

猜你喜欢

热点阅读