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.
这个问题的重点:版本和操作。
官方文档里面的答案:As of version 4.0, Redis has started implementing threaded actions.
Redis 处理客户端请求命令是单线程的,持久化、异步删除等功能是由后台线程程去完成的。
网络是Redis的主要瓶颈,Redis 6以及以后版本,通过多个IO线程处理网络请求。
一个或者一组线程处理多个TCP连接,无需创建或者维护过多的线程。
一个服务端进程可以同时处理多个套接字描述符。
单线程为什么快的原因:
单线程的问题:
删除大key的时候如果在主线程删除会造成卡顿,需要使用后台线程删除。
在4.0之前,大key的删除会导致性能问题。
在4.0以后,例如UNLINK命令 The actual removal will happen later asynchronously.
UNLINK key [key ...] 4.0
文件描述符:FD(FileDescriptor)
一个线程监控多个FD,
配置文件THREADED I/O模块配置下面两个内容。
# io-threads 4
# io-threads-do-reads no
io-threads-do-reads yes
海量数据查询某一固定前缀的key
如何生产上限制危险命令
MEMORY USAGE 命令你用过吗
多大算big,如何发现?如何删除?如何处理?
BigKey调优?lazyfree?
数据库有1000w记录,如何遍历。
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>
不用keys*,避免卡顿应该用什么呢?
SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]
多大算大:阿里云Redis开发规范
非 String 类型不要使用 del 删除,
哪些危害:
社交类: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
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 命令时,释放内存也会放到后台线程中执行
略
版本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其余无需执行读库的流程。
这里重要的思想是:DB拥有数据正确性的唯一解释权,所以一定要先操作DB,否则大概率出现并发问题。
唯一可用策略:先更新数据库,后删除缓存
为了防止删缓存失败,引入MQ等中间件保障删缓存的一致性。
如果真的要求一致性应该考虑是否真的引入缓存。
如果真的需要引入缓存,只能通过分布式锁来解决。
监听MySQL改动中间件Canal;
https://github.com/alibaba/canal/wiki/
虽然视频这里介绍了 canal 中间件,但是我不会首先考虑,因为引入中间件的代价是如何维护中间件的高可用。
后续如果遇到需要 canal 的场景回来补充。
略
UV:(Unique Visitor):独立访客,一般理解为IP,需要按IP去重。
PV:(page view):页面浏览量,不用去重。
DAU:(Daily Active User),日活跃用户数量。
MAU:(Monthly Active User),月活。
统计数量根据精确类型可以分为:精确和非精确。
其中精确可以使用HashSet和bitmaps等方案,非精确可以使用HyperLogLog等方案。
首先根据业务特点选择是否精确统计。
精确型数据结构的缺点:当数据量变大就会占用非常大的空间。
略
bitmap简介和后续说明
大数据量下的集合过滤—Bloom Filter
案例:快速判断在海量数据中找到数据是否存在。
布隆过滤器是一种概率性数据结构。
选型:不要求数据绝对准确。
优点:占用空间少。
缺点:可能错报,不会漏报,有可能有,没肯定没。
原理:
解决缓存穿透问题:在 Redis 查询之前加一层布隆过滤器,如果布隆过滤器中存在,进入查询流程,如果布隆过滤器不存在,则说明数据不存在。
使用细节:1:尽量一次性给足容量。2:当实际元素超过初始化时,应该新建一个布隆过滤器,执行重新写入。
布谷鸟过滤器(Cuckoo Filter)
没有预热:第一个读请求会从DB读取数据回写到Redis,
预热:第一个读请求之前
雪崩定义:同一时间key的大面积失效称为雪崩,Redis服务挂了严格意义上不属于雪崩的范畴。
预防+解决:
定义:redis像一个摆设,每次请求都会跳过Redis,落到DB,最终导致DB压力升高严重可能宕机。
方案:
空对象缓存:
优点:简单,
缺点:如果遇到黑客攻击,会缓存非常多的无效key,最终导致内存占用增加,无效内存等问题。
过滤器:
下面使用 Guava 布隆过滤器演示案例
使用案例后续会补充
空间越大,误判率越低,空间越小,误判率越大。
定义:热点key失效,大量请求落到DB。
危害:DB压力升高,严重可导致宕机。
解决方案:
案例:后续代码补充
锁的种类,JVM锁或者分布式锁。
可靠的分布式锁的条件:独占性、高可用、防死锁、不乱抢、重入性。
独占性:只有一个线程可以持有锁。
高可用:
防死锁:超时控制机制,
不乱抢:不能操作别人的锁。
重入性:同一个节点获取锁之后,无需重复加锁。
使用 hash 结构替代 string,实现可重入性。
考量:自动续期
https://redis.io/docs/manual/patterns/distributed-locks/
后续补充
后续补充
后续补充
https://redis.io/docs/reference/eviction/
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
根据算法分类:ttl、random、lfu、lru、noeviction。根据是否有过期时间分为:allkeys和volatile。
f = frequency,频率,访问次数。
r = recent,最近,时间。
建议:避免bigkey,开启lazy-free。
SDS动态字符串
双向链表
压缩列表,ziplist
哈希表,hashtable
跳表,skiplist
整数集合,intset
快速列表,quicklist
紧凑列表,listpack
redis / src 目录
《Redis设计与实现》
《Redis5设计与源码分析》
bitmap实质String
hyperLoglog实质String
geo实质Zset
Stream实质Stream
bitfield看具体的key
待补充
待补充
https://redis.io/commands/object-encoding/
OBJECT ENCODING key
Redis objects can be encoded in different ways:
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.
涉及视频的集数:165 - 177
多路复用之前的网络模型:同步阻塞网络IO模型。
多路复用要解决的问题:(复用)一个进程处理多个TCP连接。
存在的困难:假设一个进程对应10000条连接,如何才能知道哪一个连接可以进行读写呢?
我们希望当某个连接有事件发生时可以快速的将其找出来。
Redis利用 epoll 实现IO多路复用,select、poll、epoll 都是Linux的多路复用机制,可以理解为越来越先进。
同步(A):等待
异步(B):回调、通知
同步和异步:获取调用结果的方式上。
阻塞(C):调用方什么都不做。
非阻塞(D):调用方继续忙自己的。
阻塞和非阻塞:等候调用结果的行为。
四种组合:
同步阻塞(AC):客户端等待结果。
同步非阻塞(AD):
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 多线程解决多连接。
特点:使用轮训替代阻塞,线程关系一对多
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());
}
}
}
}
存在的问题:
改进思路:将上述过程放入Linux内核,
I/O multiplexing,
Windows iocp类似于Linux epoll函数的作用。
nginx同样使用epoll接收请求
FileDescriptor(FD)文件描述符
你是一个监考老师,有30个学生需要收卷,你有以下几种选择:
从1开始收,如果没写完,等待。(BIO)
呼叫29个老师,一个人应对一个学生,互不影响。(BIO多线程)
你站在讲台上,依次观察每一个学生,如果写完了收卷,重复这个过程。(Nio)
你站在讲台上等待,谁举手示意,你收谁的卷子。(多路复用)
man select
缺点:
man poll
解决了:
man epoll
epoll_create
epoll_ctl
epoll_wait
一个socket生命周期只有一次拷贝过程,使用event事件通知机制,每次socket中有数据主动通知内核,不需要遍历socket。
Redis
业务描述:wx红包
需求分析:
二倍均值法:剩余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元。