Chinaunix首页 | 论坛 | 博客
  • 博客访问: 470101
  • 博文数量: 92
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 981
  • 用 户 组: 普通用户
  • 注册时间: 2019-03-08 17:29
个人简介

分享 vivo 互联网技术干货与沙龙活动,推荐最新行业动态与热门会议。

文章分类
文章存档

2020年(63)

2019年(29)

我的朋友

分类: 数据库开发技术

2020-10-12 11:22:14

一、前言

在互联网应用中,缓存成为高并发架构的关键组件。这篇博客主要介绍缓存使用的典型场景、实操案例分析、Redis使用规范及常规 Redis 监控。

二、常见缓存对比

常见的缓存方案,有本地缓存,包括HashMap/ConcurrentHashMap、Ehcache、Memcache、Guava Cache等,缓存中间件包括Redis、Tair等。

三、Redis使用场景

1. 计数

Redis实现快速计数及缓存功能。

例如:视频或直播在线观看人数,用户每播放一次,就会自增1。

2. Session集中管理

Session可以存储在应用服务是JVM中,但这一种方案会有一致性的问题,还有高并发下,会引发JVM内存溢出。Redis将用户的Session集中管理,这种情况下只要保证Redis的高可用和扩展性,每次用户更新或查询登录都直接从Redis中信息获取。

3.限速

例如:业务要求用户一分钟内,只能获取5次验证码。

4.排行榜

关系型数据库在排行榜方面查询速度普遍偏慢,所以可以借助redis的SortedSet进行热点数据的排序。

比如在项目中,如果需要统计主播的吸金排行榜,可以以主播的id作为member, 当天打赏的活动礼物对应的热度值作为 score, 通过zrangebyscore就可以获取主播活动日榜。

5.分布式锁

在实际的多进程并发场景下,使用分布式锁来限制程序的并发执行。多用于防止高并发场景下,缓存被击穿的可能。

分布式锁的实际就是"占坑",当另一个进程来执行setnx时,发现标识位已经为1,只好放弃或者等待。

四、案例解析

1、【案例】过期设置- set命令会去掉过期时间

Redis所有的数据结构,都可以设置过期时间。如果一个字符串已经设置了过期时间,然后重新设置它,会导致过期时间消失。所以在项目中需要合理评估Redis容量,避免因为频繁set导致没有过期策略,间接导致内存被占满。

如下是 Redis 源码截图:


2、【案例】关于 Jedis 2.9.0 及以下版本过期设置 BUG

发现Jedis在进行expiredAt命令调用时有bug,最终调用的是pexpire命令,这个bug会导致key过期时间很长,导致Redis内存溢出等问题。建议升级到Jedis 2.9.1及以上版本。

BinaryJedisCluster.java源码如下:

1
2
3
4
5
6
7
8
9
@Override
  public Long pexpireAt(final byte[] key, final long millisecondsTimestamp) {
    return new JedisClusterCommand(connectionHandler, maxAttempts) {
      @Override
      public Long execute(Jedis connection) {
        return connection.pexpire(key, millisecondsTimestamp); //此处pexpire应该是pexpireAt
      }
    }.runBinary(key);
  }

对比pexpire和pexpireAt:

比如我们当前使用的时间是2018-06-14 17:00:00,它的unix时间戳为1528966800000毫秒,当我们使用PEXPIREAT命令时,由于是过去的时间,相应的key会立即过期。

而我们误用了PEXPIRE命令时,key不会立即过期,而是等到1528966800000毫秒后才过期,key过期时间会相当长,约几W天后,从而可能导致Redis内存溢出、服务器崩溃等问题。

3、【案例】缓存被击穿

缓存的key有过期策略,如果恰好在这个时间点对这个Key有大量的并发请求,这些请求发现缓存过期一般都会从后端DB回源数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压挂。

业界常用优化方案有两种

第一种:使用分布式锁,保证高并发下,仅有一个线程能回源后端DB。

第二种:保证高并发的请求到的Redis key始终是有效的,使用非用户请求回源后端,改成主动回源。一般可以使用异步任务进行缓存的主动刷新。

4、【案例】Redis-standalone架构禁止使用非0库

