读第二遍了,感觉和几年前读时的收获不一样了。
送上门来当树洞的
独自承担一切
Redis以简洁为美
Redis通信协议是Redis客户端与Redis之间交流的语言,通信协议规定了命令和返回值的格式。
Redis支持两种难住协议,一种是二进制安全的统一请求(unified request protocal),另一种是比较直观的便于在telnet程序中输入的简单协议。
这两种协议只是命令的格式有区别,命令返回值的格式是一样的。
1.修改配置文件
Redis的配置文件默认在/etc/redis.conf,找到如下行:
#requirepass foobared
去掉前面的注释,并修改为所需要的密码:
requirepass xxxxxxxxxxxxxxxxxxx2222222 (其中"xxxxxxxxxxxxxxxxxxx2222222"就是要设置的密码)
2. 重启Redis,让配置生效
客户端每次连接到Redis时都需要发送密码,否则Redis会拒绝执行客户端发来的命令。
redis>AUTH xxxxxxxxxxxxxxxxxxx2222222
OK
之后就可以执行任何命令了。
3. 登录验证
使用密码认证登录,并验证操作权限:
$ ./redis-cli -h 127.0.0.1 -p 6379 -a myPassword 127.0.0.1:6379> config get requirepass 1) "requirepass" 2) "myPassword"
看到类似上面的输出,说明Reids密码认证配置成功。
除了按上面的方式在登录时,使用-a参数输入登录密码外。也可以不指定,在连接后进行验证:
$ ./redis-cli -h 127.0.0.1 -p 6379 127.0.0.1:6379> auth myPassword OK 127.0.0.1:6379> config get requirepass 1) "requirepass" 2) "myPassword" 127.0.0.1:6379>
4. 在命令行客户端配置密码(redis重启前有效)
前面介绍了通过redis.conf配置密码,这种配置方式需要重新启动Redis。也可以通命令行客户端配置密码,这种配置方式不用重新启动Redis。
配置方式如下:
127.0.0.1:6379> config set requirepass newPassword OK 127.0.0.1:6379> config get requirepass 1) "requirepass" 2) "newPassword"
注意:使用命令行客户端配置密码,重启Redis后仍然会使用redis.conf配置文件中的密码。
5. 在Redis集群中使用认证密码
如果Redis服务器,使用了集群。除了在master中配置密码外,也需要在slave中进行相应配置。
在slave的配置文件中找到如下行,去掉注释并修改与master相同的密码即可:
# masterauth master-password
为了提高网站的负载能力,常常需要将一些访问频率较高但是对CPU或IO资源消耗较大的操作的结果缓存起来,并希望让这些缓存过一段时间自动过期。
Redis的命令都是原子操作,但如果有多于1条的命令组成一个业务含义,就有竞态的问题
除了PERSIST命令之外,使用SET或GETSET命令为键赋值也会同时清除键的过期时间,例如:
redis>EXPIRE foo 20
(integer)1
redis>SET foo bar
OK
redis>TTL foo
(integer)-1
使用EXPIRE命令会重新设置键的过期时间
其他只对键值进行操作的命令(如INCR、LPUSH、HSET、ZREM)均不会影响健的过期时间。
SORT是Redis中最强大最复杂的命令之一,如果使用不好很容易成为性能瓶颈。SORT命令的时间复杂度是O(n+mlog(m)),其中n表示要排序的列表(集合或有序集合)中的元素个数,m表示要返回的元素个数。当n较大的时候SORT命令的性能相对较低,并且Redis在排序前会建立一个长度为n的容器来存储待排序的元素,虽然是一个临时的过程,但如果同时进行较多的大数据量排序操作则会严重影响性能。
所以开发中使用SORT命令时需要注意以下几点:
(1)尽可能减少待排序键中元素的数据(使N尽可能小)
(2)使用LIMIT参数只获取需要的数据(使用M尽可能小)
(3)如果要排序的数据数量较大,尽可能使用STORE参数将结果缓存。
排序
SORT命令
BY参数.
如果提供了BY参数,SORT命令将不再依据元素自身的值进行排序,而是对每个元素使用元素的值替换参数键中的第一个“*”并获取其值,然后依据该值对元素排序。
tag:ruby:posts中存放的是userId,post:userId 这个散列中存放的这个用户的信息. post:*->time DESC 表示按post对象中的time字段进行DESC排序
redis>SORT tag:ruby:posts BY post:*->time DESC
GET参数
GET参数不影响排序,它的作用是使SORT命令的返回结果不再是元素自身的值,而是GET参数中指定的键值。GET参数的规则和BY参数一样,GET参数也支持字符串类型和散列类型的键,并使用“*”作为占位符。
要实现在排序后直接返回ID对应的文章标题,可以这样写:
redis>SORT tag:ruby:posts BY post:*->time DESC GET post:*->title
1)"Window 8 app designs"
2) "Uses for cURL"
3)"The Nature of Ruby"
有N个GET参数,每个元素返回的结果就有N行。这时有个问题,如果还需要返回文章ID该怎么办?答案是使用GET #。
eg:
redis>SORT tag:ruby:posts BY post:*->time DESC GET post:*->title GET post:*->time GET #
1)"Windows 8 app design"
2)"1352620100"
3)"12"
4)"Uses for cURL"
5)"1352620177"
6)"6"
STORE参数
默认情况下SORT会直接返回排序结果,如果希望保存排序结果,可以使用STORE参数。如果希望把结果保存到sort.result键中。
redis>SORT tag:ruby:posts BY post:*->time DESC GET post:*->title GET post:*->time GET # STORE sort.result
任务队列
使用任务队列有如下好处:
1、松耦合
生产者和消费者无需知道彼此的实现细节,只需要约定好任务的描述格式,这使得生产者和消费者可以由不同的团队使用不同的编程语言编写。
2、易于扩展
消费者可以有多个,而且可以分布在不同的服务器中。借此可以轻易地降低单台服务器的负载。
使用Redis实现任务队列
使用LPUSH和RPOP命令实现队列的概念。如果要实现任务队列,只需要让生产者将任务使用LPUSH命令加入到某个键中,另一边让消费者不断地使用 RPOP命令从该键中取出任务即可。
BRPOP命令和RPOP命令相似,唯一的区别是当列表中没有元素时BRPOP命令会一直阻塞住连接,直到有新元素加入。
优先级队列
BRPOP命令可以同时接收多个键,其完整的命令格式为
BLPOP key [key ...] timeout,如BLPOP queue:1 queue:2 0
最后的0,表示不限制等待的时间,即如果没有新元素加入列表就会永远阻塞下去。
意义是同时检测多个键,如果所有键都没有元素则阻塞,如果其中有一个键有元素则会从该键中弹出元素。
如果多个键都有元素则按照从左到右的顺序取第一个键中的一个元素。我们先在queue:2和queue:3中各加入一个元素:
redis>LPUSH queue:2 task2
1)(integer)1
redis>LPUSH queue:3 task3
1)(integer)1
然后执行BRPOP命令:
redis>BRPOP queue:1 queue:2 queue:3 0
1)"queue:2"
2)"task2"
借此特性可以实现区分优先级的任务队列。
发布/订阅 模式
除了实现任务队列外,Redis还提供了一组命令可以让开发者实现“发布/订阅”(publish/subscribe)模式。“发布/订阅”模式同样可以实现进程间的消息传递,
其原理是这样的:
“发布/订阅”模式中包含两种角色,分别是发布者和订阅者。订阅者可以订阅一个或若干个频道(channel),而发布者可以向指定的频道发送消息,所有订阅此频道的订阅者都会收到此消息。
管道
客户端和Redis使用TCP协议连接。不论是客户端向Redis发送命令还是Redis向客户端返回命令的执行结果,都需要经过网络传输,这两个部分的总耗时称为往返时延(round-trip delay time)。根据网络性能不同,往返时延也不同,大致来说到本地回环地址(look back address)的往返时延在数量级上相当于Redis处理一条简单命令(如LPUSH list 1 2 3)的时间。如果执行较多的命令,每个命令的往返时延累加起来对性能还是有一定影响的。
在执行多个命令时,每条命令都需要等待上一条命令执行完(即收到Redis的返回结果)才能执行,即使命令不需要上一条命令的执行结果。
Redis的底层通信协议对管道(pipelining)提供了支持。
通过管道可以一次性发送多条命令并在执行完后一次性将结果返回,当一组命令中每条命令不依赖于之前命令的执行结果时,
就可以将这组命令一起通过管道发出。管道通过减少客户端与Redis的通信次数来实现降低往返时延累计值的目的。
内存是新的硬盘,硬盘是新的磁带。
Redis是基于内存的数据库,所有的数据都存储在内存中,所以如何优化存储,减少内存空间占用对成本控制来说是一个非常重要的话题。
精简键名和键值
精简键名和键值是最直观的减少内存占用的方式
内部编码优化
实现访问频率限制
可以使用List来存储访问的IP列表,可以限制最近1分钟连接访问10次的IP
$listLength=LLEN rate.limiting.$IP
if $listLength<10
LPUSH rate.limiting:$IP,now()
else
$time =LINDEX rate.limiting.$IP,-1
if now()-$time<60
print 访问频率超过了限制,请稍后再试
else
LPUSH rate.limiting.$IP,now()
LTRIM rate.limiting.$IP,0,9
Redis事务
Redis中的事务(transaction)是一组命令的集合。事务同命令一样都是Redis的最小执行单位,一个事务中的命令要么都执行,要么都不执行。
redis>MULTI
OK
redis>SADD "user:1:following" 2 //user1关注的人新增user2
QUEUED
redis>SADD “user:2:followers” 1 //user2的粉丝增加一个user11
QUEUED
redis>EXEC
1)(integer)1
2)(integer)2
当把所有要在同一个事务中执行的命令都发送给Redis后,我们使用EXEC命令告诉Redis将等待执行的事务队列中的所有命令(即刚才所有返回QUEUED的命令)按照发送顺序依次执行。EXEC命令的返回值就是这样命令的返回值组成的列表,返回值顺序和命令的顺序相同。
Redis保证一个事务中的所有命令要么都执行,要么都不执行。如果在发送EXEC命令前客户端断线了,则Redis会清空事务队列,事务中的所有命令都不会执行。
而一旦客户端发送EXEC命令,所有的命令就都会被执行,即使此后客户端断线也没有关系,因为Redis中已经记录了所有要执行的命令。
除此之外,Redis的事务还要能保证一个事务内的命令依次执行而不被其它命令插入。
有序集合类型(sorted set)在某些方面和列表类型有些相似:
(1)二者都有有序的
(2)二者都可以获得某一范围的元素
但是二者有关很大的区别,这使得它们的应用场景也是不同的
(1)列表类型是通过双向链表(double linked list)实现的,获取靠近两端的数据速度极快,而当元素增多后,访问中间数据的速度会较慢,所以它更加适合实现如“新鲜事”或“日志”这样很少访问中间元素的应用
(2)有序集合类型是使用散列表和跳跃表(Skip list)实现的,所以即使读取位于中间部分的数据速度也很快(时间复杂度是O(log(N)))
(3)列表中不能简单地调整某个元素的位置,但有序集合可以(通过更改这个元素的分数)
(4)有序集合要比列表类型更耗费内存
有序集合算得上是Redis的5种数据类型中最高级的类型
在集合中的每个元素都是不同的,且没有顺序。
集合类型的常用操作是向集合中加入或删除元素、判断某个元素是否存在等,由于集合类型在Redis内部是使用值为空的散列表(hash table)实现的,所以这些操作的时间复杂度都是O(1).
最方便的是多个集合类型键之间还可以进行并集、交集和差集运算。
散列类型没有类似字符串类型的MGET命令那样可以通过一条命令同时获得多个键的键值的版本,所以对于每个文章ID都需要请求一次数据库,也就都会产生一次往返时延(round-trip delay time),这个问题可以通过管道和脚本来优化这个问题。
另外使用列表类型存储文章ID有以下两个 问题:
(1)文章的发布时间不易修改:修改文章的发布时间不仅要修改post:文章ID 中的time字段,还需要按照实际的发布时间重新排列post:list中的元素顺序,而这一操作晒后修复比较繁琐。【使用有序集合 将time保存为score是否就可以了呢】
(2)当文章数量较多时访问中间的页面性能较差:列表类型是通过双向链表(double linked list)实现的,所以当列表元素非常多时访问中间的元素效率并不高。
使用列表类型键 post:文章ID:comments来存储某个文章的所有评论。
LLEN命令的功能类似SQL语句SELECT COUNT(*) FROM table_name,但是LLEN的时间复杂度为O(1),使用Redis会直接取现成的值,而不需要像部分关系型数据库(如使用InnoDB存储引擎的MySQL表)那样需要遍历一遍数据表来统计条目数量。
使用链接的代价就是通过索引访问元素比较慢。
列表类型能非常快速地完成关系数据库难以应付的场景:如社交网站的新鲜事,由于中关心的只是最新的内容,使用列表类型存储,即使新鲜事的总数达到几千万个,获取其中最新的100条数据也是很快的。
同样因为在两端插入记录的时间复杂度是O(1),列表类型也适合用来记录日志,可以保证加入新日志的速度不受已有日志数量的影响。
DEL命令的参数不支持匹配符,但我们可以结合Linux的管道和xargs命令自己实现删除所有符合规则的键。
比如要删除所以以“user:”开头的键,就可以执行redis-cli KEYS "user:*"|xargs redis-cli DEL
另外由于DEL命令支持多个键作为参数,所以还可以执行redis-cli DEL `redis-cli KEYS "user:*"`来达到同样的效果,并且性能更好。
TYPE key
TYPE命令用来获取键值的数据类型,返回值可能是string(字符串类型)、hash(散列类型)、list(列表类型)、set(集合类型)、zset(有序集合类型)
一个字符串类型键允许存储的数据的最大容量是512MB
Redis命令不区分大小写
KEYS命令需要遍历Redis中的所有键,当键的数据较多时会影响性能,不建议在生产环境中使用。
获取符合规则的键名列表:KEYS pattern
pattern支持glob风格通配符格式
? 匹配一个字符
* 匹配任意个(包括0个)字符
[] 匹配括号间的任一字符,可以使用“-”符号表示一个范围,如a[b-d]可以匹配"ab"、“ac”、“ad”
x 匹配字符串x,用于转义符号,如果匹配“?”就需要使用?
判断一个键是否存在值
EXISTS key
一个客户端要么可以访问Redis全部数据库,要么连一个数据库也没有权限访问。最重要的一点是多个数据库之间并不是完全隔离的,比如FLUSHALL命令就可以清空一个Redis实例中所有数据库中的数据。
从某个角度看,一个Redis实例中的这些数据库更像是一个命名空间,而不适宜存储不同应用程序的数据。
Redis非常轻量级,一个空Redis实例占用的内存只有1MB左右,所以不用担心多个Redis实例会额外占用很多内存。
Redis实例中的数据库都是以一个从0开始的递增数字命名,Redis默认支持16个数据库。可以通过配置参数databases来修改这一数字。
客户端与Redis建立连接后会自动选择0号数据库。不过可以随时使用SELECT命令更换数据库。
$redis-server /path/to/redis.conf
$redis-server /path/to/redis.conf --loglevel warning
Reddis提供了一个配置文件的模板redis.conf,位于源代码目录的根目录中。
除此之外还可以在Redis运行时通过CONFIG SET命令在不重新启动Redis的情况下动态修改部分Redis配置。
redis>CONFIG SET loglevel warning
并不是所有的配置都可以使用CONFIG SET命令修改。
同样在运行的时候也可以使用CONFIG GET命令获得Redis当前的配置情况。
redis>CONFIG GET loglevel
1)“loglevel”
2)"warning"
Redis可以妥善处理SIGTERM信号,所以使用kill Redis进行的PID,也可以正常结束Redis,效果与发送SHUTDOWN命令一样。
Redis-cli Redis Command Line Interface
$redis-cli -h 127.0.0.1 -p 6379
Redis REmote DIctionary Server(远程字典服务器),以字典结构存储数据,并允许其它应用通过TCP协议读写字典中的内容。
TTL Time To Live 生存时间
https://gist.github.com/348262
Installation
Download, extract and compile Redis with:
$ wget http://download.redis.io/releases/redis-4.0.8.tar.gz $ tar xzf redis-4.0.8.tar.gz $ cd redis-4.0.8 $ make
The binaries that are now compiled are available in the src directory. Run Redis with:
$ src/redis-server
You can interact with Redis using the built-in client:
$ src/redis-cli redis> set foo bar OK redis> get foo "bar"
编译后执行make install命令,这些程序会被复制到/usr/local/bin目录内,所以在命令行中直接输入程序名称即可执行。
redis-server Redis服务器
redis-cli Redis命令客户端
redis-benchmark Redis性能测试工具
redis-check-aof AOF文件修复工具
redis-check-dujp RDB文件检查工具
redis-sentinel Sentinel服务器(仅在2.8版以后)
Redis服务器默认会使用6379端口。使用--port参数可以自定义端口号:
$redis-server --port 6800
以Ubuntu和Debian发行版为例,
Redis源代码目录的utils文件夹中有一个名为redis_init_script初始化脚本文件,内容如下:
#!/bin/sh # # Simple Redis init.d script conceived to work on Linux systems # as it does use of the /proc filesystem. REDISPORT=6379 EXEC=/usr/local/bin/redis-server CLIEXEC=/usr/local/bin/redis-cli PIDFILE=/var/run/redis_${REDISPORT}.pid CONF="/etc/redis/${REDISPORT}.conf" case "$1" in start) if [ -f $PIDFILE ] then echo "$PIDFILE exists, process is already running or crashed" else echo "Starting Redis server..." $EXEC $CONF fi ;; stop) if [ ! -f $PIDFILE ] then echo "$PIDFILE does not exist, process is not running" else PID=$(cat $PIDFILE) echo "Stopping ..." $CLIEXEC -p $REDISPORT shutdown while [ -x /proc/${PID} ] do echo "Waiting for Redis to shutdown ..." sleep 1 done echo "Redis stopped" fi ;; *) echo "Please use start or stop as first argument" ;; esac
结合上面的配置文件,需要配置Redis的运行方式和持久化文件、日志文件的存储位置,具体步骤如下:
(1)配置初始化脚本。首先将初始化脚本复制到/etc/init.d目录中,文件名为redis_端口号 ,其中端口号表示要让redis监听的端口号,客户端通过该端口连接Redis。
然后修改脚本第6行的REDISPORT变量的值为同样的端口号。
(2)建立需要的文件夹。
/etc/redis 存储Redis的配置文件
/var/redis/端口号 存放Redis的持久化文件
(3)修改配置文件。首先将配置文件模板复制到/etc/redis目录中,以端口号命令(如 "6379.conf"),然后对其中的部分参数进行编辑。
daemonize yes 使用Redis以守护进程模式运行
pidfile /var/run/redis_端口号.pid 设置Redis的PID文件位置
port 端口号 设置Redis监听的端口号
dir /var/redis/端口号 设置持久化文件存放位置
至此,可以使用/etc/init.d/redis_端口号start来启动Redis。而后需要执行下面的命令使Redis随系统自动启动:
$sudo updsate-rc.d redis_端口号 defaults
补充Redis事务在RedisTemplate中的使用:
5.10. Redis Transactions
Redis provides support for transactions through the multi, exec, and discard commands. These operations are available on RedisTemplate, however RedisTemplate is not guaranteed to execute all operations in the transaction using the same connection.
Spring Data Redis provides the SessionCallback interface for use when multiple operations need to be performed with the same connection, as when using Redis transactions. For example:
//execute a transaction List<Object> txResults = redisTemplate.execute(new SessionCallback<List<Object>>() { public List<Object> execute(RedisOperations operations) throws DataAccessException { operations.multi(); operations.opsForSet().add("key", "value1"); // This will contain the results of all ops in the transaction return operations.exec(); } }); System.out.println("Number of items added to set: " + txResults.get(0));
RedisTemplate will use its value, hash key, and hash value serializers to deserialize all results of exec before returning. There is an additional exec method that allows you to pass a custom serializer for transaction results.
5.10.1. @Transactional Support
Transaction Support is disabled by default and has to be explicitly enabled for each RedisTemplate in use by setting setEnableTransactionSupport(true). This will force binding the RedisConnection in use to the current Thread triggering MULTI. If the transaction finishes without errors, EXEC is called, otherwise DISCARD. Once in MULTI, RedisConnection would queue write operations, all readonly operations, such as KEYS are piped to a fresh (non thread bound) RedisConnection.
/** Sample Configuration **/ @Configuration public class RedisTxContextConfiguration { @Bean public StringRedisTemplate redisTemplate() { StringRedisTemplate template = new StringRedisTemplate(redisConnectionFactory()); // explicitly enable transaction support template.setEnableTransactionSupport(true); return template; } @Bean public PlatformTransactionManager transactionManager() throws SQLException { return new DataSourceTransactionManager(dataSource()); } @Bean public RedisConnectionFactory redisConnectionFactory( // jedis || lettuce); @Bean public DataSource dataSource() throws SQLException { // ... } }
/** Usage Constrainsts **/ // executed on thread bound connection template.opsForValue().set("foo", "bar"); // read operation executed on a free (not tx-aware) connection template.keys("*"); // returns null as values set within transaction are not visible template.opsForValue().get("foo");
5.11. Pipelining
Redis provides support for pipelining, which involves sending multiple commands to the server without waiting for the replies and then reading the replies in a single step. Pipelining can improve performance when you need to send several commands in a row, such as adding many elements to the same List.
Spring Data Redis provides several RedisTemplate methods for executing commands in a pipeline. If you don’t care about the results of the pipelined operations, you can use the standard execute method, passing true for the pipeline argument. The executePipelined methods will execute the provided RedisCallback or SessionCallback in a pipeline and return the results. For example:
//pop a specified number of items from a queue List<Object> results = stringRedisTemplate.executePipelined( new RedisCallback<Object>() { public Object doInRedis(RedisConnection connection) throws DataAccessException { StringRedisConnection stringRedisConn = (StringRedisConnection)connection; for(int i=0; i< batchSize; i++) { stringRedisConn.rPop("myqueue"); } return null; } });
The example above executes a bulk right pop of items from a queue in a pipeline. The results List contains all of the popped items. RedisTemplate uses its value, hash key, and hash value serializers to deserialize all results before returning, so the returned items in the above example will be Strings. There are additional executePipelined methods that allow you to pass a custom serializer for pipelined results.
Note that the value returned from the RedisCallback is required to be null, as this value is discarded in favor of returning the results of the pipelined commands.
https://docs.spring.io/spring-data/redis/docs/2.0.5.RELEASE/reference/html/#tx
SET key value [EX seconds] [PX milliseconds] [NX|XX]
Available since 1.0.0.
Time complexity: O(1)
Set key to hold the string value. If key already holds a value, it is overwritten, regardless of its type. Any previous time to live associated with the key is discarded on successful SET operation.
Options
Starting with Redis 2.6.12 SET supports a set of options that modify its behavior:
EX seconds -- Set the specified expire time, in seconds.
PX milliseconds -- Set the specified expire time, in milliseconds.
NX -- Only set the key if it does not already exist.
XX -- Only set the key if it already exist.
Note: Since the SET command options can replace SETNX, SETEX, PSETEX, it is possible that in future versions of Redis these three commands will be deprecated and finally removed.
Return value
Simple string reply: OK if SET was executed correctly. Null reply: a Null Bulk Reply is returned if the SET operation was not performed because the user specified the NX or XX option but the condition was not met.
Examples
redis> SET mykey "Hello" "OK" redis> GET mykey "Hello" redis>
Patterns
Note: The following pattern is discouraged in favor of the Redlock algorithm which is only a bit more complex to implement, but offers better guarantees and is fault tolerant.
The command SET resource-name anystring NX EX max-lock-time is a simple way to implement a locking system with Redis.
A client can acquire the lock if the above command returns OK (or retry after some time if the command returns Nil), and remove the lock just using DEL.
The lock will be auto-released after the expire time is reached.
It is possible to make this system more robust modifying the unlock schema as follows:
Instead of setting a fixed string, set a non-guessable large random string, called token.
Instead of releasing the lock with DEL, send a script that only removes the key if the value matches.
This avoids that a client will try to release the lock after the expire time deleting the key created by another client that acquired the lock later.
An example of unlock script would be similar to the following:
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
The script should be called with EVAL ...script... 1 resource-name token-value
https://redis.io/commands/set
SETEX key seconds value
Available since 2.0.0.
Time complexity: O(1)
Set key to hold the string value and set key to timeout after a given number of seconds. This command is equivalent to executing the following commands:
SET mykey value
EXPIRE mykey seconds
SETEX is atomic, and can be reproduced by using the previous two commands inside an MULTI / EXEC block. It is provided as a faster alternative to the given sequence of operations, because this operation is very common when Redis is used as a cache.
An error is returned when seconds is invalid.
Return value
Simple string reply
Examples
redis> SETEX mykey 10 "Hello" "OK" redis> TTL mykey (integer) 10 redis> GET mykey "Hello" redis>
https://redis.io/commands/setex
SETNX key value
Available since 1.0.0.
Time complexity: O(1)
Set key to hold string value if key does not exist. In that case, it is equal to SET. When key already holds a value, no operation is performed.
SETNX is short for "SET if Not eXists".
Return value
Integer reply, specifically:
1 if the key was set
0 if the key was not set
Examples
redis> SETNX mykey "Hello" (integer) 1 redis> SETNX mykey "World" (integer) 0 redis> GET mykey "Hello" redis>
Design pattern: Locking with SETNX
Please note that:
The following pattern is discouraged in favor of the Redlock algorithm which is only a bit more complex to implement, but offers better guarantees and is fault tolerant.
We document the old pattern anyway because certain existing implementations link to this page as a reference. Moreover it is an interesting example of how Redis commands can be used in order to mount programming primitives.
Anyway even assuming a single-instance locking primitive, starting with 2.6.12 it is possible to create a much simpler locking primitive, equivalent to the one discussed here, using the SET command to acquire the lock, and a simple Lua script to release the lock. The pattern is documented in the SET command page.
That said, SETNX can be used, and was historically used, as a locking primitive. For example, to acquire the lock of the key foo, the client could try the following:
SETNX lock.foo <current Unix time + lock timeout + 1>
If SETNX returns 1 the client acquired the lock, setting the lock.foo key to the Unix time at which the lock should no longer be considered valid. The client will later use DEL lock.foo in order to release the lock.
If SETNX returns 0 the key is already locked by some other client. We can either return to the caller if it's a non blocking lock, or enter a loop retrying to hold the lock until we succeed or some kind of timeout expires.
Handling deadlocks
In the above locking algorithm there is a problem: what happens if a client fails, crashes, or is otherwise not able to release the lock? It's possible to detect this condition because the lock key contains a UNIX timestamp. If such a timestamp is equal to the current Unix time the lock is no longer valid.
When this happens we can't just call DEL against the key to remove the lock and then try to issue a SETNX, as there is a race condition here, when multiple clients detected an expired lock and are trying to release it.
C1 and C2 read lock.foo to check the timestamp, because they both received 0 after executing SETNX, as the lock is still held by C3 that crashed after holding the lock.
C1 sends DEL lock.foo
C1 sends SETNX lock.foo and it succeeds
C2 sends DEL lock.foo
C2 sends SETNX lock.foo and it succeeds
ERROR: both C1 and C2 acquired the lock because of the race condition.
Fortunately, it's possible to avoid this issue using the following algorithm. Let's see how C4, our sane client, uses the good algorithm:
C4 sends SETNX lock.foo in order to acquire the lock
The crashed client C3 still holds it, so Redis will reply with 0 to C4.
C4 sends GET lock.foo to check if the lock expired. If it is not, it will sleep for some time and retry from the start.
Instead, if the lock is expired because the Unix time at lock.foo is older than the current Unix time, C4 tries to perform:
GETSET lock.foo <current Unix timestamp + lock timeout + 1>
Because of the GETSET semantic, C4 can check if the old value stored at key is still an expired timestamp. If it is, the lock was acquired.
If another client, for instance C5, was faster than C4 and acquired the lock with the GETSET operation, the C4 GETSET operation will return a non expired timestamp. C4 will simply restart from the first step. Note that even if C4 set the key a bit a few seconds in the future this is not a problem.
In order to make this locking algorithm more robust, a client holding a lock should always check the timeout didn't expire before unlocking the key with DEL because client failures can be complex, not just crashing but also blocking a lot of time against some operations and trying to issue DEL after a lot of time (when the LOCK is already held by another client).
https://redis.io/commands/setnx
Redis key命名规范:
(1)key要包括数据类型、版本号。使用class:classId:product.id的来描述classId所对应的ProductId
(2)key使用常量定义在相关业务类的内部。好处:可以很容易知道这个key的使用范围
(3)每个往缓存中存放数据的接口,都要提供一个删除这个业务缓存的接口
(4)提供一个通用的删除缓存的接口
一个统一存放缓存key的工具,可以使用层次结构列表,显示这个缓存使用的key,方便测试或排查问题