1:Redis单线程 vs 多线程
How can Redis use multiple CPUs or cores?(Redis如何使用多个CPU或内核)
https://redis.io/docs/getting-started/faq/#how-can-redis-use-multiple-cpus-or-cores
It’s not very frequent that CPU becomes your bottleneck with Redis, as usually Redis is either memory or network bound.
As of version 4.0, Redis has started implementing threaded actions.
面试题1:redis到底是单线程还是多线程
这个问题的重点:版本和操作。
官方文档里面的答案:As of version 4.0, Redis has started implementing threaded actions.
Redis 处理客户端请求命令是单线程的,持久化、异步删除等功能是由后台线程程去完成的。
网络是Redis的主要瓶颈,Redis 6以及以后版本,通过多个IO线程处理网络请求。
面试题2:IO多路复用
一个或者一组线程处理多个TCP连接,无需创建或者维护过多的线程。
一个服务端进程可以同时处理多个套接字描述符。
面试题3:redis为什么快
单线程为什么快的原因:
- 基于内存操作:
- 数据结构简单:
- 多路复用和非阻塞IO:
- 避免上下文切换:
Redis为什么选择单线程
单线程的问题:
删除大key的时候如果在主线程删除会造成卡顿,需要使用后台线程删除。
为什么逐渐加入多线程的特性?
在4.0之前,大key的删除会导致性能问题。
在4.0以后,例如UNLINK命令 The actual removal will happen later asynchronously.
UNLINK key [key ...] 4.0
Redis 性能影响因素?
- CPU:命令的复杂度。
- 内存:内存的大小。
- 网络:
Unix五种IO模型
文件描述符:FD(FileDescriptor)
一个线程监控多个FD,
103 开启多线程IO特性支持
配置文件THREADED I/O模块配置下面两个内容。
# io-threads 4
# io-threads-do-reads no
io-threads-do-reads yes
2:BigKey
104 BigKey面试题
海量数据查询某一固定前缀的key
如何生产上限制危险命令
MEMORY USAGE 命令你用过吗
多大算big,如何发现?如何删除?如何处理?
BigKey调优?lazyfree?
数据库有1000w记录,如何遍历。
105 100w记录案例和生产故障
MoreKey案例
首先准备:Redis写入数据,这里我先写入 1w。
DBSIZE命令:Return the number of keys in the currently-selected database,返回当前选择的库key数量。
禁止危险的命令
keys * 命令统计耗时,生产环境禁止使用 。
通过配置禁止这些命令 1071行
rename-command keys ""
rename-command flushdb ""
rename-command flushall ""
效果
127.0.0.1:6379> keys *
(error) ERR unknown command 'keys', with args beginning with: '*'
127.0.0.1:6379> flushall
(error) ERR unknown command 'flushall', with args beginning with:
127.0.0.1:6379> flushdb
(error) ERR unknown command 'flushdb', with args beginning with:
127.0.0.1:6379>
SCAN、SSCAN、HSCAN、ZSCAN
不用keys*,避免卡顿应该用什么呢?
SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]
106 BigKey发现删除优化策略
多大算大:阿里云Redis开发规范
非 String 类型不要使用 del 删除,
大 key 的认定:
哪些危害:
- 内存不均,集群迁移困难,超时删除,网络流量。
如何产生:
社交类:XXX命令粉丝列表,
汇总统计:
如何发现
–bigkeys
[root@localhost redis-one]# redis-cli -p 6379 -a 123456 --bigkeys
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
# Scanning the entire keyspace to find biggest keys as well as
# average sizes per key type. You can use -i 0.1 to sleep 0.1 sec
# per 100 SCAN commands (not usually needed).
-------- summary -------
Sampled 0 keys in the keyspace!
Total key length in bytes is 0 (avg len 0.00)
0 hashs with 0 fields (00.00% of keys, avg size 0.00)
0 lists with 0 items (00.00% of keys, avg size 0.00)
0 strings with 0 bytes (00.00% of keys, avg size 0.00)
0 streams with 0 entries (00.00% of keys, avg size 0.00)
0 sets with 0 members (00.00% of keys, avg size 0.00)
0 zsets with 0 members (00.00% of keys, avg size 0.00)
[root@localhost redis-one]#
MEMORY USAGE :统计某个key占用空间
127.0.0.1:6379> set k1 v1111111111111111111111111111111111111111111111
OK
127.0.0.1:6379> MEMORY USAGE k1
(integer) 104
127.0.0.1:6379>
如何删除
string:del
hash:hcan + hdel
list:LTRIM
set:SCAN + SREM
zset:zscan + zrange
BigKey 生产调优
redis.conf LAZY FREEING 1206行,默认配置和建议修改配置如下
lazyfree-lazy-eviction no
lazyfree-lazy-expire no
lazyfree-lazy-server-del no 默认no,改为yes
replica-lazy-flush no 默认no,改为yes
lazyfree-lazy-user-del no 默认no,改为yes
lazyfree-lazy-user-flush no
在执行 DEL 命令时,释放内存也会放到后台线程中执行
3:缓存双写一致性之更新策略讨论
107 面试题概览
略
108 细则要求
版本1:如下
@GetMapping("read")
public String read(String key) {
String val = stringRedisTemplate.opsForValue().get(key);
if (ObjectUtils.isEmpty(val)) {
val = fromDB();
if (ObjectUtils.isEmpty(val)) {
return null;
}
stringRedisTemplate.opsForValue().set(key, val);
}
return val;
}
存在的问题:多线程环境下会导致过多的请求落到DB,解决思路:加锁。
加锁的粒度:synchronized 或者 分布式锁,具体业务具体分析。 版本v2:如下
@GetMapping("read/v2")
public String readV2(String key) {
String val = stringRedisTemplate.opsForValue().get(key);
if (ObjectUtils.isEmpty(val)) {
//降低DB压力
synchronized (ConsistencyController.class) {
//多查一遍redis
val = stringRedisTemplate.opsForValue().get(key);
if (ObjectUtils.isEmpty(val)) {
val = fromDB();
if (ObjectUtils.isEmpty(val)) {
return null;
}
stringRedisTemplate.opsForValue().set(key, val);
}
}
}
return val;
}
为什么要多查一遍Redis:最大限度降低DB压力,如果某一个JVM进程已经写入Redis其余无需执行读库的流程。
109 四种更新策略
这里重要的思想是:DB拥有数据正确性的唯一解释权,所以一定要先操作DB,否则大概率出现并发问题。
唯一可用策略:先更新数据库,后删除缓存
为了防止删缓存失败,引入MQ等中间件保障删缓存的一致性。
如果真的要求一致性应该考虑是否真的引入缓存。
如果真的需要引入缓存,只能通过分布式锁来解决。
4:Redis与MySQL数据双写一致性工程落地案例
110 工程落地和canal简介
监听MySQL改动中间件Canal;
https://github.com/alibaba/canal/wiki/
虽然视频这里介绍了 canal 中间件,但是我不会首先考虑,因为引入中间件的代价是如何维护中间件的高可用。
111 案例代码
后续如果遇到需要 canal 的场景回来补充。
5:案例落地实战bitmap/hyperloglog/geo
112 常见面试题
略
113 数据统计
UV:(Unique Visitor):独立访客,一般理解为IP,需要按IP去重。
PV:(page view):页面浏览量,不用去重。
DAU:(Daily Active User),日活跃用户数量。
MAU:(Monthly Active User),月活。
114 去重复思路和误差率
统计数量根据精确类型可以分为:精确和非精确。
其中精确可以使用HashSet和bitmaps等方案,非精确可以使用HyperLogLog等方案。
首先根据业务特点选择是否精确统计。
精确型数据结构的缺点:当数据量变大就会占用非常大的空间。
115 HyperLogLog案例
116 GEO 面试题
略
117 GEO统计附近元素
118 bitmap
bitmap简介和后续说明
6:布隆过滤器 BloomFiler
119 Bloom Filter(布隆过滤器)
案例:快速判断在海量数据中找到数据是否存在。
120 Bloom Filter(布隆过滤器)是什么
布隆过滤器是一种概率性数据结构。
选型:不要求数据绝对准确。
优点:占用空间少。
缺点:可能错报,不会漏报,有可能有,没肯定没。
原理:
解决缓存穿透问题:在 Redis 查询之前加一层布隆过滤器,如果布隆过滤器中存在,进入查询流程,如果布隆过滤器不存在,则说明数据不存在。
使用细节:1:尽量一次性给足容量。2:当实际元素超过初始化时,应该新建一个布隆过滤器,执行重新写入。
121 布隆过滤器自研案例
122 手写布隆过滤器
布谷鸟过滤器(Cuckoo Filter)
7: 缓存常见问题(预热、雪崩、击穿、穿透)
123 简介
124 预热、雪崩、穿透
预热
没有预热:第一个读请求会从DB读取数据回写到Redis,
预热:第一个读请求之前
雪崩
雪崩定义:同一时间key的大面积失效称为雪崩,Redis服务挂了严格意义上不属于雪崩的范畴。
预防+解决:
- 针对key,分散过期时间或者永久有效。
- 针对高可用,采用主从哨兵或者集群的方式,并且开启持久化机制。
- 多级缓存思想,使用本地缓存预防雪崩。
- 限流&降级
- 人民币玩家,云数据库Redis。
穿透
定义:redis像一个摆设,每次请求都会跳过Redis,落到DB,最终导致DB压力升高严重可能宕机。
方案:
- 空对象缓存(当请求一个Redis中不存在的key时,如果DB中也没有同样回写Redis,不过缓存的是null值)
- 过滤器(布隆过滤器、谷歌guava等)
空对象缓存:
优点:简单,
缺点:如果遇到黑客攻击,会缓存非常多的无效key,最终导致内存占用增加,无效内存等问题。
过滤器:
下面使用 Guava 布隆过滤器演示案例
125 Guava 布隆过滤器
使用案例后续会补充
空间越大,误判率越低,空间越小,误判率越大。
126 缓存击穿
定义:热点key失效,大量请求落到DB。
危害:DB压力升高,严重可导致宕机。
解决方案:
- 方案一:热点key不设置失效时间。
- 方案二:从降低DB压力入手,采用不同粒度的锁。
127 聚划算案例
案例:后续代码补充
128 小总结
8:手写Redis分布式锁
129 简介
130 是什么
锁的种类,JVM锁或者分布式锁。
可靠的分布式锁的条件:独占性、高可用、防死锁、不乱抢、重入性。
独占性:只有一个线程可以持有锁。
高可用:
防死锁:超时控制机制,
不乱抢:不能操作别人的锁。
重入性:同一个节点获取锁之后,无需重复加锁。
131 迭代01
132 迭代02( lua脚本)
133 迭代03
使用 hash 结构替代 string,实现可重入性。
134 迭代04(可重入性)
135 迭代05 ()
136 迭代06(设计模式)
137 迭代07(自动续期)
考量:自动续期
138 小总结
9:RedLock算法和底层源码分析
139 RedLock底层Redisson源码深度分析
https://redis.io/docs/manual/patterns/distributed-locks/
后续补充
140 中
后续补充
141 下
后续补充
10:Redis的缓存过期淘汰策略
https://redis.io/docs/reference/eviction/
142 简介
143 内存查看和打满OOM
最大内存 maxmemory
maxmemory 100mb
Setting maxmemory to zero results into no memory limits.
This is the default behavior for 64 bit systems, while 32 bit systems use an implicit memory limit of 3GB.
查看内存使用情况 info memory
info memory
144 八种策略和使用建议
Eviction policies(驱逐策略)
- noeviction: New values aren’t saved when memory limit is reached. When a database uses replication, this applies to the primary database
- allkeys-lru: Keeps most recently used keys; removes least recently used (LRU) keys
- allkeys-lfu: Keeps frequently used keys; removes least frequently used (LFU) keys
- volatile-lru: Removes least recently used keys with the expire field set to true.
- volatile-lfu: Removes least frequently used keys with the expire field set to true.
- allkeys-random: Randomly removes keys to make space for the new data added.
- volatile-random: Randomly removes keys with expire field set to true.
- volatile-ttl: Removes keys with expire field set to true and the shortest remaining time-to-live (TTL) value.(存活时间最短的)
根据算法分类:ttl、random、lfu、lru、noeviction。根据是否有过期时间分为:allkeys和volatile。
f = frequency,频率,访问次数。
r = recent,最近,时间。
建议:避免bigkey,开启lazy-free。
11:Redis经典五大类型源码及底层实现
145 简介
SDS动态字符串
双向链表
压缩列表,ziplist
哈希表,hashtable
跳表,skiplist
整数集合,intset
快速列表,quicklist
紧凑列表,listpack
146 源码包简介
redis / src 目录
《Redis设计与实现》
《Redis5设计与源码分析》
bitmap实质String
hyperLoglog实质String
geo实质Zset
Stream实质Stream
bitfield看具体的key
147 dictEntry到RedisObject
待补充
148 redisObject各个字段含义
待补充
149 String类型数据结构
https://redis.io/commands/object-encoding/
OBJECT ENCODING
OBJECT ENCODING key
Redis objects can be encoded in different ways:
- raw, normal string encoding.(普通字符串编码)
- int, strings representing integers in a 64-bit signed interval, encoded in this way to save space.(节约空间)
- embstr, an embedded string, which is an object where the internal simple dynamic string, sds, is an unmodifiable string allocated in the same chuck as the object itself. embstr can be strings with lengths up to the hardcoded limit of OBJ_ENCODING_EMBSTR_SIZE_LIMIT or 44 bytes.
Lists can be encoded as ziplist or linkedlist. The ziplist is the special representation that is used to save space for small lists.
Sets can be encoded as intset or hashtable. The intset is a special encoding used for small sets composed solely of integers.
Hashes can be encoded as ziplist or hashtable. The ziplist is a special encoding used for small hashes.
Sorted Sets can be encoded as ziplist or skiplist format. As for the List type small sorted sets can be specially encoded using ziplist, while the skiplist encoding is the one that works with sorted sets of any size.
150 SDS
12:Redis为什么快? epoll和IO多路复用深度解析
涉及视频的集数:165 - 177
165 简介
多路复用之前的网络模型:同步阻塞网络IO模型。
多路复用要解决的问题:(复用)一个进程处理多个TCP连接。
存在的困难:假设一个进程对应10000条连接,如何才能知道哪一个连接可以进行读写呢?
我们希望当某个连接有事件发生时可以快速的将其找出来。
166 redis为什么这么快
Redis利用 epoll 实现IO多路复用,select、poll、epoll 都是Linux的多路复用机制,可以理解为越来越先进。
167 吃米线
168 同步、异步、阻塞、非阻塞
同步(A):等待
异步(B):回调、通知
同步和异步:获取调用结果的方式上。
阻塞(C):调用方什么都不做。
非阻塞(D):调用方继续忙自己的。
阻塞和非阻塞:等候调用结果的行为。
四种组合:
同步阻塞(AC):客户端等待结果。
同步非阻塞(AD):
169 BIO
c语言 recvfrom,
单线程模式:
server示例代码
public class RedisServer {
public static void main(String[] args) throws Exception {
try (ServerSocket serverSocket = new ServerSocket(6379)) {
while (true) {
System.out.println("等待建立连接");
Socket accept = serverSocket.accept();
int port = accept.getPort();
System.out.println(port + "建立连接");
InputStream is = accept.getInputStream();
byte[] bytes = new byte[36];
while (is.read(bytes) != -1) {
String value = new String(bytes, StandardCharsets.UTF_8);
System.out.println("读取到数据" + value);
}
is.close();
accept.close();
}
}
}
}
client示例代码
public class RedisClient1 {
public static void main(String[] args) throws Exception {
System.out.println("client1 start");
try (Socket socket = new Socket("127.0.0.1", 6379)) {
OutputStream os = socket.getOutputStream();
int size = 0;
while (true) {
TimeUnit.MILLISECONDS.sleep(500);
size++;
if (size == 10) {
break;
}
byte[] bytes = UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8);
os.write(bytes);
}
os.close();
}
System.out.println("client1 end");
}
}
一次只能处理一个客户端。
多线程模式:
public class RedisServerMoreThread {
public static void main(String[] args) throws Exception {
try (ServerSocket serverSocket = new ServerSocket(6379)) {
while (true) {
System.out.println("等待一个新的连接");
Socket accept = serverSocket.accept();
new Thread(() -> {
try {
InputStream is = accept.getInputStream();
byte[] bytes = new byte[36];
while (is.read(bytes) != -1) {
String value = new String(bytes, StandardCharsets.UTF_8);
System.out.println("读取到数据" + value);
}
is.close();
accept.close();
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
}
}
缺点:线程太多,
优化:1:使用线程池,2:NIO
tomcat 7 之前使用 BIO 多线程解决多连接。
170 nio
特点:使用轮训替代阻塞,线程关系一对多
public class RedisServerNio {
public static void main(String[] args) throws Exception {
List<SocketChannel> socketChannelList = new ArrayList<>();
ByteBuffer buffer = ByteBuffer.allocate(1024);
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
SocketAddress socketAddress = new InetSocketAddress("127.0.0.1", 6379);
serverSocketChannel.bind(socketAddress);
serverSocketChannel.configureBlocking(false);
System.out.println("server start...");
while (true) {
if (!socketChannelList.isEmpty()) {
for (SocketChannel socketChannel : socketChannelList) {
int read = socketChannel.read(buffer);
if (read > 0) {
buffer.flip();
byte[] bytes = new byte[read];
buffer.get(bytes);
String value = new String(bytes, StandardCharsets.UTF_8);
System.out.println(value);
buffer.clear();
}
}
}
SocketChannel accept = serverSocketChannel.accept();
if (accept != null) {
System.out.println("成功连接");
serverSocketChannel.configureBlocking(false);
socketChannelList.add(accept);
System.out.println("socketChannelList大小为" + socketChannelList.size());
}
}
}
}
存在的问题:
- 1:无效轮训 ,当socket变多的时候,如果只有少量socket存在数据,仍然会每次循环无效的socket。
- 2:资源浪费,涉及用户态和内核态频繁转换。
改进思路:将上述过程放入Linux内核,
171 io多路复用理论简介
I/O multiplexing,
Windows iocp类似于Linux epoll函数的作用。
nginx同样使用epoll接收请求
FileDescriptor(FD)文件描述符
172 从学术到人话版本
你是一个监考老师,有30个学生需要收卷,你有以下几种选择:
从1开始收,如果没写完,等待。(BIO)
呼叫29个老师,一个人应对一个学生,互不影响。(BIO多线程)
你站在讲台上,依次观察每一个学生,如果写完了收卷,重复这个过程。(Nio)
你站在讲台上等待,谁举手示意,你收谁的卷子。(多路复用)
173 能干嘛
174 select
man select
缺点:
- 一个进程最大处理1024个客户端。
- rest不能重用,每次有数据都需要置位。
- FD数组拷贝内核态,仍然有开销
- 仍然需要O(n)级别的遍历
175 poll
man poll
解决了:
- 1024 限制
- 实现了数组复用
缺点:剩下两条
176 epoll
man epoll
epoll_create
epoll_ctl
epoll_wait
一个socket生命周期只有一次拷贝过程,使用event事件通知机制,每次socket中有数据主动通知内核,不需要遍历socket。
177 兜底函数方案和系统选择
Redis
178 抢红包案例
业务描述:wx红包
需求分析:
- 1:金额拆分,每个人抢到的金额尽量均匀,尽量不受到顺序的影响。
- 2:每个人只能抢一次,并且记录抢红包信息。
- 3:计算抢红包耗时。
- 4:红包未抢完、超时金额回退。
二倍均值法:剩余M,剩余人数N,计算公式如下,(0,(M/N)*2)
@Api(tags = "微信红包")
@RestController
@RequestMapping("red-bag")
public class RedBagController {
@Autowired
StringRedisTemplate stringRedisTemplate;
public static final String LIST_KEY_PREFIX = "redbag:";
public static final String HASH_KEY_PREFIX = "redbag:record:";
@GetMapping("send")
public void sendRedBag(int money, int peopleNum) {
ArrayList<String> list = sf(money, peopleNum);
String key = LIST_KEY_PREFIX + UUID.randomUUID().toString();
stringRedisTemplate.opsForList().leftPushAll(key, list);
stringRedisTemplate.expire(key, Duration.ofDays(1));
}
/** * 抢红包 * * @param uid 实际 uid 来自于上下文或者其他地方 * @param redBagId * @return */
@GetMapping("rob")
public String rob(String uid, String redBagId) {
//红包记录
String recordKey = HASH_KEY_PREFIX + redBagId;
//红包list
String redBagKey = LIST_KEY_PREFIX + redBagId;
HashOperations<String, Object, Object> hashOps = stringRedisTemplate.opsForHash();
//判断资格,
Object userRobInfo = hashOps.get(recordKey, uid);
//可以抢
if (userRobInfo == null) {
String money = stringRedisTemplate.opsForList().rightPop(redBagKey);
//记录红包信息
if (money != null) {
hashOps.put(recordKey, uid, money);
System.out.println("抢到了" + money + "红包");
// 后续将数据数据统计........
return money;
}
return "errorCode:-1,抢完了";
}
//不可以抢多次
return "errorCode:-2,message:" + uid + "你已经抢过红包了,不能重新抢";
}
/** * 二倍均值算法 */
private ArrayList<String> sf(int money, int peopleNum) {
ArrayList<String> list = new ArrayList<>(peopleNum);
int useMoney = 0;
for (int i = 0; i < peopleNum; i++) {
//最后一个给所有的钱
if (i == peopleNum - 1) {
list.add(i, String.valueOf(money - useMoney));
continue;
}
//剩余钱数
int free = money - useMoney;
//剩余人数
int lastPeopleNum = peopleNum - i;
//max金额
int max = (free / lastPeopleNum) * 2;
//【0,max)
//实际【1,max),1是保底
//【0.01,max)
int intVal = 1 + new Random().nextInt(max - 1);
useMoney += intVal;
String val = String.valueOf(intVal);
list.add(i, val);
}
return list;
}
public static void main(String[] args) {
ArrayList<String> sf = new RedBagController().sf(100, 10);
System.out.println(sf);
}
}
说明:抢红包中的uid实际应该来自上下文而不是参数传入,案例只考虑到整数的情况,也就是最小金额1元。
文章评论