redis实践及思考

导语:当面临存储选型时是选择关系型还是非关系型数据库?如果选择了非关系型的redis,redis常用数据类型占用内存大小如何估算的?redis的性能瓶颈又在哪里?

背景

前段时间接手了一个业务,响应时间达到10s左右。阅读源码后发现,每一次请求都是查询多个分表数据(task1,task2….),然后再join其他表(course,teacher..),时间全部花在了大量磁盘I/O上。脑袋一拍,重构,上redis!

为什么选择redis

拍脑袋做技术方案肯定是不行的,得用数据和逻辑说服别人才可以。

时延

时延=后端发起请求db(用户态拷贝请求到内核态)+ 网络时延 + 数据库寻址和读取如果想要降低时延,只能减少请求数(合并多个后端请求)和减少数据库寻址和读取得时间。从降低时延的角度,基于单线程和内存的redis,每秒10万次得读写性能肯定远远胜过磁盘读写性能。

数据规模

以redis一组K-V为例(”hello” -> “world”),一个简单的set命令最终会产生4个消耗内存的结构。

关于Redis数据存储的细节,又要涉及到内存分配器(如jemalloc),简单说就是存储170字节,其实内存分配器会分配192字节存储。

那么总的花费就是

  • 一个dictEntry,24字节,jemalloc会分配32字节的内存块
  • 一个redisObject,16字节,jemalloc会分配16字节的内存块
  • 一个key,5字节,所以SDS(key)需要5+9=14个字节,jemalloc会分配16字节的内存块
  • 一个value,5字节,所以SDS(value)需要5+9=14个字节,jemalloc会分配16字节的内存块

综上,一个dictEntry需要32+16+16+16=80个字节。上面这个算法只是举个例子,想要更深入计算出redis所有数据结构的内存大小,可以参考这篇文章。笔者使用的是哈希结构,这个业务需求大概一年的数据量是200MB,从使用redis成本上考虑没有问题。

需求特点

笔者这个需求背景读多写少,冷数据占比比较大,但数据结构又很复杂(涉及多个维度数据总和),因此只要启动定时任务离线增量写入redis,请求到达时直接读取redis中的数据,无疑可以减少响应时间。

[ 最终方案 ]

redis瓶颈和优化

HGETALL

最终存储到redis中的数据结构如下图。

采用同步的方式对三个月(90天)进行HGETALL操作,每一天花费30ms,90次就是2700ms!redis操作读取应该是ns级别的,怎么会这么慢?利用多核cpu计算会不会更快?

常识告诉我,redis指令执行速度 >> 网络通信(内网) > read/write等系统调用。因此这里其实是I/O密集型场景,就算利用多核cpu,也解决不到根本的问题,最终影响redis性能,**其实是网卡收发数据用户态内核态数据拷贝**

pipeline

这个需求qps很小,所以网卡也不是瓶颈了,想要把需求优化到1s以内,减少I/O的次数是关键。换句话说,充分利用带宽,增大系统吞吐量。

于是我把代码改了一版,原来是90次I/O,现在通过redis pipeline操作,一次请求半个月,那么3个月就是6次I/O。很开心,时间一下子少了1000ms。

pipeline携带的命令数

代码写到这里,我不经反问自己,为什么一次pipeline携带15个HGETALL命令,不是30个,不是40个?换句话说,一次pipeline携带多少个HGETALL命令才会发起一次I/O?

我使用是golang的redisgo 的客户端,翻阅源码发现,redisgo执行pipeline逻辑是 把命令和参数写到golang原生的bufio中,如果超过bufio默认最大值(4096字节),就发起一次I/O,flush到内核态。

redisgo编码pipeline规则如下图,*表示后面参数加命令的个数,$表示后面的字符长度,一条HGEALL命令实际占45字节。

那其实90天数据,一次I/O就可以搞定了(90 * 45 < 4096字节)!

果然,又快了1000ms,耗费时间达到了1秒以内

对吞吐量和qps的取舍

