《Redis设计与实现》读书笔记

312 阅读20分钟

1 Redis常见的数据类型

Redis常见的5种数据类型包括string,list,hash,set,zset,每一种数据类型的编码可以有多种,例如list类型的底层数据结构可以是双向链表,也可以是跳跃表。下面先介绍redis底层的6种数据结构。

数据结构

简单动态字符串SDS

SDS和C字符串的区别:

  • 以常数复杂度获取字符串的长度,通过len记录了字符串的长度。
  • 不会出现缓冲区溢出
    • C字符串拼接,长度不够会产生缓冲区溢出;
    • SDS字符串拼接,先检查SDS空间是否足够,不够先扩容。
  • 减少字符串修改时带来的内存重新分配次数
    • 空间预分配:修改sds时会对buf进行扩容,当buf<1M时翻倍扩容,即给free分配的空间大小和len一致。当buf>1M时,给free分配1M。
    • 惰性空间释放:sds缩短时,释放的空间先不释放,由free记录。
  • 二进制安全
    • 可以保持文本、图片、音频等数据。sds通过len记录了字符串的长度,不像C字符串通过''\0'判断是否到字符串末位。
  • 兼容部分C字符串的函数

链表

双端无环链表:节点通过prev和next指针实现,表头节点的prev,表尾节点的next都指向NULL 通过head和tail指针可以快速获取表头与表尾,通过len可以快速获取链表长度。

字典

字典数据结构用于实现Redis的哈希键、数据库等,图中字典有两个哈希表,一个平时使用,一个仅在rehash时使用

  • 计算键值对放到哈希数组的哪个位置,Redis使用MurmurHash2算法计算键的哈希值: hash=dict->type->hashFunction(key); index=hash & dict->ht[x].sizemask;
  • 键冲突:采用链地址法解决。
  • 渐进式rehash过程:
    • 给ht[1]哈希表分配空间
      • 扩展操作,空间大小为第一个大于等于ht[0].used*2的2^n;
      • 收缩操作,空间大小为第一个大于等于ht[0].used的2^n。
    • rehashidx设置为0,表示rehash开始。将ht[0]在rehashidx索引上的键值对rehash到ht[1],rehash完成rehashidx值增一,ht[0]的键值对全部rehash至ht[1],rehashidx设置为-1,表示rehash操作已完成。
    • 释放ht[0],将ht[1]设置为ht[0],并在ht[1]新创建一个空白哈希表。

跳跃表

Redis只有两个地方用到了跳跃表结构,一是用于实现有序集合,二是在集群节点中用作内部数据结构。 level:层数最大的那个节点的层数,节点的层数是1-32的随机数 length:跳跃表节点数量(不包括表头节点); 层:节点中用L1,L2等字样表示,每个层带有前进指针、跨度两个属性; 后退指针:BW,反向遍历时使用; 分值:跳跃表的节点按分值从小到大排列;分值相同时,节点按成员对象的大小进行排序; 成员对象:通过o1,o2字样表示,成员对象必需唯一。

整数集合

整数集合是集合键的底层实现之一,集合元素有序且不重复,可以保存int16,int32,int64的整数值,具体的类型由encoding决定。 升级:当前所有元素为int16,添加新元素为int32,此时需要先升级,扩展数组空间,每个元素空间为32位。 降级:不支持降级。

压缩列表

压缩列表(ziplist)是Redis为了节约内存而开发的,其结构如下: zlbytes:整个压缩列表占用的内存字节数; zltail:表尾节点距离压缩列表的起始地址有多少字节; zllen:节点数量; entryX:节点,可以保存字节数组或者整数; zlend:特殊值0xFF,标记压缩列表的末端。 压缩列表的节点结构如下:

  • previous_entry_length:前一个节点的长度,添加、删除节点可能产生连锁更新问题。
    • 前一个节点长度小于254字节,previous_entry_length长度为1个字节;
    • 前一个节点长度小大于等于254字节,previous_entry_length长度为5个字节。
  • encoding:记录content保持的数据类型及长度,数组、整数值的类型和长度由编码出去最高两位的其他位记录。 | 编码 | 编码长度| content保持的值| | :----- | --------: | --------: | | 00 | 1字节 | 长度小于等于63字节的字节数组| | 01 | 2字节 | 长度小于等于16383字节的字节数组| | 10 | 5字节 | 长度小于等于4294967295字节的字节数组| | 11 | 1字节 | 整数 |
  • content:保存节点的值。

