# 字符串使用和内部原理

# 使用场景

redis中的字符串常用于存储key value数据或计数数据。

# key value字符串存储

在平时的需求中存在大量的存储key value数据的场景,比如存储一个用户的用户名,我们把用户id作为key,用户名字符串作为value值; 如果是复杂的对象,则可以通过json等方式序列化成字符串存储,比如一篇文章的文章标题、内容、创建时间等信息作为一个对象序列化成json存储。

常用命令

命令 命令说明 时间复杂度
get key 获取可以对应的value值,如果没有返回null O(1)
set key value [过期时间/KEEPTTL] [NX/XX] [GET] 设置key对应的value值, 可以传入过期时间,过期时间可以是相对时间或绝对时间。如果没有传过期时间,则set方法会覆盖实现的过期时间设置即把key变成不过期的,可以传入KEEPTTL避免保存之前的过期时间,如果传入NX标记,则只有当对应的key不存在的时候才会设置值。如果传入XX标记,则只有在key存在的时候才会设置值。如果set操作成功,返回OK字符串,否则返回空(比如因为NX或XX导致没有设置值)。如果传入GET,则会返回key对应的旧的值。 O(1)
mget 批量获取一批key的value O(N) N是key的数量
mset 批量设置一批key的value O(N) N是key的数量
setnx 相当于前面的set key value NX的简化版本,区别是setnx操作成功返回1否则返回0 O(1)
127.0.0.1:6379> set hello world
OK
(4.37s)
127.0.0.1:6379> get hello
"world"
1
2
3
4
5

# 实现分布式锁

通过redis可以快速实现一个简单的分布式锁。给要锁的资源生成key(比如userId对应userId的字符串),然后再生成一个随机字符串做为value。 然后尝试通过set带有nx标记以及过期时间写入key value值,过期时间目的是为了防止宕机等异常导致锁一直不释放。 如果成功,说明获取到了这个分布式锁,然后在锁内执行业务逻辑,执行完成后(finally模块中),删除对应的key value(删除之前需要再get获取判断 value值是否还是前面生成的随机数,防止持有锁超时后误删其他客户端的加锁,这个操作分为了两步所以尽量放到一个lua script保证原子删除)。

distributed-locks (opens new window)

# 计数

字符串类型也可以作为数值来存储(可以存储整数或浮点数),由于redis使用单线程处理用户请求,有原子操作保证并发使用线程安全,可以类比成Java里的AtomicLong。

命令 命令说明 时间复杂度
incr key 给指定的key的值加1,如果对应的key不存在,则会先初始化为0再加一 O(1)
decr key 给指定的key的值减1,如果对应的key不存在,则会先初始化为0再减一 O(1)
incrby key 给指定的key增加指定的值,如果对应的key不存在,则会先初始化为0再加 O(1)
decrby key 给指定的key减去指定的值,如果对应的key不存在,则会先初始化为0再减 O(1)

通过计数功能能够实现非常多的计数场景需求,比如评论数、粉丝数等等。除此之外分布式的计数器还有很多业务架构上的应用场景比如实现分布式限流等。

# 实现分布式计数

实现分布式计数的注意事项

在实现分布式计数时,我们需要分析使用场景,评估请求量、请求数据热点情况、读写请求比例、是否对计数一致性要求高,来做出更合适的取舍。

比如有的场景对计数数据一致性要求不高,比如展示某个活动的参数人数,我们可以在读取计数时增加本地缓存配合本地缓存过期时间/刷新时间,减少 redis的请求压力;如果写入量比较大,则对写入请求在本地进行聚合(类似kafka的内存buffer),每隔一定时间异步写入redis; 如果对一致性要求比较高,比如使用redis存储一件商品的库存数量,则在扣减库存时,可以先读取本地缓存判断有库存后,再调用decr尝试扣减库存,decr结果>=0 说明扣减成功否则失败。

更多的redis使用技巧可以参考高并发应用缓存设计与实现这篇文章。

# 实现分布式限流、分布式计数限制

在系统稳定性保护中,限流是一个重要的功能,然而现有的大部分限流都是进程内的限流,比如各种ratelimiter。 加入我们现在的服务会调用数据库,数据库能够支持1w qps的写流量,那么我们需要限制全局的写入qps小于1w,这就需要实现分布式限流。 通过redis的计数即可实现,我们分析可知这个限流限制不需要保证严格的一致性,所以可以使用缓存、buffer等策略降低redis的调用量。 比如我们把每一秒作为一个key来统计调用次数,本地缓存每100ms从redis中读取当前秒调用次数到本地,本地的调用量也按照每100ms一次聚合写入到redis 中,如果调用次数超过阈值,说明超限不能继续请求抛出异常。

上述的间隔、key表示的时间范围都可以按照实际情况进行调整。

另外在业务需求中,还有一类比较常见的计数限制,比如限制一个用户一天只能发表N个文章,则我们可以把用户和日期作为key,用户发表文章调用incr判断是否超过N 如果没有超过继续执行否则返回超过阈值异常。如果要考虑异常情况比如发表过程中因为某种原因失败了,我们还可以通过decr回退计数。

# 字符串实现

# 编码

编码选择

int类型: 如果在LONG_MIN和LONG_MAX之间,使用OBJ_ENCODING_INT编码。否则使用OBJ_ENCODING_RAW编码(long转换成字符串表示)。 string类型: 长度小于44字符时,使用OBJ_ENCODING_EMBSTR编码,长度大于等于55字符时使用OBJ_ENCODING_RAW编码

OBJ_ENCODING_EMBSTR和OBJ_ENCODING_RAW有什么区别?

# 空间预分配

当字符串长度增加后,redis会增加对应字符串的空间,同时为了避免拼接字符串命令时频繁申请新的空间的开销,redis会通过预分配的策略提前多预留一些空间。 预留需要和内存占用进行取舍。

预分配的优化思想在计算机中非常常用,比如Java中ArrayList、HashMap等扩容时会进行进行充分的扩容而不是只增加到需要的大小。

# 总结