笔者需求任务算是完成了,可是再进一步思考,redis的pipeline一次性带上多少HGETALL操作的key才是合理的呢?换句话说,服务器吞吐量大了,可能就会导致qps急剧下降(网卡大量收发数据和redis内部协议解析,redis命令排队堆积,从而导致的缓慢),而想要qps高,服务器吞吐量可能就要降下来,无法很好的利用带宽。对两者之间的取舍,同样是不能拍脑袋决定的,用压测数据说话!

简单写了一个压测程序,通过比较请求量和qps的关系,来看一下吞吐量和qps的变化,从而选择一个适合业务需求的值。

package main
import (
    "crypto/rand"
    "fmt"
    "math/big"
    "strconv"
    "time"
    "github.com/garyburd/redigo/redis"
)
const redisKey = "redis_test_key:%s"
func main() {
    for i := 1; i < 10000; i++ {
        testRedisHGETALL(getPreKeyAndLoopTime(i))
    }
}
func testRedisHGETALL(keyList [][]string) {
    Conn, err := redis.Dial("tcp", "127.0.0.1:6379")
    if err != nil {
        fmt.Println(err)
        return
    }

    costTime := int64(0)
    start := time.Now().Unix()
    for _, keys := range keyList {
        for _, key := range keys {
            Conn.Send("HGETALL", fmt.Sprintf(redisKey, key))
        }
        Conn.Flush()
    }
    end := time.Now().Unix()
    costTime = end - start
    fmt.Printf("cost_time=[%+v]ms,qps=[%+v],keyLen=[%+v],totalBytes=[%+v]",
        1000*int64(len(keyList))/costTime, costTime/int64(len(keyList)), len(keyList), len(keyList)*len(keyList[0])*len(redisKey))
}

//根据key的长度,设置不同的循环次数,平均计算,取除网络延迟带来的影响
func getPreKeyAndLoopTime(keyLen int) [][]string {
    loopTime := 1000
    if keyLen < 10 {
        loopTime *= 100
    } else if keyLen < 100 {
        loopTime *= 50
    } else if keyLen < 500 {
        loopTime *= 10
    } else if keyLen < 1000 {
        loopTime *= 5
    }
    return generateKeys(keyLen, loopTime)
}

func generateKeys(keyLen, looTime int) [][]string {
    keyList := make([][]string, 0)
    for i := 0; i < looTime; i++ {
        keys := make([]string, 0)
        for i := 0; i < keyLen; i++ {
            result, _ := rand.Int(rand.Reader, big.NewInt(100))
            keys = append(keys, strconv.FormatInt(result.Int64(), 10))
        }
        keyList = append(keyList, keys)
    }
    return keyList
}

windows上单机版redis结果如下:

扩展 (分布式方案下pipeline操作)

需求最终是完成了,可是转念一想,现在都是集群版的redis,pipeline批量请求的key可能分布在不同的机器上,但pipeline请求最终可能只被一台redis server处理,那不就是会读取数据失败吗?于是,笔者查找几个通用的redis 分布式方案,看看他们是如何处理这pipeline问题的。

redis cluster

redis cluster 是官方给出的分布式方案。 Redis Cluster在设计中没有使用一致性哈希,而是使用数据分片(Sharding)引入哈希槽(hash slot)来实现。一个 Redis Cluster包含16384(0~16383)个哈希槽,存储在Redis Cluster中的所有键都会被映射到这些slot中,集群中的每个键都属于这16384个哈希槽中的一个,集群使用公式slot=CRC16 key/16384来计算key属于哪个槽。比如redis cluster有5个节点,每个节点就负责一部分哈希槽,如果参数的多个key在不同的slot,在不同的主机上,那么必然会出错。因此redis cluster分布式方案是不支持pipeline操作,如果想要做,只有客户端缓存slot和redis节点的关系,在批量请求时,就通过key算出不同的slot以及redis节点,并行的进行pipeline。

github.com/go-redis就是这样做的,有兴趣可以阅读下源码。

codis