数据类型

Redis常见的5种数据对象包括字符串、列表、哈希、集合、有序集合对象,对象的结构为:

typedef struct redisObject{
	unsigned type:4; //类型
	unsigned encoding:4; //编码
	void *ptr; //指向底层实现数据结构的指针
	...
}

常见的5种数据对象的类型和编码如下:

数据类型typeencoding编码条件
字符串REDIS_STRINGREDIS_ENCODING_INT整数
字符串REDIS_STRINGREDIS_ENCODING_EMBSTR字符串长度小于等于39字节,浮点数
字符串REDIS_STRINGREDIS_ENCODING_RAW字符串长度大于39字节,浮点数
列表REDIS_LISTREDIS_ENCODING_ZIPLIST字符串长度<64字节,且数量<512个
列表REDIS_LISTREDIS_ENCODING_LINKEDLIST不满足ZIPLIST编码条件时
哈希REDIS_HASHREDIS_ENCODING_ZIPLIST键和值的长度都<64字节,且键值对数量<512个
哈希REDIS_HASHREDIS_ENCODING_HT不满足ZIPLIST编码条件时
集合REDIS_SETREDIS_ENCODING_INTSET元素都是整数,且数量<512个
集合REDIS_SETREDIS_ENCODING_HT不满足INTSET编码条件时
有序集合REDIS_ZSETREDIS_ENCODING_ZIPLIST字符串长度<64字节,且数量<128个
有序集合REDIS_ZSETREDIS_ENCODING_SKIPLIST不满足ZIPLIST编码条件时
注意:部分编码条件的上限值可以通过配置文件修改。

2 Redis过期键删除策略

  • 惰性删除:获取某个key时,检查key是否过期,如果过期了就删除;
  • 定期删除:每隔一段时间,检查数据库,删除过期键;每秒检查10次,每次检查如果超过25%的键过期,则触发下一次检查
  • 内存淘汰机制

3 数据持久化

数据库存储内容

Redis是一个键值对数据库服务器,所有的键值对存储在dict字典结构中。下图展示了一个带有过期字典的数据库例子。

RDB持久化

RDB持久化将redis在内存中的数据库状态保持到磁盘,生成一个压缩的二进制文件,通过RDB文件,可以还原数据库状态。

生成RDB文件

  • SAVE:阻塞Redis服务器进程,直到RDB文件创建完成。
  • BGSAVE:派生子进程,由子进程负责创建RDB文件。 BGSAVE执行期间,拒绝执行SAVE、BGSAVE命令,BGREWRITEAOF延迟到BGSAVE执行完之后。 BGREWRITEAOF执行期间,拒绝执行BGSAVE命令。
  • 自动间隔性保存 save 900 1 save 300 10 save 60 10000 服务器900秒内,对数据库进行了至少一次修改,服务器自动执行一次BGSAVE命令。服务器通过dirty记录修改次数,通过lastsave记录上一次执行SAVE或BGSAVE的命令时间,由这两个参数实现自动间隔性保存。

载入RDB文件

服务器启动时自动执行,没有专门的命令。 如果开启了AOF持久化功能,优先使用AOF文件还原数据库,AOF持久化关闭,使用RDB文件还原数据库。

RDB文件结构

  • RDB文件结构如下: RDB文件最开头是REDIS,长度为5字节,保存着"REDIS"五个字符,用于快速检查载入的文件是否是RDB文件。 db_version长度为4字节,记录RDB文件的版本号。 database包含零个或任意多个数据库,以及各个数据库中的键值对。 EOF长度为1字节,标志RDB文件正文内容结束。 check_sum是一个8字节的无符号整数,通过对前面4部分内容计算得出,用于检查RDB文件是否损坏。
  • database部分数据库结构如下:
  • pairs保存了数据库所有的键值对和过期时间,结构如下: EXPIRETIME_MS为1字节,表示接下来读入的将是一个毫秒为单位的过期时间。 ms为8字节,记录毫秒为单位的UNIX时间戳。 TYPE为1字节,记录了value的类型。 key是字符串对象。 如果TYPE为REDIS_RDB_TYPE_STRING,那么value保存的是一个字符串对象,字符串对象的编码可以是REDIS_ENCODING_INT或者REDIS_ENCODING_RAW。如果字符串的长度大于20字节,将压缩后再保存。
  • 压缩后的字符串结构如下: REDIS_RDB_ENC_LZF标志着字符串被LZF算法压缩过。 compressed_len记录字符串被压缩后的长度。 origin_len记录原长度。 compressed_string记录被压缩后的字符串。 压缩后的字符结构示例如下:

