前导
在了解具体的数据结构类型之前,我们有必要了解下Redis提供的操作key的全局命令、 数据结构和内部编码、 单线程命令处理机制,都有助于加深对Redis的理解。
全局命令
Redis 是一个Key-Value内存数据库,不管是何种数据结构,对于键来说有一些通用的命令。简单句几个例子
查看所有键 keys *
keys *
命令会将所有的键输出
127.0.0.1:6379> set key1 artisan OK 127.0.0.1:6379> set key2 artisan2 OK 127.0.0.1:6379> set key3 artisan3 OK 127.0.0.1:6379> keys * 1) "key3" 2) "key2" 3) "key1" 127.0.0.1:6379>
keys命令会遍历所有键, 所以它的时间复杂度是O(n) , 当Redis保存了大量键时, 线上环境 禁止使用
键总数dbsize
dbsize命令会返回当前数据库中键的总数
127.0.0.1:6379> rpush artisanlist a d d d x we sdsd dsd sddfdg dsfdf sdfsdfsdf sdfsfdf ---- 返回增加的list的个数 (integer) 12 127.0.0.1:6379> dbsize --加上刚才设置的3个,一共4个key (integer) 4 127.0.0.1:6379> keys * 1) "artisanlist" 2) "key3" 3) "key2" 4) "key1" 127.0.0.1:6379>
dbsize命令在计算键总数时不会遍历所有键, 而是直接获取Redis内置的键总数变量, 所以dbsize命令的时间复杂度是O(1)
检查键是否存在 exists key
127.0.0.1:6379> exists artisanlist (integer) 1 127.0.0.1:6379> exists sdsdsd (integer) 0 127.0.0.1:6379>
如果键存在则返回1, 不存在则返回0
删除键del key
127.0.0.1:6379> del key1 (integer) 1 127.0.0.1:6379> del xdsdse (integer) 0 127.0.0.1:6379>
del是一个通用命令, 无论值是什么数据结构类型, del命令都可以将其删除
27.0.0.1:6379> exists artisanlist (integer) 1 127.0.0.1:6379> del artisanlist (integer) 1 127.0.0.1:6379> exists artisanlist (integer) 0 127.0.0.1:6379>
返回结果为成功删除键的个数, 假设删除一个不存在的键, 返回0,如上。
同时del命令可以支持删除多个键
127.0.0.1:6379> set x 123 OK 127.0.0.1:6379> set y 456 OK 127.0.0.1:6379> set z 789 OK 127.0.0.1:6379> del x y z (integer) 3 127.0.0.1:6379>
键过期expire key seconds
Redis支持对键添加过期时间, 当超过过期时间后, 会自动删除键
ttl命令会返回键的剩余过期时间 ,它有三种返回值
- 大于等于0的整数: 键剩余的过期时间。
- -1: 键没设置过期时间。
- -2: 键不存在
127.0.0.1:6379> set name artisan OK 127.0.0.1:6379> expire name 20 (integer) 1 127.0.0.1:6379> ttl name (integer) 16 ..... 127.0.0.1:6379> ttl name (integer) 1 127.0.0.1:6379> ttl name -- -2说明不存在 (integer) -2 127.0.0.1:6379> get name --- get为空 (nil) 127.0.0.1:6379>
上面的 set name artisan expire name 20
两句 可以简化成 set name artisan EX 20
语法如下
set key value [expiration EX seconds|PX milliseconds] [NX|XX
127.0.0.1:6379> set name artisan EX 20 OK 127.0.0.1:6379> ttl name (integer) 16 127.0.0.1:6379> get name "artisan" ....... 127.0.0.1:6379> ttl name (integer) 1 127.0.0.1:6379> ttl name (integer) -2 127.0.0.1:6379>
键的数据结构类型type key
键name 是字符串类型, 返回结果为string,
键artisanlist是列表类型, 返回结果为list,
键user是列表hash, 返回结果为hash
127.0.0.1:6379> set name artisan OK 127.0.0.1:6379> rpush artisanlist asds sddfd sdfdsf dfdfsfd sdfsdfsd f (integer) 6 127.0.0.1:6379> hset user name artisan2 age 20 country china sex male (integer) 4 127.0.0.1:6379> type name string 127.0.0.1:6379> type artisanlist list 127.0.0.1:6379> type user hash 127.0.0.1:6379>
数据结构和内部编码
type命令实际返回的就是当前键的数据结构类型: string(字符串) 、 hash(哈希) 、 list(列表) 、 set(集合) 、 zset(有序集合) , 其实这些只是Redis对外的数据结构。 事实上,Redis内部每种数据结构都有自己底层的内部编码实现, 而且是多种实现,Redis会在合适的场景选择合适的内部编码。
可以通过object encoding
命令查询内部编码
127.0.0.1:6379> set name artisan OK 127.0.0.1:6379> LPUSH artisanlist c b a (integer) 9 127.0.0.1:6379> hset user name artisan age 20 sex male (integer) 0 127.0.0.1:6379> sadd myset a b c (integer) 3 127.0.0.1:6379> zadd myzset 90 artisan (integer) 1 127.0.0.1:6379> object encoding name "embstr" 127.0.0.1:6379> object encoding user "ziplist" 127.0.0.1:6379> object encoding myset "hashtable" 127.0.0.1:6379> object encoding myzset "ziplist" 127.0.0.1:6379>
单线程架构
Redis使用了单线程架构和I/O多路复用模型来实现高性能的内存数据库服务。
为什么Redis使用单线程模型会达到每秒万级别的处理能力呢?
纯内存访问, Redis将所有数据放在内存中, 内存的响应时长大约为100纳秒, 这是Redis达到每秒万级别访问的重要基础
非阻塞I/O, Redis使用epoll作为I/O多路复用技术的实现, 再加上Redis自身的事件处理模型将epoll中的连接、 读写、 关闭都转换为事件, 不在网络I/O上浪费过多的时间
单线程避免了线程切换和竞态产生的消耗。
string概述
字符串是 Redis 最基本的数据结构 ,它将以一个键和一个值存储于 Redis 内部,很像Java 的 Map 结构 ,让 Redis 通过键去找到值。
Redis 会通过 key 去找到对应的字符串 ,比如通过 keyl 找到 valuel。假设产品的编号为 0001 , 只要设置 key 为 product_0001 , 就可以通过 product_0001去保存该产品到 Redis 中,也可以通过 product_0001 从 redis 中找到产品信息。
字符串类型的值实际可以 是字符串(简单的字符串、 复杂的字符串(例如JSON、 XML) ) 、 数字 (整数、 浮点数) , 甚至是二进制(图片、 音频、 视频) , 但是值最大不能 超过512MB。
基本命令
官网 : https://redis.io/commands#string
中文网站: http://www.redis.cn/commands.html#string
这里仅仅列出常用的一些,其他详见上述官网
set key value [ex seconds] [px milliseconds] [nx|xx]
- ex seconds: 为键设置秒级过期时间。
- px milliseconds: 为键设置毫秒级过期时间。
- nx: 键必须不存在, 才可以设置成功, 用于添加。
- xx: 与nx相反, 键必须存在, 才可以设置成功, 用于更新
除了set选项, Redis还提供了setex和setnx两个命令
设置过期时间
setex key seconds value
setnx当前键不存在,允许设置
setnx key value
127.0.0.1:6379> set key1 value1 OK 127.0.0.1:6379> setnx key1 value2 -- 因为存在,所以设置key1 失败,返回0 (integer) 0 127.0.0.1:6379> setex key1 100 vlaue3 -- 设置过期时间 100秒 OK 127.0.0.1:6379> get key1 "vlaue3" 127.0.0.1:6379> ttl key1 (integer) 93 127.0.0.1:6379> ttl key1 (integer) 88
setnx和setxx在实际使用中有什么应用场景吗? 以setnx命令为例子, 由于Redis的单线程命令处理机制, 如果有多个客户端同时执行setnx key value,根据setnx的特性只有一个客户端能设置成功, setnx可以作为分布式锁的一种实现方案。
批量设置值
mset key value [key value ...]
127.0.0.1:6379> mset key1 a key2 b key3 c key4 d key5 5 OK
批量获取值
mget key [key ...]
127.0.0.1:6379> mget key1 key2 key3 key4 key5 1) "a" 2) "b" 3) "c" 4) "d" 5) "5"
如果有些键不存在, 那么它的值为nil(空) , 结果是按照传入键的顺序返回:
127.0.0.1:6379> mget key1 key2 xxx key3 1) "a" 2) "b" 3) (nil) 4) "c" 127.0.0.1:6379>
批量操作命令可以有效提高效率,假如没有mget这样的命令, 要执行n次get命令如下所示
n次get时间 = n次网络时间 + n次命令时间
使用mget命令后, 要执行n次get命令操作如下
n次get时间 = 1次网络时间 + n次命令时间
Redis可以支撑每秒数万的读写操作, 但是这指的是Redis服务端的处理能力, 对于客户端来说, 一次命令除了命令时间还是有网络时间, 假设网络时间为1毫秒, 命令时间为0.1毫秒(按照每秒处理1万条命令算) , 那么执行1000次get命令和1次mget命令如下
因为Redis的处理能力已经足够高,所以 网络可能会成为性能的瓶颈。
使用批量操作, 有助于提高业务处理效率, 但是要注意的是每次批量操作所发送的命令数不是无节制的, 如果数量过多可能造成Redis阻塞或者网络拥塞。
string的内部编码
字符串类型的3种内部编码
1. int: 8个字节的长整型
2. embstr: 小于等于39个字节的字符串
3. raw: 大于39个字节的字符串
Redis会根据当前值的类型和长度决定使用哪种内部编码实现。
127.0.0.1:6379> set k1 1234 OK 127.0.0.1:6379> object encoding k1 "int" 127.0.0.1:6379> set k2 "my name is artisan" OK 127.0.0.1:6379> object encoding k2 "embstr" 127.0.0.1:6379> STRLEN k2 (integer) 18 127.0.0.1:6379> set k3 "sdfdfsdfsdfdf sfds dfsdfs dsf sfsdfs fd fsf fs sf sfds fdfs fsdf sfs fsdf sdfs f ssfd fsd fdf sfsdf f" OK 127.0.0.1:6379> STRLEN k3 (integer) 102 127.0.0.1:6379> object encoding k3 "raw" 127.0.0.1:6379>
客户端操作
我这里的redis server设置了密码,密码为artisan ,所以有
auth artisan
[redis@artisan bin]$ pwd /home/redis/redis/redis-4.0.11/bin [redis@artisan bin]$ ./redis-cli 127.0.0.1:6379> auth artisan OK 127.0.0.1:6379> set key1 value1 OK 127.0.0.1:6379> set key2 value2 OK 127.0.0.1:6379> get key1 "value1" 127.0.0.1:6379> del key1 (integer) 1 127.0.0.1:6379> strlen key2 (integer) 6 127.0.0.1:6379> getset key2 new_value2 "value2" 127.0.0.1:6379> get key2 "new_value2" 127.0.0.1:6379> getrange key2 0 3 "new_" 127.0.0.1:6379> append key2 _app (integer) 14 127.0.0.1:6379> get key2 "new_value2_app" 127.0.0.1:6379>
Spring中操作redis的字符串
spring-redis-string.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <context:property-placeholder location="classpath:redis/redis.properties" /> <!--2,注意新版本2.3以后,JedisPoolConfig的property name,不是maxActive而是maxTotal,而且没有maxWait属性,建议看一下Jedis源码或百度。 --> <!-- redis连接池配置 --> <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig"> <!--最大空闲数 --> <property name="maxIdle" value="${redis.maxIdle}" /> <!--连接池的最大数据库连接数 --> <property name="maxTotal" value="${redis.maxTotal}" /> <!--最大建立连接等待时间 --> <property name="maxWaitMillis" value="${redis.maxWaitMillis}" /> <!--逐出连接的最小空闲时间 默认1800000毫秒(30分钟) --> <property name="minEvictableIdleTimeMillis" value="${redis.minEvictableIdleTimeMillis}" /> <!--每次逐出检查时 逐出的最大数目 如果为负数就是 : 1/abs(n), 默认3 --> <property name="numTestsPerEvictionRun" value="${redis.numTestsPerEvictionRun}" /> <!--逐出扫描的时间间隔(毫秒) 如果为负数,则不运行逐出线程, 默认-1 --> <property name="timeBetweenEvictionRunsMillis" value="${redis.timeBetweenEvictionRunsMillis}" /> <property name="testOnBorrow" value="true"></property> <property name="testOnReturn" value="true"></property> <property name="testWhileIdle" value="true"></property> </bean> <!--redis连接工厂 --> <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" destroy-method="destroy"> <property name="poolConfig" ref="jedisPoolConfig"></property> <!--IP地址 --> <property name="hostName" value="${redis.host.ip}"></property> <!--端口号 --> <property name="port" value="${redis.port}"></property> <!--如果Redis设置有密码 --> <property name="password" value="${redis.password}" /> <!--客户端超时时间单位是毫秒 --> <property name="timeout" value="${redis.timeout}"></property> <property name="usePool" value="true" /> <!--<property name="database" value="0" /> --> </bean> <!-- 键值序列化器设置为String 类型 --> <bean id="stringRedisSerializer" class="org.springframework.data.redis.serializer.StringRedisSerializer"/> <!-- redis template definition --> <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate" p:connection-factory-ref="jedisConnectionFactory" p:keySerializer-ref="stringRedisSerializer" p:valueSerializer-ref="stringRedisSerializer"> </bean> </beans>
SpringRedisStringsDemo
package com.artisan.redis.baseStructure.strings; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.data.redis.core.RedisTemplate; public class SpringRedisStringsDemo { public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:spring/spring-redis-string.xml"); // 在 Spring 中, // redisTemplate.opsForValue()所返回的对象可以操作简单的键值对,可以是字符串,也可以是对象,具体依据你所配置的序列化方案 // 这里在spring-redis-string.xml中key和value都是指定的 stringRedisSerializer RedisTemplate<String,String> redisTemplate = (RedisTemplate<String, String>) ctx.getBean("redisTemplate"); // 设置值 //127.0.0.1:6379> set key1 value1 //OK //127.0.0.1:6379> set key2 value2 //OK redisTemplate.opsForValue().set("key1", "value1"); redisTemplate.opsForValue().set("key2", "value2"); // 通过 key 获取值 //127.0.0.1:6379> get key1 //"value1" String key1 = redisTemplate.opsForValue().get("key1"); System.out.println("key1:" + key1); // 通过 key 删除值 //127.0.0.1:6379> del key1 //(integer) 1 Boolean success = redisTemplate.delete("key1"); System.out.println("删除key1是否成功:" + success); // 求长度 //127.0.0.1:6379> strlen key2 //(integer) 6 Long size = redisTemplate.opsForValue().size("key2"); System.out.println("key2长度:" + size); // 设值新值并返回旧值 //127.0.0.1:6379> getset key2 new_value2 //"value2" String oldValue = redisTemplate.opsForValue().getAndSet("key2", "new_value2"); System.out.println("key2旧值:" + oldValue); // 127.0.0.1:6379> get key2 // "new_value2" String newValue = redisTemplate.opsForValue().get("key2"); System.out.println("key2新值:" + newValue); // 获取子串 // 127.0.0.1:6379> getrange key2 0 3 // "new_" String subString = redisTemplate.opsForValue().get("key2", 0, 3); System.out.println("subString:" + subString); // 将新的字符串value加入到原来 key指向的字符串末 // 127.0.0.1:6379> append key2 _app // (integer) 14 Integer value = redisTemplate.opsForValue().append("key2", "_app"); System.out.println("value:" + value); // 127.0.0.1:6379> get key2 // "new_value2_app" String newValue2 = redisTemplate.opsForValue().get("key2"); System.out.println("key2:" + newValue2); } }
首先在客户端中flushdb,确保数据干净。
输出
redis中的数据
Redis中对整数和浮点型数字的支持
上面介绍了字符串最常用的命令 , 但是 Redis 除了这些之外还提供了对整数和浮点型数字的功能,如果字符串是数字(整数或者浮点数〉,那么 Redis 还能支持简单的运算。但是它的运算能力比较弱 , 目前版本只能支持简单的加减法运算。
客户端操作
[redis@artisan bin]$ ./redis-cli 127.0.0.1:6379> auth artisan OK 127.0.0.1:6379> flushdb OK 127.0.0.1:6379> set number 8 OK 127.0.0.1:6379> GET number "8" 127.0.0.1:6379> INCR number (integer) 9 127.0.0.1:6379> INCRBY number 5 (integer) 14 127.0.0.1:6379> DECR number (integer) 13 127.0.0.1:6379> DECRBY number 10 (integer) 3 127.0.0.1:6379> INCRBYFLOAT number 8.88 "11.88" 127.0.0.1:6379> get number "11.88" 127.0.0.1:6379>
如果开始把val 设置为浮点数,那么 incr、 deer、 incrby 、 deerby 的命令都会失败。 Redis 并不支持减法 、 乘法、除法操作,功能十分有限,需要注意。
Spring中操作redis的字符串的加减运算
package com.artisan.redis.baseStructure.strings; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.data.redis.core.RedisTemplate; public class SpringRedisStringAdd_Subtraction { public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:spring/spring-redis-string.xml"); RedisTemplate redisTemplate = (RedisTemplate) ctx.getBean("redisTemplate"); // 127.0.0.1:6379> set number 8 // OK redisTemplate.opsForValue().set("number", String.valueOf(8)); // 127.0.0.1:6379> GET number // "8" System.out.println(redisTemplate.opsForValue().get("number")); // 127.0.0.1:6379> INCR number // (integer) 9 System.out.println(redisTemplate.opsForValue().increment("number", 1)); // 127.0.0.1:6379> INCRBY number 5 // (integer) 14 System.out.println(redisTemplate.opsForValue().increment("number", 5)); // 127.0.0.1:6379> DECR number // (integer) 13 Long number = redisTemplate.getConnectionFactory().getConnection().decr(redisTemplate.getKeySerializer().serialize("number")); System.out.println(number); // 127.0.0.1:6379> DECRBY number 10 // (integer) 3 Long number2 = redisTemplate.getConnectionFactory().getConnection().decrBy(redisTemplate.getKeySerializer().serialize("number"), 10); System.out.println(number2); // 127.0.0.1:6379> INCRBYFLOAT number 8.88 // "11.88" System.out.println(redisTemplate.opsForValue().increment("number", 8.88)); // 127.0.0.1:6379> get number // "11.88" System.out.println(redisTemplate.opsForValue().get("number")); } }
在配置文件中使用的是字符串序列化器,所以 Redis 保存的还是字符串 ,如果采用其他的序列化器,比如 JDK 序列化器,那么 Redis 保存的将不会是数字而是产生异常。
Spring 己经优化了代码,所increment 方法可 以支持long和double的加法,如下
对于减法, RedisTemplate 并没有进行支持。 所以用下面的代码去代替它 :
redisTemplate . getConnectionFactory() .getConnection() . decrBy(redisTemplate.getKeySerializer() . serialize ("number") , 10) ;
通过获得连接工厂再获得连接从而得到底层的 Redis 连接对象。为了和 RedisTemplate的配置保持一致 ,所以先获取了其 keySerializer 属性 ,对键进行了序列化,如果获取结果也可以进行同样的转换。
当然 getConnection()只是获取一个 spring data redis 项目中封装的底层对象 RedisConnection , 甚至可以获取原始的链接对象Jedis 对象。
Jedis jedis =(Jedis)redisTemplate.getConnectionFactory() .getConnection() .getNativeConnection() ;
关于减法的方法,原有值都必须是整数,否则就会引发异常.
// 如果进行减法运算原有值都必须是整数,否则就会引发异常,比如下面的,编译通过,但运行会抛出异常 redisTemplate.opsForValue().set("number", "1.1"); redisTemplate.getConnectionFactory().getConnection().decr(redisTemplate.getKeySerializer().serialize("number"));
Exception in thread "main" org.springframework.dao.InvalidDataAccessApiUsageException: ERR value is not an integer or out of range; nested exception is redis.clients.jedis.exceptions.JedisDataException: ERR value is not an integer or out of range at org.springframework.data.redis.connection.jedis.JedisExceptionConverter.convert(JedisExceptionConverter.java:64) at org.springframework.data.redis.connection.jedis.JedisExceptionConverter.convert(JedisExceptionConverter.java:41) at org.springframework.data.redis.PassThroughExceptionTranslationStrategy.translate(PassThroughExceptionTranslationStrategy.java:44) at org.springframework.data.redis.FallbackExceptionTranslationStrategy.translate(FallbackExceptionTranslationStrategy.java:42) at org.springframework.data.redis.connection.jedis.JedisConnection.convertJedisAccessException(JedisConnection.java:181) at org.springframework.data.redis.connection.jedis.JedisStringCommands.convertJedisAccessException(JedisStringCommands.java:757) at org.springframework.data.redis.connection.jedis.JedisStringCommands.decr(JedisStringCommands.java:469) at org.springframework.data.redis.connection.DefaultedRedisConnection.decr(DefaultedRedisConnection.java:301) at com.artisan.redis.baseStructure.strings.SpringRedisStringAdd_Subtraction.main(SpringRedisStringAdd_Subtraction.java:49) Caused by: redis.clients.jedis.exceptions.JedisDataException: ERR value is not an integer or out of range at redis.clients.jedis.Protocol.processError(Protocol.java:127) at redis.clients.jedis.Protocol.process(Protocol.java:161) at redis.clients.jedis.Protocol.read(Protocol.java:215) at redis.clients.jedis.Connection.readProtocolWithCheckingBroken(Connection.java:340) at redis.clients.jedis.Connection.getIntegerReply(Connection.java:265) at redis.clients.jedis.BinaryJedis.decr(BinaryJedis.java:708) at org.springframework.data.redis.connection.jedis.JedisStringCommands.decr(JedisStringCommands.java:467) ... 2 more
注意
使用 Spring 提供的 RedisTemplate 去展示多个命令可以学习到如何使用 RedisTemplate 操作 Redis 。 实际工作中并不是那么用的,因为每一 个操作会尝试从连接池里获取 一 个新的 Redis 连接,多个命令应该使用SessionCallback 接口进行操作 。
常见使用场景
仅列举常见场景,不展开描述
缓存
Redis作为缓存层, 绝大部分请求的数据都是从Redis中获取。由于Redis具有支撑高并发的特性, 所以缓存通常能起到加速读写和降低后端压力的作用。
计数器
使用Redis作为计数的基础工具, 它可以实现快速计数、查询缓存的功能, 同时数据可以异步落地到其他数据源。
限流
举个例子,对某个接口在1分钟内限制调用10次
session共享
使用Redis将用户的Session进行集中管理,在这种模式下只要保证Redis是高可用和扩展性的, 每次用户更新或者查询登录信息都直接从Redis中集中获取
分布式锁
http://www.importnew.com/27477.html
代码
代码托管到了 https://github.com/yangshangwei/redis_learn