市面上还流行着一种在客户端和服务端之间增设代理的方案,比如codis就是这样。对于上层应用来说,连接 Codis-Proxy 和直接连接 原生的 Redis-Server 没有的区别,也就是说codis-proxy会帮你做上面并行分槽请求redis server,然后合并结果在一起的操作,对于使用者来说无感知。

总结

在做需求的过程中,发现了很多东西不能拍脑袋决定,而是前期做技术方案的时候,想清楚,调研好,用数据和逻辑去说服自己。

Image placeholder
Zsoner
未设置
  15人点赞

没有讨论,发表一下自己的看法吧

推荐文章
基于Redis实现Spring Cloud Gateway的动态管理

引言:SpringCloudGateway是当前使用非常广泛的一种API网关。它本身能力并不能完全满足企业对网关的期望,人们希望它可以提供更多的服务治理能力。但SpringCloudGateway并不

实践和思考的重要意义(论软件代码设计)

感触 最近这段时间,包括以前,经常听到,程序员们大谈设计模式,这个话题并不陌生,面试必问的问题,活了这么多年,我就一直没搞清楚,为啥面试官喜欢问这个问题。如果一个面试官喜欢问这种问题,我觉得也没啥意思

实践和思考的重要意义(论软件代码设计)

感触最近这段时间,包括以前,经常听到,程序员们大谈设计模式,这个话题并不陌生,面试必问的问题,活了这么多年,我就一直没搞清楚,为啥面试官喜欢问这个问题。如果一个面试官喜欢问这种问题,我觉得也没啥意思。

SACC 2019:达梦数据库推进实践与思考

2019年10月31日~11月2日,由IT168旗下ITPUB企业社区平台主办的第十一届中国系统架构师大会(SACC2019)在北京成功召开。本届大会继续沿用四大主线并行的演讲模式,设置业务系统架构设

瓜子二手车在 Dubbo 版本升级、多机房方案方面的思考和实践

前言随着瓜子业务的不断发展,系统规模在逐渐扩大,目前在瓜子的私有云上已经运行着数百个Dubbo应用,上千个Dubbo实例。瓜子各部门业务迅速发展,版本没有来得及统一,各个部门都有自己的用法。随着第二机

CSS实现自适应分隔线的N种方法

分割线是网页中比较常见的一类设计了,比如说知乎的更多回答这里的自适应是指两边的横线会随着文字的个数和父级的宽度自适应偷偷的看了一下知乎的实现,很显然是用一块白色背景覆盖的,加一点背景就露馅了心想:知乎

怎样用css实现图片不间断滚动

怎样用css实现图片不间断滚动效果图:思路分析:第一步,定义div>ul>li*7,因为有7张图片。第二步,设置div的宽度高度以及li的浮动,保证它们全部在div内的同一行。第三步,实现滚动。用到了

CSS实现多行省略

什么是多行省略?当字数多到一定程度就显示省略号点点点。最初只是简单的点点点,之后花样越来越多,点点点加下箭头,点点点加更多,点点点加更多加箭头...。多行省略就是大段文字后面的花式点点点。同行这么做:

基于Webpack的css sprites实现方案

一、前言关于csssprites(雪碧图/精灵图)的几种实现方案可以参考浅谈CSSSprites雪碧图应用。本文主要讨论基于webpack的csssprites实现方案。由于使用webpack时会涉及

Ant Design Vue 中a-upload组件通过axios实现文件列表上传与更新回显的前后端处理方案

前言在企业应用的快速开发中,我们需要尽快的完成一些功能。如果您使用了AntDesignVue,在进行表单的文件上传相关功能开发的时候,您肯定迫不及待地需要找到一篇包治百病的文章,正是如此,才有了该文的

js实现固定大小轮播图