AOF持久化

AOF持久化通过保存REDIS服务器执行的命令来记录数据库的状态,文件内容如下:

生成AOF文件

服务器执行完一个写命令,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区的末尾。服务器每结束一个事件循环之前,都会考虑是否需要将aof_buf缓冲区中的内容写入和保存到AOF文件。具体是否需要由服务器配置的appendfsync的值决定。

  • always 将aof_buf缓冲区的所有内容写入并同步到AOF文件
  • everysec ,默认配置,将aof_buf缓冲区的所有内容写入到AOF文件,如果距离上次同步超过一秒钟,那么在此对AOF文件进行同步。(写入:保存到内存缓冲区,同步:写入磁盘)。
  • no 将aof_buf缓冲区的所有内容写入到AOF文件,何时同步由操作系统决定。

载入AOF文件

AOF重写

AOF文件重写不需要对现有的AOF文件进行操作,这个功能是通过读取服务器当前的数据库状态来实现的。从数据库读取键当前的值,代替之前记录这个键值对的多条命令。 AOF重写由子线程执行,但是在子进程进行AOF重写期间,服务器进程处新的命令,可能会对现有数据库状态进行修改,导致当前数据库状态和重写后的AOF文件所保存的数据库状态不一致。 为了解决这一问题,Redis服务器设置了一个AOF重写缓冲区,这个缓冲区在服务器创建子进程之后开始使用,当Redis服务器执行完一个写命令后,它会同时将这个写命令发送给AOF缓冲区和AOF重写缓冲区。 AOF重写过程如下:整个过程中,只有T8和T9执行时会阻塞服务器进程。

4 Redis使用常见问题

redis的QPS为10万-100万量级,数据库的QPS为1000量级

缓存穿透

  • 产生原因:大量的请求访问不存在的key,redis没有缓存,如果百万级请求都落到数据库,可能导致数据库宕机。
  • 解决方式:
    • 缓存空值,根据不存在的key查数据库,将查到的空值缓存。如果是恶意攻击,每次请求的key都不一样,这种情况无法解决。
    • 布隆过滤器,用来判断key是否存在于某个集合中,如果不存在就直接返回,存在查缓存,查 数据库。

缓存击穿

  • 产生原因:高并发下,大量请求同时查询某个key,这个key正好失效了,比如热点数据失效
  • 解决方式:查数据库时加互斥锁。

缓存雪崩

  • 产生原因:某一时刻发生大规模的缓存失效,比如redis宕机了,请求都落到数据库,可能导致数据库宕机。
  • 解决方式:
    • redis集群,保证缓存服务的高可用。
    • ehcache本地缓存+Hystrix限流&降级,降级类似于在页面提示用户"服务器崩溃请刷新重试"。
    • redis持久化,重启服务,并通过持久化文件恢复缓存数据。

5 redis常用命令

  • 连接redis 进入到redis组件的bin目录,执行./redis-cli -p 7019 -a Qz3Qp9Fq1F

  • 手动执行持久化,生成rdb文件

    127.0.0.1:7019> bgsave

  • 关闭持久化,redislinux64的conf/redis.conf文件

    • 取消rdb持久化

      # save 900 1
      # save 300 10
      # save 60 10000

    • 取消aof持久化

      appendonly no

    • 取消混合持久化

      aof-use-rdb-preamble no

  • 选择数据库:select 7

  • 查询key:get xxx:login:gb35114:isAuth

  • 查看所有key:keys *

  • 搜索key:keys abc*

  • 删除key:del xxx

  • redis监控管理:info命令

    # Clients
    connected clients:130
    # Memory
    used memory:10670184
    used memory human:10.18M
    used memory rss:11345920

  • 查看连接的客户端:./redis-cli -p 7019 -a Qz3Qp9Fq1F -c client list