Redis执行命令select 0和select 1切换,造成性能损耗。

RedisTemplate在执行execute方法的时候会先获取链接。

执行到RedisConnectionUtils.java,会有一段获取链接的方法。

JedisConnectionFactory.java 会调用JedisConnection构造器,注意这边的dbIndex就是数据库编号,如:1

继续跟进JedisConnection代码,当选择库大于1时,会有select db操作。如果一直使用0库是不需要额外执行切库命令的。知道了第一个切库select 1的地方,那么select 0是哪来的呢?

其实客户端使用Redis也会是要释放链接的,只不过RedisTemplate已经帮我们自动释放了,让我们再回到一开始RedisTemplate执行execute(...)方法的地方。

下面还是RedisConnectionUtils.java,执行链接关闭的代码。

按代码注释的意思,如果选择库编号不为0,spring-data-redis框架每次都会执行重置select 0!


笔者在vivo商城业务中,商品详情页接口经过上面的调优,性能提高了3倍多。

进一步验证数据库切换至少影响性能3倍左右(视具体业务而定)。

Rediscluster集群数据库,默认0库,无法选择其他数据库,也就避免了这个问题。

5、【案例】当心时间复杂度o(n)Redis命令

Redis是单线程的,所以线程安全的。

Redis使用非阻塞IO,并且大部分命令的时间复杂度O(1)。

使用高耗时的命令是非常危险的,会占用唯一的一个线程的大量处理时间,导致所有的请求都被拖慢。

例如:获取所有set集合中的元素 smembers myset,返回指定Hash中所有的member,时间复杂度O(N)。

缓存的Value集合变大,当高并接口请求时,会从Redis读取相关数据,每个请求读取的时间变长,不断的叠加,导致出现热点KEY情况,Redis某个分片处于阻塞,CPU使用率达到100%。

6、【案例】缓存热key

在Redis中,访问频率高的key称为热点key,当某一热点key的请求到Server主机时,由于请求量特别大,导致主机资源不足,甚至宕机,影响正常的服务。

热key问题的产生,有如下两种原因:

  1. 用户消费的数据远大于生产的数据,比如热卖商品或秒杀商品、热点新闻、热点评论等,这些典型的读多写少的场景会产生热点问题。
  2. 请求分片集中,超过单Server的性能极限,比如 固定名称key,哈希落入一台Server,访问量极大的情况,超过Server极限时,就会导致热点Key问题的产生。

那么在实际业务中,如何识别到热点key呢?

  1. 凭借业务经验,进行预估哪些是热key;
  2. 客户端统计收集,本地统计或者上报;
  3. 如果服务端有代理层,可以在代理层进行收集上报;

当我们识别到热key,如何解决热key问题

  1. Redis集群扩容:增加分片副本,均衡读流量;
  2. 进一步对热key进行散列,比如将一个key备份为key1,key2……keyN,同样的数据N个备份,N个备份分布到不同分片,访问时可随机访问N个备份中的一个,进一步分担读流量。
  3. 使用二级缓存,即本地缓存。

当发现热key后,将热key对应数据首先加载到应用服务器本地缓存中,减少对Redis的读请求。

五、Redis 规范

1、禁止使用非database 0

说明:

Redis-standalone架构,禁止使用Redis中的其他database。

原由:

  • 为以后业务迁移 Redis Cluster 保持兼容性.
  • 多个 database 用 select 切换时,更消耗CPU资源。
  • 更易自动化运维管理,如 scan/dbsize 命令只用于当database。
  • 部分 Redis Clients 因线程安全问题,不支持单实例多 database。

2、Key设计规范

按业务功能命名key前缀,防止key冲突覆盖,推荐 用冒号分隔,例如,业务名:表名:id:,如 live:rank:user:weekly:1:202003。

Key的长度小于30个字符,Key名字本身是String对象,Redis硬编码限制最大长度512MB。

在Redis缓存场景,推荐Key都设置TTL值,保证不使用的Key能被及时清理或淘汰。

Key设计时禁止包含特殊字符,如空格、换行、单双引号以及其他转义字符。

阅读(3914) | 评论(0) | 转发(1) |
给主人留下些什么吧!~~