精华内容
下载资源
问答
  • 功能优化直播账号创建优化场景:每个企业云用户在直播时需要登录第三方直播账号,如果没有,则创建一个直播账号问题:创建直播账号的服务是第三方提供的,不支持高并发优化:在用户登录的时候调用直播服务,初始化...

    功能优化

    直播账号创建优化

    场景:每个企业云用户在直播时需要登录第三方直播账号,如果没有,则创建一个直播账号

    问题:创建直播账号的服务是第三方提供的,不支持高并发

    优化:在用户登录的时候调用直播服务,初始化直播账号,将创建直播账号的并发压力分散到用户登录期间,这个优化经历了几个版本,最终我们使用方案3实现。

    方案1:在登录的回调函数中同步调用直播服务创建账号。这个方案存在的问题是,假如创建直播账号的服务响应缓慢、异常将影响用户登录。

    方案2:在登录的回调函数中异步调用直播服务创建账号。这个方案解决了方案1的问题,但代码、服务依然耦合在一起。登录本不应该依赖直播服务,如果以后需要在登录功能上做另外一些额外的预处理和通知,将会让登录的功能变得复杂且职责不清。

    方案3:在登录的回调函数中发送一个主题消息,并投递到MQ,之所以选择主题消息而不是点对点消息,是考虑到未来可能会在登录的时候做其他的一些业务操作。直播业务通过订阅登录消息,收到消息后进行账号初始化、缓存,使登录服务与创建直播账号服务完全解耦,并且有良好的扩展性。

    通过消息队列削峰异步处理

    场景:生成试卷、保存草稿、提交试卷

    问题:这些场景有着较高的并发,这些业务操作本身也比较复杂,原先的同步调用在高并发的场景下会导致服务处理不过来,提示业务操作失败

    优化:将同步操作改为消息+轮询的方式。

    1、 用户点击考试的的时候发送出卷的消息,并在redis中写入出卷中的状态并返回客户端

    2、 后端根据业务需要配置处理线程数(生成试卷这里配置了100个线程),执行业务操作,在出卷完成后将试卷放到redis中,并擦除redis出卷状态。

    3、 客户端首先延迟1s再发轮询请求(调用出卷请求后马上开始轮询,这时大部分情况下后端是还没出好试卷,所以这个请求意义不大),然后再间隔每两秒轮询,处理完成,再获取试卷数据。

    通过缓存提升服务响应时间、降低DB读压力

    场景:获取期次、章节、课时单元、试卷、考试、题目信息等

    问题:之前的这些操作基本都是直接查数据库,在高并发的情况会给数据造成较大的压力,增加业务操作的响应时间

    优化:对于这些与用户无直接关联,写少读多的数据,使用缓存减轻数据库的压力

    1、 缓存框架可以覆盖到的地方(Service的接口)通过注解的方式添加缓存,减少缓存代码的侵入性

    2、 由于修改数据的地方比较多、业务也较复杂,避免对代码改动的范围过大影响其他业务线,所以并没有在原接口上面直接加注解缓存,而是增加一个xxxWithCache的接口,在这个接口上配置缓存注解,在需要缓存的地方调用withCache接口。因为在写时没有及时的擦掉缓存,会导致了部分数据最多有90秒的延迟。

    3、 其他不能使用缓存框架地方通过编码实现缓存

    读写分离,降低主库压力

    场景:导出报表等功能

    优化:通过从读库获取数据

    代码调用剥离

    场景:期次指派全部3万名员工、提交试卷计算每个学生的期次总成绩

    问题:整个指派过程需要40分钟

    优化:用户指派之后还需要干这么几个事情

    1、发送指派通知

    2、更新指派用户数

    3、初始化签到数据

    所以整个指派的时长是成倍增加的,之前的实现是通过同步观察者模式实现,后面改为消息队列实现,并且在索引优化后整个指派过程只有3分钟。

    延迟合并请求

    场景:用户提交试卷更新答卷人数、用户指派更新指派汇总数等

    问题:在高并发下的对同一查询条件的count,同一记录的update,会造成资源剧烈争抢,还可能造成死锁

    优化:通过延迟合并处理,降低并发争抢资源和无用的计算。目前设置的是2分钟,用户提交试卷后并不会马上进行答卷人数的统计更新,而是延迟2分钟进行更新,这期间如果有其他用户提交试卷,更新请求就会被忽略。未来可以通过缓存增量更新+定时重算的方式实现更好的用户体验。

    云信聊天室优化

    场景:直播场景下的讨论区聊天

    问题:并发聊天的时候整个直播页面就崩溃了。

    优化:在服务端和前端都做了并发限制

    服务端:云信针对整个聊天室做并发控制,在并发超过上限后会丢弃部分普通用户的信息

    客户端:通过队列上限500+定时任务的方式,来避免服务端推送的消息量过快时,客户端频繁刷新页面导致崩溃。

    前端随机打散请求

    场景:获取直播状态、定时保存草稿

    问题:客户端发起接口请求的间隔步长相同,在比较极端的情况下可能造成服务端并发过大(如果3w人在10s内一起进入考试,那么每隔5分钟就会有一轮3000的并发保存草稿)

    优化:定时保存草稿,第一次保存草稿的时间是300秒内的随机数,第二次开始为间隔300秒(这样哪怕3w人一起进入考试,保存草稿的并发依然被分散了)。

    批处理、合并SQL

    场景:获取课时资源

    问题:通过遍历每个章节来获取章节下的课时,这样如果有10个章节,需要执行10次获取课时的接口

    优化:通过合并SQL,将期次下的所有课时一次取出,然后在内存中根据课时对应的章节ID进行分组组合,从而减少了SQL语句的数量。

    压力测试过程中发现的一些问题

    性能测试主要是通过jvisualvm监控服务器CPU的压力,通过profile快照分析各种线程和各个接口CPU耗时,以及通过哨兵平台的服务调用汇总信息等作为参考依据。

    调整log级别,取消无用log

    log4j 1.x版本,大量输出日志很耗CPU资源

    a、将sql日志的日志级别从info改为error

    b、控制日志的输出长度

    c、后续可以考虑将log4j改为log4j2(https://my.oschina.net/OutOfMemory/blog/789267)

    调整框架并发数

    提高并发框架的中每个接口的并发数到300,提高dubbo服务提供方的总体线程数到300,提高消费方每服务每方法最大并发数到100.

    JSON转换消耗大量CPU

    通过profile快照发现json转换上面耗费大量cpu,主要体现在有些地方用的是gson转换,有些地方对象转换的用法是将dto先转为json字符串,再将json转换成vo。通过将gson换成fastjson性能得到了提升,而对象转换尽量用beancopier(http://www.cnblogs.com/kaka/archive/2013/03/06/2945514.html)。

    AOP切面尽量不要做太多事情

    AOP存在性能问题将影响所有被拦截的接口

    a、BeanPlacementInterceptor

    每个请求都需要通过远程服务获取站点信息,目前provider中的服务实现已经改为从缓存中取站点,但依然存在2次的远程调用。这是一个极高并发的接口,在AOP里调用服务还会受到并发数的限制,建议后续改成直接从缓存中读取,这样可以省去一次远程调用;如果改成本地缓存,则完全去除了远程调用。不建议在AOP中调用远程服务。

    b、WebMethodsCalledLogAspectj

    这个拦截器用于记录每个请求的响应时间,在finally方法中会用gson序列化返回值,对于试卷这么大的对象转换时耗时的。

    踩过的一些坑

    请求链路上的某个服务并发没有提高,导致目标服务TPS上不去

    dubbo提供者原先总体并发数是100,每个服务的消费方的并发数是20。由于很多服务都依赖于base-conf,provider-server,导致目标服务的代码怎么优化TPS都提不上去,体现在profile快照上的现象是不管怎么提高压力机并发数,快照中的耗时线程数就是20个。修改请求链路中涉及的服务提供者并发数后,TPS提升。

    CPU满载导致中间件服务表现异常,影响优化方向判断

    通过jvisualvm工具分析出从redis中获取试卷快照,每次占用CPU200毫秒甚至更多,继而怀疑redis对大数据的缓存操作能力,最后改为memcached,没有任何提升,实际上是因为CPU跑满,导致其他的服务异常,事实上,每次从缓存取100k快照的时间不过10毫秒左右。

    压力机能力不够导致TPS上不去

    在测试获取快照接口的过程中,把一些DB操作的代码注释掉,几乎是全内存操作,TPS依然保持在50左右,最后把压力机改为6台,TPS马上提升了好几倍。

    交换机能力影响TPS

    第三方直播的几个接口在并发测试的过程中,1台服务器和5台集群服务的吞吐能力几乎一模一样,最后发现交换机是100M的。

    后续待探讨的一些优化思考

    预处理

    以生成试卷为例,是否可以在开卷前的某段时间内将试卷生成好,等用户进入考试时直接将试卷发给用户

    功能设计上多考虑一些抽象

    现在的做法是每个用户生成了一张不同的卷子,因为是用户的维度,数据量是比较大的,这种情况用缓存其实是存在一定风险的,一场3w用户的考卷就用掉3、4G。如果我们针对每次考试,生成几张不同试卷,每个用户进来后从这个试卷池挑选一张试卷,缓存使用就会小很多。生成试卷和获取试卷信息的操作就简单。获取答卷信息的操作就变为通过试卷快照id从缓存中获取试卷,然后读取用户填写的答案等答卷相关信息即可。涉及的DB操作也会变得很轻量。

    数据库设计的时候对一些大字段定义做分离

    分离易变和不变的信息,尽量不要将易变和不变的信息糅合在一个字段里。

    以生成试卷快照为例:目前的快照包含了试卷本身+用户填写的答案。保存草稿的时候实际上是读取试卷快照,然后将用户填写的答案填充到快照中,然后再更新整个大快照。试卷再生成后就不在变化,而答案在多次保存草稿和提交时都会发生变化,如果我们在设计上将试卷和用户答案分离,那么保存草稿的动作就非常简单,就是更新答案字段,对于客观题来讲答案就是一个很小的字段。

    类似的业务场景尽量使用框架或公共代码的方式解决

    a、比如使用缓存框架来处理缓存,未来要将缓存中间件替换,业务代码无须改动;如果在现有的分布式缓存基础上再加一级本地缓存,业务代码基本也是不需要调整的。

    b、比如目前所有导出excel功能用的是相同的导出模板框架,未来我们要在现有的进度上面增加预计完成时间,只要在模板框架底层做这个改动就可以,业务层面的代码无须改动。

    并发处理

    目前代码的整个处理流程都是串行的,但对于同一个服务内部调用的其他服务可能没有依赖关系,对于一些性能要求比较高的场景可以考虑并行处理,当然这样会使变成模型变得复杂一些。

    举个例子:导出excel的过程分为2个部分,1、从数据库获取数据;2、往excel文件写入数据,目前是串行的做法,如果改成并行的做法就是,获取数据的线程在读取一页数据后,交给excel写入线程后马上开始读取第二页的数据。再比如指派名单列表,需要显示指派用户的最新状态(调用员工服务),还需要显示指派用户的问卷完成情况(调用问卷服务),实际上员工服务和问卷服务是可以并行的(这个dubbo也是支持的,不过不推荐生产环境用,是通过NIO实现,不需要客户端启动多个线程)。

    本文来自网易实践者社区,经作者陈志良授权发布。

    展开全文
  • 1.高并发下,数据库连接池资源不足,影响读写效率 在yml配置文件 kikari配置下增加最大连接数配置 设置为60 根据机器jvm调整 maximum-pool-size: 60 #多数据源配置, 通过enable=false禁用 hikari: # 多数据源-主...

    接口效率优化总结:

    1.高并发下,数据库连接池资源不足,影响读写效率
    在yml配置文件 kikari配置下增加最大连接数配置 设置为60 根据机器jvm调整
    maximum-pool-size: 60

    #多数据源配置, 通过enable=false禁用
    hikari:
      # 多数据源-主数据源
      master:
        # 数据库连接地址
        jdbc-url: jdbc:postgresql://127.0.0.1:8080/sffinterdev?useUnicode=true&characterEncoding=utf8&serverTimezone='Asia/Shanghai'&autoReconnect=true
        # 数据库用户名
        #username: tbaseadmin
        username: devadm
        # 数据库密码
        # 秘钥 sff-ck-encrypt
        # 云环境加密密文如下, 应用部署参数:-Dtsf_config_encrypt_password=sff-ck-encrypt
        #    password: ENC(HTa3ksFN2tFV83on6tItnA==)
        #password: tbaseadmin@123
        password: devadm@sff17
        # 数据库最大连接数量
        maximum-pool-size: 50
        # 数据库最大等待时间
        max-wait: 10000
        # 数据库最小空闲数量
        minimum-idle: 5
        # 数据库初始化连接数量
        initial-size: 5
        # 验证连接
        validation-query: SELECT 1
        # 是否在从池中取出连接前进行检验,如果检验失败,则从池中去除连接,并尝试取出另一个
        test-on-borrow: false
        # 是否进行空闲测试,如果检测失败,则从连接池中去除连接
        test-while-idle: true
        # 在空闲连接回去线程运行期间休眠的时间值,单位毫秒
        time-between-eviction-runs-millis: 18800
        jdbc-interceptors: ConnectionState;SlowQueryReport(threshold=0)
        # 是否启用主数据源配置项,为true才会加载上面的主数据库配置
        enable: true
      # 多数据源-从数据源
      slave:
        # 是否启用从数据源配置项,为true才会加载从数据库配置
        enable: false
    

    2.校验规则配置需要读取redis,高并发下redis连接数量不足,影响redis读取效率
    修改yml redis的最大活动数量 max-active: 60
    原配置为8 调整为60

    redis:
      # 秘钥 sff-ck-encrypt
      # 云环境加密密文如下, 应用部署参数:-Dtsf_config_encrypt_password=sff-ck-encrypt
      #    password: ENC(m+r9qVD5rIz0XMLjweKbjg==)
      password: 1q2w3e4r5t
      # redis 集群
      cluster:
        # test:61 dev:68 dj:22
        nodes: 10.186.1.68:6379
        # 最大重连次数i
        max-redirects: 3
      # spring-boot-starter-data-redis default use lettuce pool
      lettuce:
        pool:
          # 最大活动数
          max-active: 60
          # 最大空闲数量
          max-idle: 8
          # 最大等待数量
          max-wait: -1
          # 最小空闲数量
          min-idle: 0
    

    3.本地测试环境机器资源不足(就是硬件配置需要跟上)
    (1)部署组原来配置实力资源限制
    原配置: cpu限制 1 核 内存限制 1024MB
    调整为: cpu限制 2 核 内存限制 4096MB
    (2)启动参数
    原设置:
    -Xms128m -Xmx512m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m
    调整为:
    -Xms2048m -Xmx2048m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m
    Xms表示初始化java堆的大小
    Xmx 表示最大可扩展的java堆大小
    4.redis使用方式
    校验模块原来动态配置中,每次请求都会去redis根据场景拿好几次规则,并发状态下多次与redis建立连接导致效率过高
    调整为使用批量获取数据,批量处理请求。一次交互减少通信时间。
    Redis的multiget方法可以一次发送多条命令,在执行完成后一次性返回结果。减少client和server的交互次数降低通信时间。
    原代码:每个场景单独去读取配置信息

    String rule =redisUtil.get(busScene);
    

    修改后代码:获取所有场景后,一次性读取

    List allRuleList = redisUtil.getRedisTemplate().opsForValue().multiGet(busSceneList);
    

    5.调整cmq发送方法
    由原来同步发送数据到cmq由于发送数据时数据量太大也会有一个排队的现象,所以得修改为异步发送,不用去等待。

    Boolean isSendSuccess = piccCmqProducer.send(warrantyString);
    

    调整为

    piccCmqProducer.sendAsync(warrantyString);
    

    6.调整幂等查询
    在虚拟桌面本地进行压测时,发现跟数据库的连接非常慢,同一张表中如果查询,保存更新发现要么查询保存耗时,要么保存更新耗时,由于中间处理业务比较快,数据库交互间隔时间极短,加上cpu,内存等硬件设备也跟不上,所以打算优化设计,减少数据库的交互次数:

    之前幂等操作是先查询如果存在,并且状态成功直接返回。每笔数据进来都会执行一次查询操作。
    调整为:使用mergeinto方式,在主键冲突后,执行更新语句时加上状态不等于成功的条件,如果返回影响数为0,再去执行查询操作,确认是否真的是等于成功而导致影响数量为0导致的。

    // 第一次保存
    int number = interLogDAO.insertExistsUpdate(interLogPO, Constants.LOG_STATUS.SUCCESS);
    if (number == Constants.NUMBER.ZERO) {
        // 查询是否真的是成功状态
        String logResults = findLogResults(StringUtil.trim(interLogDO.getUuid()));
        if (Constants.LOG_STATUS.SUCCESS.equals(logResults)) {
            throw new PiccCommonException("该uuid数据已处理,请勿重复发送。");
        } else {
            throw new PiccCommonException("保存或更新数据失败,请重试。");
        }
    }
    

    7.反射机制调用校验方法优化
    校验模块采用java反射的机制实现灵活配置,连续调用发现速度明显很慢,程序启动后,类信息与调用方法入口信息在栈中的位置是不变的。耗时主要就是在不连续的代码栈中找出位置。
    基于此种情况,采用了缓存方式,就需要将类信息与调用方法存入map结构的缓存中,第一次调用时,放进map中,后面的请求就可以从map中获取。
    修改为:

    //缓存类信息
    public static Map<String,Class> classMap = new HashMap<String,Class>();
    //缓存方法
    public static Map<String, Method> metMap = new HashMap<String,Method>();
    

    (1)属性校验

    public static Object cacheClassInfo(Object clazz,String method) throws NoSuchMethodException {
    try {
        Method m = metMap.get(method);
        if(null == m) {
            Class cl = classMap.get(clazz.toString());
            if(null == cl) {
                cl = clazz.getClass();
                classMap.put(clazz.toString(),cl);
            }
                m = cl.getMethod(method);
                metMap.put(method,m);
        }
        return m.invoke(clazz);
    }
    

    先从缓存中取方法对象,如果为空就缓存方法信息
    再从缓存中取类对象,如果为空就缓存类对象
    最后用invoke反射调用方法
    8.线程池优化(上一篇文章)
    原来整个清分模块就只有一个线程池,由于清分的时候会去3个后端系统都会一个个去清分,消耗时间长。因此清分模块修改为资源隔离与服务降级熔断设计思路
    将调用发票微服务,缴费微服务,新收付费服务的请求,通过创建三个静态线程池实现资源隔离。
    目的:
    (1)某些下游服务处理请求时间过长,创建三个静态线程池,针对不同的下游服务的请求只调用各自的线程池中的线程进行处理。不会因其他下游服务消费请求耗时长导致不能正常清分数据给其他服务。实现资源隔离。
    (2)实现对下游请求的限流作用,例如前端请求qps为1000次/秒。使用静态线程池,初始核心线程数为50,又因为与下游服务的请求是实时接口,故该50个请求是同步阻塞的。即:请求下游服务的qps为50次/秒。
    注意事项:
    静态线程池参数因根据运行环境以及业务场景并发量进行设置。参考值:若发票微服务处理一次请求的响应时间为50ms,接口引擎一个线程1s就可以处理1000/50=20个实时接口请求。若核心线程数为50,那么接口引擎1s就可以处理20*50=1000个请求,该值为理论理想值,计算过程中忽略了50个核心线程因上下文且还造成的额外消耗时间。若同时清分三个下游服务,故cpu同时处理的线程数就清分请求150个,实际运行环境的线程数量远大于此。

    销项税发票效率优化点:

    1.sql并表查询效率慢:

    SELECT
                invo.reqserialno,
                invo.billstatus,
                invo.billdoc,
                invo.billcode,
                invo.billnumber,
                (
                SELECT
                    taxpayernumber
                FROM
                    sffinvosellerinfo seller
                WHERE
                    seller.reqserialno = invo.reqserialno) taxpayernumber
            FROM
                sffinvoinfo AS invo
            JOIN
                sffinvoreladoc AS doc
            ON
                doc.reqserialno = invo.reqserialno
            JOIN
                sffinvoininterface AS interface
            ON
                interface.invoid = doc.refid::NUMERIC
            WHERE
                invo.billredblue=#{billRedBlue}
                AND
                interface.commodityno = #{policy}
                AND
                invo.useflag LIKE '1%'
                AND
                invo.billstatus IN
                <foreach collection="billStatusList" item="no" index="index" open="(" separator="," close=")">
                    #{no}
                </foreach>
            GROUP BY
                invo.reqserialno
    

    通过执行计划发现此条sql在sffinvoininterface表和sffinvoreladoc表并表时进行了全表扫描导致并发时效率急剧下降,检查sffinvoininterface表的invoid字段是唯一索引,sffinvoreladoc表的refid字段和reqserialno字段时联合主键及联合唯一索引,但使用invoid和refid并表关联时并未走到索引,发现在pg中使用联合索引其中的一个字段为条件并不一定能走索引,后续为refid添加单独索引,再执行发现还是全表扫描,后尝试将条件interface.invoid = doc.refid::NUMERIC更换为interface.invoid:VARCHAR = doc.refid,再执行走到索引。
    后排查所有sql,发现有一些sql相同的并表条件走到了索引,经测试后得出结论:
    (1)涉及并表的字段尽量不要使用不同的类型。
    (2)同样的并表条件不是在所有sql中都会使用索引,需要通过执行计划分析执行步骤,如interface.invoid = doc.refid::NUMERIC并表条件,在where条件中涉及interface表字段时需调整为interface.invoid:VARCHAR = doc.refid才能走索引,在where条件中不涉及interface表字段时,无需条件才能走索引,若调整反而无法走索引。
    (3)Pgsql的联合索引不是在所有场景都生效,复杂sql需要通过执行计划分析执行步骤。

    2.字符流的优化:
    在压测中心测试适配老架构xml请求接口时,发现并发时效率差,通过打日志排查,发现入库转换字符集编码的filter速度很慢,发现代码中使用InputSreamReader进行字符集编码转换:

    StringBuffer xml = new StringBuffer();
            int b ;
            try (InputStreamReader in = new InputStreamReader(input,requestCoding)){
                while ((b = in.read()) != -1)
                {
                    xml.append((char)b);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            return xml.toString();
    

    优化后使用BufferedReader进行字符集编码转换:

    StringBuilder xml = new StringBuilder();
            String b;
            try (BufferedReader in = new BufferedReader(new InputStreamReader(input, requestCoding))) {
                while ((b = in.readLine()) != null) {
                    xml.append(b);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            return xml.toString();
    

    BufferedReader类与InputSreamReader类比较:
    从read()方法理解,若使用InputStreamReader的read()方法,可以发现存在每2次就会调用一次解码器解码,但若是使用 BufferedReader包装InputStreamReader后调用read()方法,可以发现只会调用一次sReader类会尽量提取比当前操作所需的更多字节,以应该更多情况下的效率提升,
    因此在设计到文件字符输入流的时候,我们使用BufferedReader中包装InputStreamReader类即可

    3.Xml转换Jaxb的优化:
    在优化了字符流了,经过测试,在并发时效率有优化但不明显未达到理想效果,通过日志分析,发现在并发情况下xml的转换时间成倍增加,分析代码发现原代码在每次转换xml时都会new一个新的模版对象,在并发时会急剧影响效率,优化后我们将xml模版对象放入一个map,只有取不到才能new一个,若能取到就使用map中的模版,大大的提升了效率。
    优化前代码:

    context = JAXBContext.newInstance(obj.getClass());
    

    优化后代码:

    JAXBContext jaxbContext = jaxbContextMap.get(obj.getClass().getName());
                if(StringUtil.isNull(jaxbContext)){
                    context = JAXBContext.newInstance(obj.getClass());
                    jaxbContextMap.put(obj.getClass().getName(),context);
                }
    

    总结:
    在平时写代码中养成良好习惯,在new对象时需考虑此段逻辑是否会使用在并发场景,若存在考虑若何避免new重复对象。

    展开全文
  • 高并发业务场景下,数据库是相对薄弱的环节,通常用户请求先访问到redis。如下图: 业务场景1:从Redis缓存读数据 业务场景2:数据库和缓存更新 一般设计数据库和缓存更新,就容易出现缓存(Redis)...

    一、业务背景

    在高并发的业务场景下,数据库是相对薄弱的环节,通常用户请求先访问到redis。

     

     

    业务场景1:从Redis缓存读数据

     

    业务场景2:数据库和缓存更新

           一般设计数据库和缓存更新,就容易出现缓存(Redis)和数据库(MySQL)间的数据一致性问题。不管是先写数据库,再删除缓存;还是反过来,都可能出现数据不一致。

    举个例子:

       1.如果删除了缓存Redis,还没有来得及写库MySQL,另一个线程就来读取,发现缓存为空,则去数据库中读取数据写入缓存,此时缓存中为脏数据。

       2.如果先写了库,在删除缓存前,写库的线程宕机了,没有删除掉缓存,则也会出现数据不一致情况。

     因为写和读是并发的,没法保证顺序,就会出现缓存和数据库的数据不一致。如来解决?这里有两个解决方案,结合业务和技术代价选择使用。

    二、缓存和数据库一致性解决方案

    1.第一种方案:采用延时双删策略

    在写库前后都进行redis.del(key)操作,并且设定合理的超时时间。

    伪代码如下

    public void write(String key,Object data){

        redis.delKey(key);

        db.updateData(data);

        Thread.sleep(500);

        redis.delKey(key);

    }

    2.具体的步骤就是:

    1)先删除缓存

    2)再写数据库

    3)休眠500毫秒

    4)再次删除缓存

    那么,这个500毫秒怎么确定的,具体该休眠多久呢?

    需要评估自己的项目的读数据业务逻辑的耗时。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。

    当然这种策略还要考虑redis和数据库主从同步的耗时。最后的的写数据的休眠时间:则在读数据业务逻辑的耗时基础上,加几百ms即可。比如:休眠1秒。

    3.设置缓存过期时间

    从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案。所有的写操作以数据库为准,只要到达缓存过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存。

    4.该方案的弊端

    结合双删策略+缓存超时设置,这样最差的情况就是在超时时间内数据存在不一致,而且又增加了写请求的耗时。并且随着业务量增加,无法准备预估休眠的时间。

    2、第二种方案:异步更新缓存(基于订阅binlog的同步机制)

    1.技术整体思路:

    MySQL binlog增量订阅消费+消息队列+增量数据更新到redis

    1)读Redis:热数据基本都在Redis

    2)写MySQL:增删改都是操作MySQL

    3)更新Redis数据:MySQ的数据操作binlog,来更新到Redis

    2.Redis更新

    1)数据操作主要分为两大块:

    一个是全量(将全部数据一次写入到redis)

    一个是增量(实时更新)

    这里说的是增量,指的是mysql的update、insert、delate变更数据。

    2)读取binlog后分析 ,利用消息队列,推送更新各台的redis缓存数据。

    这样一旦MySQL中产生了新的写入、更新、删除等操作,就可以把binlog相关的消息推送至Redis,Redis再根据binlog中的记录,对Redis进行更新。

    其实这种机制,很类似MySQL的主从备份机制,因为MySQL的主备也是通过binlog来实现的数据一致性。

    这里可以结合使用canal(阿里的一款开源框架),通过该框架可以对MySQL的binlog进行订阅,而canal正是模仿了mysql的slave数据库的备份请求,使得Redis的数据更新达到了相同的效果。

    当然,这里的消息推送工具你也可以采用别的第三方:kafka、rabbitMQ等来实现推送更新Redis。

    展开全文
  • 随着业务量的增加,目前的系统都朝着高并发、高可用的方向发展,同时带来了分布式数据的一致性问题。例如: 数据库主从架构、读写分离,存在访问时的数据一致性问题 为了进一步提高并发量,在数据库上层又引入一层...

    随着业务量的增加,目前的系统都朝着高并发、高可用的方向发展,同时带来了分布式数据的一致性问题。例如:

    1. 数据库主从架构、读写分离,存在访问时的数据一致性问题
    2. 为了进一步提高并发量,在数据库上层又引入一层缓存。现在又产生了缓存与数据库中的数据一致性问题
    3. 然后为了缓存的高并发,缓存也采用主从架构,缓存间进行主从复制,访问时又有数据一致性问题

    以下就上述问题进行分析讨论,缓存此处默认为redis。相关内容分布在其他的文章中,在此对其进行集中整合下。

    面试:MySQL基础知识——六、3主从复制

    面试:redis基础知识——八、主从复制;十、保证缓存与数据库的一致性

    一、数据库主从架构一致性问题

    1、半同步复制(不能保证强一致性)

    是介于全同步复制与全异步复制之间的一种,主库只需要等待至少一个从库节点收到并且 Flush Binlog 到 Relay Log 文件即可,主库不需要等待所有从库给主库反馈。同时,这里只是一个收到的反馈,而不是已经完全完成并且提交的反馈,如此,节省了很多时间。

    介于异步复制和全同步复制之间,相对于异步复制,半同步复制提高了数据的安全性,同时它也造成了一定程度的延迟,这个延迟最少是一个TCP/IP往返的时间。所以,半同步复制最好在低延时的网络中使用。
    在这里插入图片描述

    2、组复制(强一致性)

    组复制技术,简称MGR。是MySQL在5.7.17版本中推出的一种新的数据复制技术,这种复制技术是基于paxos协议的状态机复制。

    MGR将MySQL带入了数据强一致性的时代,是一个划时代的创新,其中一个重要原因是MGR是基于paxos协议的。

    异步复制和半同步复制都无法最终保证数据的一致性问题,半同步复制是通过判断从库响应的个数来决定是否返回给客户端,虽然数据一致性相比于异步复制有提升,但仍然无法满足对数据一致性要求高的场景,比如金融领域。MGR 很好地弥补了这两种复制模式的不足。
    在这里插入图片描述
    首先我们将多个节点共同组成一个复制组,在执行读写(RW)事务的时候,需要通过一致性协议层(Consensus)的同意,也就是读写事务想要进行提交,必须要经过组里“大多数人”(对应Node节点)的同意,大多数指的是同意的节点数量要大于N/2+1,这样才可以进行提交,而不是一方说了算。而针对只读(RO)事务则不需要经过组内同意,直接COMMIT即可。

    在一个复制组内有多个节点组成,它们各自维护了自己的数据副本,并且在一致性协议层实现了源自消息和全局有序消息,从而保证组内数据的一致性。

    3、利用中间件

    在这里插入图片描述
    (1)所有的读写都走数据库中间件,通常情况下,写请求路由到主库,读请求路由到从库。
    (2)记录所有路由到主库的key,在经验主从同步时间窗口内(假设是500ms),如果有读请求访问中间件,此时有可能从库还是旧数据,就把这个key上的读请求路由到主库。
    (3)经验主从同步时间过完后,对应key的读请求继续路由到从库。

    中间件带来的好处就是能保证数据的强一致性(概率问题),但同时也带来成本上升的问题。

    4、利用缓存

    在这里插入图片描述
    写请求:
    (1)将某个库上的某个key要发生写操作,记录在cache里,并设置“经验主从同步时间”的cache超时时间。
    (2)修改数据库。
    在这里插入图片描述
    读请求:
    (1)先到cache里查看,对应库的对应key有没有相关数据;
    (2)如果cache hit,有相关数据,说明这个key上刚发生过写操作,此时需要将请求路由到主库读最新的数据;
    (3)如果cache miss,说明这个key上近期没有发生过写操作,此时将请求路由到从库,继续读写分离。

    显然,利用缓存,减少了中间件带来的成本问题,但多了一个Cache组件,并且读写数据库多了一步Cache操作,操作相对其他稍较繁琐。

    MySQL高级——MySQL主从复制及读写分离实战

    话说数据库主从复制,读写分离,数据一致性

    数据库主从同步的作用是什么,如何解决数据不一致问题?

    二、缓存与数据库一致性问题

    更新缓存 VS 淘汰缓存

    • 主要取决于“更新缓存的复杂度
    • 更新缓存的代价很小,此时我们应该更倾向于更新缓存,以保证更高的缓存命中率
    • 更新缓存的代价很大,此时我们应该更倾向于淘汰缓存。
    • 淘汰缓存操作简单,并且带来的副作用只是增加了一次cache miss,建议作为通用的处理方式。

    1、Cache Aside Pattern(旁路缓存模式)(最常用)

    适合读请求比较多的场景

    写:

    • 先更新 DB
    • 然后直接删除 cache

    读:

    • 从 cache 中读取数据,读取到就直接返回
    • cache中读取不到的话,就从 DB 中读取数据返回
    • 再把数据放到 cache 中。

    写的部分有一些争议,网上流传很多种做法,主要有以下几种:

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

    同时有请求A和请求B对同一数据进行更新操作,那么会出现

    (1)线程A更新了数据库

    (2)线程B更新了数据库

    (3)线程B更新了缓存

    (4)线程A更新了缓存

    这就出现请求A更新缓存应该比请求B更新缓存早才对,但是因为网络等原因,B却比A更早更新了缓存。这就导致了脏数据,因此不考虑。

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

    对同一数据,同时有一个请求A进行更新操作,另一个请求B进行查询操作。那么会出现如下情形:

    (1)请求A进行写操作,删除缓存

    (2)请求B查询发现缓存不存在

    (3)请求B去数据库查询得到旧值

    (4)请求B将旧值写入缓存

    (5)请求A将新值写入数据库

    这种线程安全问题需要通过延时双删等方案解决

    大概的策略是:

    (1)先淘汰缓存

    (2)再写数据库(这两步和原来一样)

    (3)休眠x秒,再次淘汰缓存

    这么做,可以将x秒内所造成的缓存脏数据,再次删除。这个时间设定可根据业务场景进行一个调节。

    这种策略主要考虑到以下情形:假如先更新数据库,再淘汰缓存,假如缓存淘汰失败,那么后面的请求都会得到脏数据,直至缓存过期(或者下文所述的case)。假如先淘汰缓存再更新数据库,如果数据库更新失败,只会产生一次缓存miss,相比较而言,后者对业务影响更小一点。

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

    对同一数据,同时有一个请求A进行更新操作,另一个请求B进行查询操作。

    (1)请求B进行查询操作,未命中缓存

    (2)请求B去数据库查询

    (3)请求A进行写操作,执行完毕,删除缓存

    (4)请求B将旧值写入缓存

    这个case理论上会出现,不过,实际上出现的概率可能非常低,因为这个条件需要发生在读缓存时缓存失效,而且并发着有一个写操作。而实际上数据库的写操作会比读操作慢得多,而且还要锁表,而读操作必需在写操作前进入数据库操作,而又要晚于写操作删除缓存,所有的这些条件都具备的概率基本并不大。

    缺点:

    1:首次请求数据一定不在 cache 的问题

    解决办法:可以将热点数据可以提前放入cache 中。

    2:写操作比较频繁的话导致cache中的数据会被频繁被删除,这样会影响缓存命中率 。

    解决办法:

    • 数据库和缓存数据强一致场景 :更新DB的时候同样更新cache,不过我们需要加一个锁/分布式锁来保证更新cache的时候不存在线程安全问题。
    • 可以短暂地允许数据库和缓存数据不一致的场景 :更新DB的时候同样更新cache,但是给缓存加一个比较短的过期时间,这样的话就可以保证即使数据不一致的话影响也比较小。

    2、Read/Write Through Pattern(读写穿透)

    Read/Write Through Pattern 中服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中。cache 服务负责将此数据读取和写入 DB,从而减轻了应用程序的职责。

    这种缓存读写策略在平时在开发过程中非常少见。抛去性能方面的影响,大概率是因为我们经常使用的分布式缓存 Redis 并没有提供 cache 将数据写入DB的功能。

    写:

    • 先查 cache,cache 中不存在,直接更新 DB。
    • cache 中存在,则先更新 cache,然后 cache 服务自己更新 DB。

    读:

    • 从 cache 中读取数据,读取到就直接返回 。
    • 读取不到的话,cache先从 DB 加载,写入到 cache 后返回响应。

    Read-Through Pattern 实际只是在 Cache-Aside Pattern 之上进行了封装。在 Cache-Aside Pattern 下,发生读请求的时候,如果 cache 中不存在对应的数据,是由客户端自己负责把数据写入 cache,而 Read Through Pattern 则是 cache 服务自己来写入缓存的,这对客户端是透明的。

    3、Write Behind Caching Pattern(异步缓存写入)

    异步缓存写入和读写穿透很相似,两者都是由 cache 服务来负责 cache 和 DB 的读写。

    但是,两个又有很大的不同:读写穿透是同步更新 cache 和 DB,而异步缓存写入则是只更新缓存,不直接更新 DB,而是采用异步批量的方式来更新 DB。

    很明显,这种方式对数据一致性带来了更大的挑战,比如cache数据可能还没异步更新DB的话,cache服务可能就就挂掉了。

    这种策略在平时开发过程中也非常少见,但是不代表它的应用场景少,比如消息队列中消息的异步写入磁盘、MySQL 的 InnoDB Buffer Pool 机制都用到了这种策略。

    异步缓存写入下 DB 的写性能非常高,非常适合一些数据经常变化又对数据一致性要求没那么高的场景,比如浏览量、点赞量。

    3种常用的缓存读写策略

    如何保证缓存与数据库的双写一致性?

    怎么保证缓存和数据库数据的一致性?

    缓存与数据库一致性之一:缓存更新设计

    三、redis主从架构一致性问题

    Redis 的主从数据是异步同步的,所以分布式的 Redis 系统并不满足「一致性」要求。当客户端在 Redis 的主节点修改了数据后,立即返回,即使在主从网络断开的情况下,主节点依旧可以正常对外提供修改服务,所以 Redis 满足「可用性」。

    Redis 保证「最终一致性」,从节点会努力追赶主节点,最终从节点的状态会和主节点的状态将保持一致。如果网络断开了,主从节点的数据将会出现大量不一致,一旦网络恢复,从节点会采用多种策略努力追赶上落后的数据,继续尽力保持和主节点一致。

    主从复制中存在的问题

    1、延迟与不一致问题

    由于主从复制的命令传播是异步的,延迟与数据的不一致不可避免

    如果应用对数据不一致的接受程度程度较低,可能的优化措施包括:

    1. 优化主从节点之间的网络环境(如在同机房部署);
    2. 监控主从节点延迟(通过offset)判断,如果从节点延迟过大,通知应用不再通过该从节点读取数据;使用集群同时扩展写负载和读负载等。
    3. 修改从节点参数配置。当连接发生在数据同步阶段,或从节点失去与主节点的连接时。可修改从节点的slave-serve-stale-data参数。
      如果slave-serve-stale-data设置为yes(默认设置),从库会继续响应客户端的请求。
      如果slave-serve-stale-data设置为no,除去INFO和SLAVOF命令之外的任何请求都会返回一个错误”SYNC with master in progress”。

    2、数据过期问题

    在单机版Redis中,存在两种删除策略:惰性删除、 定期删除

    在主从复制场景下,为了主从节点的数据一致性,从节点不会主动删除数据,而是由主节点控制从节点中过期数据的删除。由于主节点的惰性删除和定期删除策略,都不能保证主节点及时对过期数据执行删除操作,因此,当客户端通过Redis从节点读取数据时,很容易读取到已经过期的数据。

    Redis 3.2中,从节点在读取数据时,增加了对数据是否过期的判断:如果该数据已过期,则不返回给客户端;将Redis升级到3.2可以解决数据过期问题。

    彻底搞懂 Redis 主从复制机制

    以上是个人的理解,有不足的地方,或者有更好的方法,欢迎一起讨论交流。

    展开全文
  • 点击上方IT牧场,选择置顶或者星标技术干货每日送达数据库事务是访问可能操作各种数据项的一个数据库操作序列,这些操作要么全部成功,要么全部失败。提起事务,大家都知道ACID属性。在J...
  • 找工作时,经常能在招聘信息上看到这么一条:有构建大型互联网服务及高并发经验者,优先。但对中小公司的程序员来说,高并发似乎遥不可及——公司业务不需要,用户量级不够,老板说用不上高并发架构设计...
  • 如果是十分热销的商品,就会有大量的会员等待在商品推出的那一刻,打开手机、电脑和平板电脑点击抢购,这个瞬间就会给网站带来很大的并发量,这便是一个高并发场景,处理这些并发是互联网常见的场景之一。...
  • 高并发场景下的httpClient优化使用

    千次阅读 2021-03-09 22:03:12
    使用了httpclient来完成业务。之前因为qps上不去,就看了一下业务代码,并做了一些优化,记录在这里。先对比前后:优化之前,平均执行时间是250ms;优化之后,平均执行时间是80ms,降低了三分之二的消耗,容器不再动...
  • 从工程师的角度来看,如果发红包的并发流量太,服务器则容易分分钟陷入宕机状态,撒钱都没辙。罗振宇在 2019 年的跨年演讲中提到:得到原本打算在春晚投放广告,但是被劝住了,因为春晚红包有一条不成文的规定——...
  • 智能终端设备的普及,以及业务需求的多样化,终端设备之间数据通信的重要性日益凸显,特别是在通过多种设备进行数据采集后再由计算设备进行统一处理的应用场景,如何保证实际应用中数据获取的实时性,高并发场景下的...
  • 当useCount大于1000时 这个id就不能在被使用了(换句话说 无法从数据库中查出)在高并发情况下,会遇到一种问题:假设数据表中有一条记录为 id=123456; useCount=999a与b两个连接并发查询这个id 123456 ...
  • 场景描述对于预算扣减/库存扣减类场景,我们需要根据业务对已有预算/库存做减法,拿发券的场景来举例:需要满足不同的发券需求,运营可配置预算扣减业务每次请求扣减一定数量的金额,比如发10元券给用户需要保持强...
  • 这种方案在不需要考虑高并发得去写缓存,高并发得读写缓存时,是不会有问题,但是如果是在高并发场景下,要保证缓存和数据库的一致性,至少需要解决以下问题: 高并发写时的数据不一致问题 高并发读写时,请求执行...
  • 58集团是中国互联网生活服务领域的领导者,旗下有国内最大的生活服务平台,覆盖各类业务场景,例如车业务、房产业务、本地服务、招聘业务、金融业务等等。随着业务的高速发展,越来越多的分析需求涌现...
  • 需求起因在高并发业务场景下,数据库大多数情况都是用户并发访问最薄弱的环节。所以,就需要使用redis做一个缓冲操作,让请求先访问到redis,而不是直接访问MySQL等数据库。这个业务场景,主要是解决读数据从Redis...
  • mysql高并发场景下重复插入如何保证唯一性 有家公司的题: 1.用户通过手机号进行注册,手机号仅允许注册一次, 需求如下: - 账户的注销采用软删除进行 - 注销的数据对于应用来说是不存在的,应用不允许对其有任何...
  • 谈谈高并发之缓存

    2021-03-22 14:21:07
    缓存简介众所皆知,缓存在计算机技术中应用地十分广泛。如CPU->...缓存对于支撑高并发系统,起到了非常关键的作用。缓存命中率缓存命中率 = 从缓存中成功读取的次数/总读取次数(从缓存读次数 + 从真实...
  • 需求起因在高并发业务场景下,数据库大多数情况都是用户并发访问最薄弱的环节。所以,就需要使用redis做一个缓冲操作,让请求先访问到redis,而不是直接访问MySQL等数据库。 这个业务场景,主要是解决读数据从...
  • 实现系统在面对高并发、突发流量等场景时,具备更强的稳定性和韧性,保障业务稳定运行。 阿里云解决方案 在高可用架构的基础上,通过全站加速、负载均衡、云原生数据库、削峰填谷、限流控制、弹性伸缩、性能测试等...
  • 高并发下关于利用redis分布式锁实现插入数据唯一性 (业务场景: 先根据条件查询数据库 若没有则新增 有则直接返回结果)的加锁注意事项 加锁不能像下面这样加 伪代码如下: fun(){ *lock;* val data= 根据条件...
  • 缓存一致性问题当数据时效性要求很高时,需要保证缓存中的...缓存并发问题缓存并发问题通常发生在高并发场景下, 当一个缓存key过期时,有大量的请求在获取该缓存key, 多个请求同时发现缓存过期, 因此多个请求会同时...
  • mysql如何处理高并发

    2021-02-03 07:39:14
    mysql高并发的解决方法有:1、优化SQL语句;2、优化数据库字段;3、加缓存;4、分区表;5、读写分离以及垂直拆分;6、解耦模块,水平切分等。高并发大多的瓶颈在后台,在存储mysql的正常的优化方案如下:(1)代码中...
  • 点击关注公众号,实用技术文章及时了解来源:http://blog.thankbabe.com前言高并发经常会发生在有大活跃用户量,用户高聚集的业务场景中,如:秒杀活动,定时领取红包等。为了...
  • 概述表格存储(原OTS)的一大特性是能够支撑海量数据高并发、高吞吐率的写入,特别适合日志数据或物联网场景(例如轨迹追踪或溯源)数据的写入和存储。这些场景的特性是,会在短时间内产生大量的数据需要消化并写入...
  • 天天说高并发高并发,到底啥tm是高并发啊!你们有这种困扰吗?"秒杀活动"、"抢红包"、"微博热搜"、"12306抢票"、&...
  • 前言:高并发,几乎是每个程序员都想拥有的经验。原因很简单:随着流量变大,会遇到各种各样的技术问题,比如接口响应超时、CPU load升高、GC频繁、死锁、大数据量存储等等,这些问题能推动我们在技术深度上不断精进...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 176,880
精华内容 70,752
关键字:

高并发数据业务场景