6 项目中redis配置

  • rdb持久化关闭

    # save 900 1
    # save 300 10
    # save 60 10000

  • 内存为8G

    maxmemory 8589934592

  • 内存容量超过maxmemory后的处理策略,Redis的内存淘汰策略是指在Redis用于缓存的内存不足时,如何处理新写入且需要申请额外空间的数据。

    maxmemory-policy volatile-lru

    • noeviction:当内存不足容纳新写入数据时,新写入操作就会报错。
    • allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。
    • allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。
    • volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
    • volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除 某个key。
    • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。
      Redis的内存淘汰策略的选取并不会影响过期的key的处理,内存淘汰策略用于处理内存不足的需要申请额外空间的数据,过期策略用于处理过期的缓存数据。
  • 惰性删除配置

    lazyfree-lazy-eviction yes
    lazyfree-lazy-expire yes
    lazyfree-lazy-server-del yes
    slave-lazy-flush yes

    • lazyfree-lazy-eviction,针对redis内存使用达到maxmeory,并设置有淘汰策略,在被动淘汰键时,是否采用lazy free机制。因为此场景开启lazy free, 可能使用淘汰键的内存释放不及时,导致redis内存超用,超过maxmemory的限制。
    • lazyfree-lazy-expire,针对设置有TTL的键,达到过期后,被redis清理删除时是否采用lazy free机制。此场景建议开启,因TTL本身是自适应调整的速度。
    • lazyfree-lazy-server-del,针对有些指令在处理已存在的键时,会带有一个隐式的DEL键的操作。如rename命令,当目标键已存在,redis会先删除目标键,如果这些目标键是一个big key,那就会引入阻塞删除的性能问题。此参数设置就是解决这类问题,建议可开启。
    • replica-lazy-flush,针对slave进行全量数据同步,slave在加载master的RDB文件前,会运行flushall来清理自己的数据场景,参数设置决定是否采用异常flush机制。如果内存变动不大,建议可开启。可减少全量同步耗时,从而减少主库因输出缓冲区爆涨引起的内存使用增长。
  • AOF持久化

    appendonly yes
    appendfsync no
    no-appendfsync-on-rewrite yes

    • appendfsync,AOF同步频率,no表示将aof_buf缓冲区的所有内容写入到AOF文件,何时同步由操作系统决定。
    • no-appendfsync-on-rewrite,同时执行bgrewriteaof操作和主进程写aof文件的操作时,两者都会操作磁盘,而bgrewriteaof往往会涉及大量磁盘操作,这样就会造成主进程在写aof文件的时候出现阻塞的情形。
      • no-appendfsync-on-rewrite设置为no,是最安全的方式,不会丢失数据,但是要忍受阻塞的问题。
      • no-appendfsync-on-rewrite设置为yes,设置为yes,相当于将appendfsync设置为no,说明并没有执行磁盘操作,只是写入了缓冲区,因此这样并不会造成阻塞,但是如果这个时候redis挂掉,就会丢失数据。在linux的操作系统的默认设置下,最多会丢失30s的数据。

7 Redisson分布式锁

分布式锁主要有以下几个特点:

  • 独占性:同一时刻只有一个线程能够持有锁。
  • 可重入:同一个线程能够重复获取已获得的锁。
  • 超时释放:在获得锁之后限制锁的有效时间,避免资源无法释放而造成死锁。
  • 高可用:有良好的获取锁与释放锁的功能,避免分布式锁失效。

项目上使用


	private void executeMenuSync() {
        try {
            RLock lock = redissonClient.getLock(LockKey.MENU_SYNC);
            // 获取同步锁,并持有 90 秒,锁会自动释放不用手动释放
            if (lock.tryLock(LOCK_WAIT_TIME, LOCK_LEASE_TIME, TimeUnit.SECONDS)) {
                try {
                    LOGGER.info("acquire the synchronization lock and start to perform menu synchronization");
                    menuSyncService.syncMenus();
                    LOGGER.info("menu synchronization end");
                } finally {
                    MENU_SYNC_STATE = Signal.UN_READY;
                    lock.unlock();
                }
            } else {
                LOGGER.info("the synchronization lock is not acquired so the menu synchronization method is not called this time");
            }
        } catch (Exception e) {
            LOGGER.error(e, XauthErrorCode.SYSTEM.SYSTEM_ERROR, "menu synchronization failed");
        }
    }