{ value.onclick=function(){ //赋值索引 indexActive=index //执行轮播方法 swipterMoveFn() } }) //右箭头点击 arrow

自动驾驶思考:仿真系统构建

如何构建自动驾驶仿真系统? 仿真最主要的目的是:通过模拟真实环境和构建汽车模型,找出自动驾驶过程中可能出现的问题。 那么如何构建自动驾驶仿真系统呢?目前主流的实现方式是通过游戏引擎来模拟真实环境,通

技术大牛创业失败,原来是缺少这套思考框架

2016年以前,大众媒体对技术人创业的报道可以总结为一句话:“为何技术人创业更容易成功?”,2018年后,这个总结变成了“一个程序员创业的血泪史”。这样的转变令人哭笑不得。最近几年,技术创业者多到让

工程师笔记:我对数据库系统云原生化的一些思考

作者|张敏(于期)阿里云智能高级技术专家划重点我眼中的云原生我认为的云原生关键能力我眼中的云原生化技术手段我对数据库云原生化的思考伴随着云原生技术越来越热门,阿里内部关于CloudNative、Ser

税务信息化跨入大数据云计算时代的思考

现状,目前据了解国税总局执行征收管理、行政管理、决策支持和外部信息等四大类应用系统在全国的推广部署,实施大数据开放与共享的建设与开发,已经完成2个国家级税务处理中心的扩容,包括计算存储资源、系统软件及

Dubbo 在 K8s 下的思考

作者|曹胜利  ApacheDubboPMC导读:Dubbo作为高性能JavaRPC框架的刻板印象早已深入人心,在CloudNative的架构选型上,SpringCloud或许才是业界的优先选择。实际

人社部大数据应用场景思考

文/涵诚人社部尹蔚民部长在2017年5月全国“互联网+人社”座谈会指出,要充分运用大数据手段,通过“互联网+人社”,实现决策科学、管理精准化、服务人本化,人社的统计数据对于服务决策、研究政策、支撑事业

万万没想到,HashMap默认容量的选择,竟然背后有这么多思考!?

集合是Java开发日常开发中经常会使用到的,而作为一种典型的K-V结构的数据结构,HashMap对于Java开发者一定不陌生。在日常开发中,我们经常会像如下方式以下创建一个HashMap:Map ma

分布式场景下Kafka消息顺序性的思考

在业务中使用kafka发送消息异步消费的场景,并且需要实现在消费时实现顺序消费,利用kafka在partition内消息有序的特点,实现消息消费时的有序性。1、在发送消息时,通过指定partition

一个业务小需求引发了我对代码的思考

课程推荐:web全栈开发就业班--拿到offer再缴学费--融职教育 前言根据公司的业务需求我是这么封装组件上文是前段时间写了一篇根据公司的业务需求我是如何封装组件,也算是对那段忙碌的工作一份小小的总

关于后台管理系统前端项目的思考

课程推荐:web全栈开发就业班--拿到offer再缴学费--融职教育 了解需求,熟悉掌握需求这一要求无论是对于前端开发人员或是其他端的开发人员,都是能够顺利开发项目的前提。在开发项目之前,需对PM的需

从Oracle到PostgreSQL,某保险公司迁移实践

摘要:去O一直是金融保险行业永恒的话题,但去O的难度之大也只有真正经历过的人才知其中的艰辛。此次笔者结合实际去O工作,对去O过程中碰到的DBLINK、SEQUENCE最大值、空串、SQL语句中的别名等

从 Oracle 到 PostgreSQL ,某保险公司迁移实践

作者 |章晨曦编辑 | 老鱼摘要:去O一直是金融保险行业永恒的话题,但去O的难度之大也只有真正经历过的人才知其中的艰辛。此次笔者结合实际去O工作,对去O过程中碰到的DBLINK、SEQUENCE最大值

Code Review最佳实践

我一直认为CodeReview(代码审查)是软件开发中的最佳实践之一,可以有效提高整体代码质量,及时发现代码中可能存在的问题。包括像Google、微软这些公司,CodeReview都是基本要求,代码合

springDataJpa 最佳实践

springDataJpa最佳实践 前言 SpringDataJpa框架的目标是显著减少实现各种持久性存储的数据访问层所需的样板代码量。SpringDataJpa存储库抽象中的中央接口是Reposit