获取分布式锁tryLock方法源码

tryLock方法主要可以分为四步:

  • tryAcquire尝试获取锁,如果获取到返回true。
  • 获取不到锁说明锁被占用了,订阅解锁消息通知。
  • 收到解锁消息通知,再次尝试获取锁,如果获取不到重复步骤三,直到超过waitTime获取锁失败。
  • 不论是否获取锁成功,取消解锁消息订阅。

参考:www.cnblogs.com/CF1314/p/17…

	
	// 在waitTime时间范围内尝试获取锁,如果获取到锁,则设置锁过期时间leaseTime
    public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
        long time = unit.toMillis(waitTime);
        long current = System.currentTimeMillis();
        long threadId = Thread.currentThread().getId();
		// 第一步:尝试获取锁
        Long ttl = this.tryAcquire(waitTime, leaseTime, unit, threadId);
		// ttl为空说明获取到了锁
        if (ttl == null) {
            return true;
        } else {
			// 判断尝试获取锁是否超过waitTime
            time -= System.currentTimeMillis() - current;
            if (time <= 0L) {
				// 尝试获取锁超过waitTime,获取锁失败
                this.acquireFailed(waitTime, unit, threadId);
                return false;
            } else {
				// 第二步:订阅释放锁消息通知
                current = System.currentTimeMillis();
                CompletableFuture subscribeFuture = this.subscribe(threadId);

                try {
                    subscribeFuture.get(time, TimeUnit.MILLISECONDS);
                } catch (TimeoutException var21) {
                    if (!subscribeFuture.completeExceptionally(new RedisTimeoutException("Unable to acquire subscription lock after " + time + "ms. Try to increase 'subscriptionsPerConnection' and/or 'subscriptionConnectionPoolSize' parameters."))) {
                        subscribeFuture.whenComplete((res, ex) -> {
                            if (ex == null) {
                                this.unsubscribe(res, threadId);
                            }

                        });
                    }

                    this.acquireFailed(waitTime, unit, threadId);
                    return false;
                } catch (ExecutionException var22) {
                    this.acquireFailed(waitTime, unit, threadId);
                    return false;
                }


                try {
					// 如果在剩余等待时间内收到订阅通知,那么会继续计算剩余等待时间(排除掉订阅等待的时间)
                    time -= System.currentTimeMillis() - current;
                    if (time <= 0L) {
						// 无剩余时间返回false
                        this.acquireFailed(waitTime, unit, threadId);
                        boolean var24 = false;
                        return var24;
                    } else {
                        boolean var16;
                        do {
                            long currentTime = System.currentTimeMillis();
							// 如果剩余等待时间依然有剩余,就可以再次尝试获取锁
                            ttl = this.tryAcquire(waitTime, leaseTime, unit, threadId);
                            if (ttl == null) {
								// 如果加锁成功返回true
                                var16 = true;
                                return var16;
                            }
							// 否则不断计算剩余等待时间
                            time -= System.currentTimeMillis() - currentTime;
                            if (time <= 0L) {
                                this.acquireFailed(waitTime, unit, threadId);
                                var16 = false;
                                return var16;
                            }

                            currentTime = System.currentTimeMillis();
                            if (ttl >= 0L && ttl < time) {
                                ((RedissonLockEntry)this.commandExecutor.getNow(subscribeFuture)).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                            } else {
                                ((RedissonLockEntry)this.commandExecutor.getNow(subscribeFuture)).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
                            }

                            time -= System.currentTimeMillis() - currentTime;
                        } while(time > 0L);

                        this.acquireFailed(waitTime, unit, threadId);
                        var16 = false;
                        return var16;
                    }
                } finally {
                    this.unsubscribe((RedissonLockEntry)this.commandExecutor.getNow(subscribeFuture), threadId);
                }
            }
        }
    }

tryAcquire方法点进去,一直到tryAcquireAsync0->tryAcquireAsync->tryLockInnerAsync方法,这里面是尝试获取分布式锁redis lua脚本。变量KEYS[1]是锁key,ARGV[1]是锁过期时间,ARGV[2]是当前线程id。 lua脚本能够保证操作的原子性,这里判断锁是否存在或者是当前线程,锁的次数加 1 并重置有效期。反之无法加锁则返回锁的剩余等待时间。


 	<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        return this.commandExecutor.syncedEval(this.getRawName(), LongCodec.INSTANCE, command, 
		// 判断锁是否存在,或者锁已经存在,判断threadId是否是自己
	    "if ((redis.call('exists', KEYS[1]) == 0) or (redis.call('hexists', KEYS[1], ARGV[2]) == 1)) 
			// 锁次数加1
	    	then redis.call('hincrby', KEYS[1], ARGV[2], 1); 
			// 设置有效期
	    	redis.call('pexpire', KEYS[1], ARGV[1]); 
			// 返回结果
	    	return nil; 
		end;
		// 没获取到锁,返回锁的剩余等待时间
		return redis.call('pttl', KEYS[1]);", 
		Collections.singletonList(this.getRawName()), new Object[]{unit.toMillis(leaseTime), this.getLockName(threadId)});
    }

释放锁

同样使用到了 lua 脚本,如果是自己的线程,重入次数 - 1,当可重入次数为 0 删除锁,否则重置有效期。


 	protected RFuture<Boolean> unlockInnerAsync(long threadId) {
        return this.evalWriteAsync(this.getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, 
		// 判断锁是否是自己持有
		"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0)
			// 不是自己持有,直接返回
			then return nil;
		end; 
		// 是自己持有的锁,重入次数-1
		local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); 
		// 可重入次数是否为0
		if (counter > 0) 
			// 大于0,不能释放锁,设置有效期
			then redis.call('pexpire', KEYS[1], ARGV[2]); 
			return 0;
		else 
			// 等于0,删除锁
			redis.call('del', KEYS[1]);
	 		redis.call(ARGV[4], KEYS[2], ARGV[1]); 
			return 1; 
		end; 
		return nil;", 
		Arrays.asList(this.getRawName(), this.getChannelName()), new Object[]{LockPubSub.UNLOCK_MESSAGE, this.internalLockLeaseTime, this.getLockName(threadId), this.getSubscribeService().getPublishCommand()});
    }

8 lua脚本

优点

  • 简单易学:Lua语言采用简单直观的语法和易于理解的编程模型,使得初学者可以快速上手。
  • 轻量高效:Lua语言的解释器非常轻量级,运行速度快,并且占用系统资源少。
  • 高度可扩展:Lua语言具有嵌入式特性,可以与其他编程语言(如C、C++)进行无缝集成,使得开发者可以轻松扩展语言功能。
  • 可移植性强:Lua语言在多个平台上都具有良好的兼容性,可以在各种操作系统和嵌入式设备上运行。

缺点

  • 缺乏标准库:虽然 Lua 有许多强大的第三方库,但它自带的标准库不够完整,比如不支持正则表达式、XML 解析和数据库操作等。这对于一些需要大量处理字符串或数据操作的应用可能会带来不便。
  • 较小的生态圈和比较小众的用户群体:相对于其他主流编程语言,Lua 的用户群体比较小众,因此在开发过程中可能会缺少相关的支持和资料。此外,开发者需要自行探索适合自己的解决方案。
  • 面向对象支持较弱:尽管Lua支持面向对象编程,但是它没有内置面向对象语法,例如类和继承,这些需要通过元表或第三方库来实现。对于需要大量面向对象编程的项目,这会增加开发复杂度和代码量。

9 其他面试题

  • 热点数据集中失效问题
    • 设置不同的过期时间,在设置缓存过期时间的时候,将过期时间错开,比如在一个基础的时间上加上或者减去一个范围内的随机值。
    • 数据库访问时加互斥锁
  • 百万用户访问的热点key问题如何解决?
    • 负载均衡多个读节点,备份热点Key
  • Redis中上亿个key,如何把固定前缀的key查出来?
    • scan命令