1. Redis介绍
Redis 是用C语言开发的一个开源的高性能键值对( key-value )内存数据库,它是一种NoSQL 数据库。它是单进程单线程的内存数据库,所以说不存在线程安全问题。它可以支持并发 10W QPS,所以说性能非常优秀。之所以单进程单线程性能还这么好,是因为底层采用了IO 多路复用(NIO思想)。
相比Memcache这种专业缓存技术,它有更优秀的读写性能,及丰富的数据类型。它提供了五种数据类型来存储值:字符串类型(string)、散列类型(hash)、列表类型(list)、集合类型(set)、有序集合类型(sortedset、zset)。
1.1 Redis官网
官网地址:http://redis.io/
中文官网地址:http://www.redis.cn/
下载地址:http://download.redis.io/releases/
1.2 Redis发展历史
2008年,意大利的一家创业公司Merzia 推出了一款基于MySQL 的网站实时统计系统LLOOGG ,然而没过多久该公司的创始人 Salvatore Sanfilippo 便 对MySQL 的性能感到失望,于是他决定亲自为LLOOGG 量身定做一个数据 库,并于2009年开发完成,这个数据库就是 Redis。
不过Salvatore Sanfilippo 并不满足只将Redis 用于LLOOGG 这一款产品,而是希望更多的人使用它,于是在同一年 Salvatore Sanfilippo 将 Redis 开源发布,并开始和Redis 的另一名主要的代码贡献者Pieter Noordhuis 一起继续着 Redis 的开发,直到今天。
Salvatore Sanfilippo 自己也没有想到,短短的几年时间, Redis 就拥有了庞大的用户群体。 Hacker News 在2012年发布了一份数据库的使用情况调查,结果显示有近12%的公司在使用Redis。国内如新浪微博、街旁网、知乎网,国外如 GitHub 、 Stack Overflow 、 Flickr 等都是 Redis 的用户。
VMware 公司从2010年开始赞助Redis 的开发, Salvatore Sanfilippo 和Pieter Noordhuis 也分别在3月和5 月加入 VMware ,全职开发 Redis 。
2. Redis单机版安装配置
2.1 Redis下载
官网地址:http://redis.io/
中文官网地址:http://www.redis.cn/
下载地址:http://download.redis.io/releases/
2.2 Redis安装环境
Redis 没有官方的Windows 版本,所以建议在Linux 系统上安装运行,我们使用CentOS 7 作为安装环境。
2.3 Redis安装
2.3.1 预安装环境
由于在安装过程中需要对源码进行编译,而编译依赖 gcc 环境,并且我们需要从网络上自动下载文件的自由工具wget,使用以下命令来完成(yum 方式需要联网)。
1 yum install -y gcc-c++
2 yum install -y wget
2.3.2 下载并解压缩Redis 源码压缩包
1 cd /usr/local
2 wget http://download.redis.io/releases/redis-5.0.4.tar.gz
3 tar -zxf redis-5.0.4.tar.gz
2.3.3 编译Redis 源码
进入到解压的Redis文件目录,然后输入 make 命令进行编译:
1 cd /usr/local/redis-5.0.4
2 make
2.3.4 构建安装Redis
编译完成之后,还是在该目录下输入 make install 进行构建:该命令会生成 Redis的5个二进制文件,默认是在 /usr/local/bin 路径下,但是我们可以手动指定生成的文件位置,将 make install 变成:
make install PREFIX=/usr/local/redis
2.4 Redis启动
2.4.1 前端启动
2.4.1.1 启动命令:
cd /usr/local/redis/bin
./redis-server
2.4.1.2 关闭命令:
ctrl+c
2.4.1.3 启动缺点:
客户端窗口关闭则redis-server 程序结束,不推荐使用此方法
2.4.2 后端启动
2.4.2.1 拷贝Redis解压目录下的redis.conf 配置文件到Redis 安装目录下的bin 目录
cp /usr/loca/redis-5.0.4/redis.conf /usr/local/redis/bin/
2.4.2.2 修改redis.conf
#将daemonize由no改为yes
daemonize yes
# 默认绑定的是回环地址,不能被其他机器访问,将其屏蔽
# bind 127.0.0.1
# 将protected-mode由yes改为no
protected-mode no
2.4.2.3 启动命令
./redis-server redis.conf
2.4.2.4 关闭命令
./redis-cli shutdown
2.4.2.5 其他命令说明
redis-server :启动redis 服务
redis-cli :进入redis 命令客户端
redis-benchmark :性能测试工具
redis-check-aof :aof 文件检查工具
redis-check-dump :rdb 文件检查工具
redis-sentinel :启动哨兵监控服务
3. Redis客户端
3.1 Redis命令行客户端
命令格式
./redis-cli -h 127.0.0.1 -p 6379
./redis-cli
Redis不仅可以使用命令来操作,而且可以使用程序客户端操作。
3.2 Java客户端Jedis
3.2.1 Jedis介绍
在官方网站里列一些Java的客户端,有Jedis、Redisson、Jredis、JDBC-Redis等,其中官方推荐使用Jedis和Redisson。
3.2.2 添加依赖
<dependencies>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.0.7.RELEASE</version>
</dependency>
<!-- 单元测试Junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 配置Maven的JDK编译级别 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.2</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
3.2.3 单实例连接
@Test
public void testJedis() {
Jedis jedis = new Jedis("192.168.56.101", 6379);
jedis.set("test", "hello world, this is jedis client!");
String result = jedis.get("test");
jedis.close();
}
3.2.4 连接池连接
@Test
public void testJedisPool() {
//创建一连接池对象
JedisPool jedisPool = new JedisPool("192.168.56.101", 6379);
//从连接池中获得连接
Jedis jedis = jedisPool.getResource();
String result = jedis.get("test") ;
System.out.println(result);
//关闭连接
jedis.close();
//关闭连接池
jedisPool.close();
}
3.2.5 连接redis集群
@Test
public void testJedisCluster() throws Exception {
//创建一连接,JedisCluster对象,在系统中是单例存在
Set<HostAndPort> nodes = new HashSet<>();
//此时连接的是所有的主从节点,若使用哨兵机制的话,连接的是哨兵,面会讲到
nodes.add(new HostAndPort("192.168.56.101", 7001));
nodes.add(new HostAndPort("192.168.56.101", 7002));
nodes.add(new HostAndPort("192.168.56.101", 7003));
nodes.add(new HostAndPort("192.168.56.101", 7004));
nodes.add(new HostAndPort("192.168.56.101", 7005));
nodes.add(new HostAndPort("192.168.56.101", 7006));
JedisCluster cluster = new JedisCluster(nodes);
cluster.set("cluster-test", "my jedis cluster test");
String result = cluster.get("cluster-test");
System.out.println(result);
cluster.close();
}
3.2.6 spring整合Jedis
3.2.6.1 配置spring配置文件applicationContext.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" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 连接池配置 -->
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
<!-- 最大连接数 -->
<property name="maxTotal" value="30" />
<!-- 最大空闲连接数 -->
<property name="maxIdle" value="10" />
<!-- 每次释放连接的最大数目 -->
<property name="numTestsPerEvictionRun" value="1024" />
<!-- 释放连接的扫描间隔(毫秒) -->
<property name="timeBetweenEvictionRunsMillis" value="30000" />
<!-- 连接最小空闲时间 -->
<property name="minEvictableIdleTimeMillis" value="1800000" />
<!-- 连接空闲多久后释放, 当空闲时间>该值 且 空闲连接>最大空闲连接数 时直接释放 --> <property name="softMinEvictableIdleTimeMillis" value="10000" />
<!-- 获取连接时的最大等待毫秒数,小于零:阻塞不确定的时间,默认-1 -->
<property name="maxWaitMillis" value="1500" />
<!-- 在获取连接的时候检查有效性, 默认false -->
<property name="testOnBorrow" value="true" />
<!-- 在空闲时检查有效性, 默认false -->
<property name="testWhileIdle" value="true" />
<!-- 连接耗尽时是否阻塞, false报异常,ture阻塞直到超时, 默认true -->
<property name="blockWhenExhausted" value="false" />
</bean>
<!-- redis单机 通过连接池 -->
<bean id="jedisPool" class="redis.clients.jedis.JedisPool" destroy-method="close">
<constructor-arg name="poolConfig" ref="jedisPoolConfig" />
<constructor-arg name="host" value="192.168.10.135" />
<constructor-arg name="port" value="6379" />
</bean>
<!-- redis集群 -->
<bean id="jedisCluster" class="redis.clients.jedis.JedisCluster">
<constructor-arg index="0">
<set>
<bean class="redis.clients.jedis.HostAndPort"> <constructor-arg index="0" value="192.168.56.101"></constructor-arg> <constructor-arg index="1" value="7001"></constructor-arg> </bean>
<bean class="redis.clients.jedis.HostAndPort"> <constructor-arg index="0" value="192.168.56.101"></constructor-arg> <constructor-arg index="1" value="7002"></constructor-arg></bean>
<bean class="redis.clients.jedis.HostAndPort"><constructor-arg index="0" value="192.168.56.101"></constructor-arg><constructor-arg index="1" value="7003"></constructor-arg> </bean>
<bean class="redis.clients.jedis.HostAndPort"><constructor-arg index="0" value="192.168.56.101"></constructor-arg><constructor-arg index="1" value="7004"></constructor-arg> </bean>
<bean class="redis.clients.jedis.HostAndPort"><constructor-arg index="0" value="192.168.56.101"></constructor-arg> <constructor-arg index="1" value="7005"></constructor-arg></bean>
<bean class="redis.clients.jedis.HostAndPort"><constructor-arg index="0" value="192.168.56.101"></constructor-arg><constructor-arg index="1" value="7006"></constructor-arg> </bean>
</set>
</constructor-arg>
<constructor-arg index="1" ref="jedisPoolConfig"></constructor-arg>
</bean>
</beans>
3.2.6.2 测试代码
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:application.xml")
public class TestJedis2 {
@Autowired private JedisPool jedisPool;
@Resource private JedisCluster cluster;
@Test public void testJedisPool() {
Jedis jedis = jedisPool.getResource();
String result = jedis.get("test");
System.out.println(result);
jedis.close();
}
@Test public void testJedisCluster() throws Exception {
cluster.set("cluster-test", "my jedis cluster test");
String result = cluster.get("cluster-test");
System.out.println(result);
}
}
4. Redis数据类型
Redis 中存储数据是通过key-value 格式存储数据的,其中value 可以定义五种数据类型:
String(字符类型) Hash(散列类型) List(列表类型) Set(集合类型)SortedSet(有序集合类型,简称zset)
4.1 string类型
SET/GET/GETSET:赋值/取值/取值并赋值
SET key value
GET key
GETSET key value
数值增减
当value为整数数据时,才能使用以下命令操作数值的增减。
数值增减都是原子操作。
redis中的每一个单独的命令都是原子性操作。
当多个命令一起执行的时候,就不能保证原子性,不过我们可以使用事务和lua脚本来保证这一点
INCR/DECR:递增/递减
INCR key
DECR key
INCRBY/DECRBY:增加/减少指定的整数
INCRBY key increment
DECRBY key decrement
SETNX:仅当不存在时赋值(使用该命令可以实现分布式锁的功能)
SETNX key value
127.0.0.1:6379> EXISTS job
(integer) 0
127.0.0.1:6379> SETNX job "programmer"
(integer) 1 # 赋值成功
127.0.0.1:6379> SETNX job "code-farmer"
(integer) 0 # 赋值失败
127.0.0.1:6379> GET job
"programmer"
APPEND:向尾部追加值。如果键不存在则将该键的值设置为value ,即相当于 SET key value 。返回值是追加后字符串的总长度。
APPEND key value
STRLEN:获取字符串长度,返回键值的长度,如果键不存在则返回0。
STRLEN key
MSET/MGET:同时设置/获取多个键值
MSET key value [key value …]
MGET key [key …]
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3
OK
127.0.0.1:6379> get k1
"v1"
127.0.0.1:6379> mget k1 k3
1) "v1"
2) "v3"
4.2 hash类型
hash 类型也叫散列类型,它提供了字段和字段值的映射。字段值只能是字符串类型,不支持散列类型、集合类型等其它类型。如下:
001.png赋值
HSET 命令不区分插入和更新操作,当执行插入操作时HSET 命令返回 1 ,当执行更新操作时返回 0 。
HSET/HMSET 设置一个/多个字段值
HSET key field value
HMSET key field value [field value ...]
HSETNX:当字段不存在时赋值,类似HSET ,区别在于如果字段存在,该命令不执行任何操作
HSETNX key field value
取值
HGET/HMGET/HGETALL:获取一个/多个/所有字段值
HGET key field
HMGET key field [field ...]
HGETALL key
127.0.0.1:6379> hgetall user
1) "age"
2) "20"
3) "username" 5 4) "lisi"
HDEL:删除字段。可以删除一个或多个字段,返回值是被删除的字段个数
HDEL key field [field ...]
HINCRBY:增加指定的整数
HINCRBY key field increment
HEXISTS:判断字段是否存在
HEXISTS key field
127.0.0.1:6379> hexists user age
(integer) 1
127.0.0.1:6379> hexists user name
(integer) 0
HKEYS/HVALS:只获取字段名或字段值
HKEYS key
HVALS key
127.0.0.1:6379> hmset user age 20 name lisi
OK
127.0.0.1:6379> hkeys user
1) "age"
2) "name"
127.0.0.1:6379> hvals user
1) "20"
2) "lisi"
HLEN:获取字段数量
HLEN key
127.0.0.1:6379> hlen user
(integer) 2
HGETALL:获得hash 的所有信息,包括key 和value
HGETALL key
string类型和hash类型的区别
hash类型适合存储那些对象数据,特别是对象属性经常发生【增删改】操作的数据。
string类型也可以存储对象数据,将java对象转成json字符串进行存储,这种存储适合【查询】操作。
4.3 list类型
Redis 的列表类型( list 类型)可以存储一个有序的字符串列表 ,常用的操作是向列表两端添加元素,或者获得列表的某一个片段。
列表类型内部是使用双向链表( double linked list )实现的,所以向列表两端添加元素的时间复杂度0(1) ,获取越接近两端的元素速度就越快。这意味着即使是一个有几千万个元素的列表,获取头部或尾部的10条记录也是极快的。
LPUSH/RPUSH:从列表两端压入元素
LPUSH key value [value ...]
RPUSH key value [value ...]
LRANGE:获取列表中的某一片段。将返回start
、stop
之间的所有元素(包含两端的元素),索引从0
开始。索引可以是负数,如:“-1”代表最后边的一个元素。
LRANGE key start stop
LPOP/RPOP:从列表两端弹出元素
从列表左边弹出一个元素,会分两步完成:
第一步是将列表左边的元素从列表中移除。第二步是返回被移除的元素值。
LPOP key
RPOP key
127.0.0.1:6379> lpop list:1
"3"
127.0.0.1:6379> rpop list:1
"6"
LLEN:获取列表中元素的个数
llen key
LREM:删除列表中前count 个值为value 的元素,返回实际删除的元素个数。
根据count 值的不同,该命令的执行方式会有所不同:
当count>0时, LREM会从列表左边开始删除。
当count<0时, LREM会从列表后边开始删除。
当count=0时, LREM删除所有值为value的元素。
LREM key count value
LINDEX:获得指定索引的元素值
LINDEX key index
LSET:设置指定索引的元素值
LSET key index value
LTRIM:只保留列表指定片段,指定范围和LRANGE一致
LTRIM key start stop
LINSERT: 向列表中插入元素。
该命令首先会在列表中从左到右查找值为pivot的元素,然后根据第二个参数是BEFORE还是AFTER来决定将value插入到该元素的前面还是后面
LINSERT key BEFORE|AFTER pivot value
127.0.0.1:6379> lrange list 0 -1
1) "3"
2) "2"
3) "1"
127.0.0.1:6379> linsert list after 3 4
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "3"
2) "4"
3) "2"
4) "1"
RPOPLPUSH:将元素从一个列表转移到另一个列表中
RPOPLPUSH source destination
127.0.0.1:6379> rpoplpush list newlist 2
"1"
127.0.0.1:6379> lrange newlist 0 -1
1) "1"
127.0.0.1:6379> lrange list 0 -1
1) "3"
2) "4"
3) "2"
4.4 set类型
set 类型即集合类型,其中的数据是不重复且没有顺序。
集合类型和列表类型的对比:
002.png 集合类型的常用操作是向集合中加入或删除元素、判断某个元素是否存在等,由于集合类型的Redis 内部是使用值为空的散列表实现,所有这些操作的时间复杂度都为 0(1) 。Redis 还提供了多个集合之间的交集、并集、差集的运算。
SADD/SREM:添加元素/删除元素
SADD key member [member ...]
SREM key member [member ...]
SMEMBERS:获得集合中的所有元素
SMEMBERS key
SISMEMBER:判断元素是否在集合中
SISMEMBER key member
SDIFF:集合的差集运算 A-B
SDIFF key [key ...]
127.0.0.1:6379> sadd setA 1 2 3
(integer) 3
127.0.0.1:6379> sadd setB 2 3 4
integer) 3
127.0.0.1:6379> sdiff setA setB
1) "1"
127.0.0.1:6379> sdiff setB setA
1) "4"
SINTER:集合的交集运算 A ∩ B
SINTER key [key ...]
SUNION:集合的并集运算 A ∪ B
SUNION key [key ...]
SCARD:获得集合中元素的个数
SCARD key
127.0.0.1:6379> smembers setA
1) "1"
2) "2"
3) "3"
127.0.0.1:6379> scard setA
(integer) 3
SPOP:从集合中弹出一个元素。由于集合是无序的,所有SPOP命令会从集合中随机选择一个元素弹出
SPOP key
127.0.0.1:6379> spop setA
"1"
4.5 zset类型 (sortedset)
zset介绍
在set 集合类型的基础上,有序集合类型(sortedset)为集合中的每个元素都关联一个分数 ,这使得我们不仅可以完成插入、删除和判断元素是否存在在集合中,还能够获得分数最高或最低的前N个元素、获取指定分数范围内的元素等与分数有关的操作。
在某些方面有序集合(sortedset)和列表类型(list)有些相似:
1.二者都是有序的。
2.二者都可以获得某一范围的元素。
但是,二者有着很大区别:
1.列表类型是通过链表实现的,获取靠近两端的数据速度极快,而当元素增多后,访问中间数据的速度会变慢。
2.有序集合类型使用散列表实现,所以即使读取位于中间部分的数据也很快。
3.列表中不能简单的调整某个元素的位置,但是有序集合可以(通过更改分数实现) 。
4.有序集合要比列表类型更耗内存。
ZADD:增加元素。向有序集合中加入一个元素和该元素的分数,如果该元素已经存在则会用新的分数替换原有的分数。返回值是新加入到集合中的元素个数,不包含之前已经存在的元素。
ZADD key score member [score member ...]
127.0.0.1:6379> zadd scoreboard 80 zhangsan 89 lisi 94 wangwu
(integer) 3
127.0.0.1:6379> zadd scoreboard 97 lisi
(integer) 0
ZRANGE/ZREVRANGE:获得排名在某个范围的元素列表。
ZRANGE:按照元素分数从小到大的顺序返回索引从start到stop之间的所有元素(包含两端的元素)
ZREVRANGE:按照元素分数从大到小的顺序返回索引从start到stop之间的所有元素(包含两端的元素)
ZRANGE key start stop [WITHSCORES]
ZREVRANGE key start stop [WITHSCORES]
127.0.0.1:6379> zrange scoreboard 0 2
1) "zhangsan"
2) "wangwu"
3) "lisi"
127.0.0.1:6379> zrevrange scoreboard 0 2
1) "lisi"
2) "wangwu"
3) "zhangsan"
如果需要获得元素的分数的可以在命令尾部加上WITHSCORES 参数
1127.0.0.1:6379> zrange scoreboard 0 1 WITHSCORES
1) "zhangsan"
2) "80"
3) "wangwu"
4) "94"
ZSCORE:获取元素的分数。
ZSCORE key member
127.0.0.1:6379> zscore scoreboard lisi
"97"
ZREM:删除元素。移除有序集合key中的一个或多个成员,不存在的成员将被忽略。当key存在但不是有序集类型时,返回一个错误。
ZREM key member [member ...]
127.0.0.1:6379> zrem scoreboard lisi
(integer) 1
ZRANGEBYSCORE:获得指定分数范围的元素。
ZRANGEBYSCORE key min max [WITHSCORES]
127.0.0.1:6379> ZRANGEBYSCORE scoreboard 90 97 WITHSCORES
1) "wangwu"
2) "94"
3) "lisi"
4) "97"
127.0.0.1:6379> ZRANGEBYSCORE scoreboard 70 100 limit 1 2
1) "wangwu"
2) "lisi"
ZINCRBY:增加某个元素的分数,返回值是更改后的分数
ZINCRBY key increment member
127.0.0.1:6379> ZINCRBY scoreboard 4 lisi
"101"
ZCARD:获得集合中元素的数量。
ZCARD key
127.0.0.1:6379> ZCARD scoreboard
(integer) 3
ZCOUNT:获得指定分数范围内的元素个数
ZCOUNT key min max
127.0.0.1:6379> ZCOUNT scoreboard 80 90
(integer) 1
ZREMRANGEBYRANK:按照排名范围删除元素
ZREMRANGEBYRANK key start stop
127.0.0.1:6379> ZREMRANGEBYRANK scoreboard 0 1
(integer) 2
127.0.0.1:6379> ZRANGE scoreboard 0 -1
1) "lisi"
ZREMRANGEBYSCORE:按照分数范围删除元素
ZREMRANGEBYSCORE key min max
127.0.0.1:6379> zadd scoreboard 84 zhangsan
(integer) 1
127.0.0.1:6379> ZREMRANGEBYSCORE scoreboard 80 100
(integer) 1
ZRANK/ZREVRANK: 获取元素的排名。
ZRANK:从小到大
ZREVRANK:从大到小
ZRANK key member
ZREVRANK key member
127.0.0.1:6379> ZRANK scoreboard lisi
(integer) 0
127.0.0.1:6379> ZREVRANK scoreboard zhangsan
(integer) 1
4.6 通用命令
keys:返回满足给定pattern 的所有key
keys pattern
127.0.0.1:6379> keys mylist*
1) "mylist"
2) "mylist5"
3) "mylist6"
4) "mylist7"
5) "mylist8"
del:删除key
del key
exists:确认一个key 是否存在 。存在则返回1,不存在的返回0 。
exists key
Redis在实际使用过程中更多的用作缓存。缓存数据一般需要设置生存时间,到期后数据销毁。
EXPIRE:设置key的生存时间(秒)
EXPIRE key seconds
PEXPIRE :设置key的生存时间(毫秒)
PEXPIRE key milliseconds
TTL :以秒为单位返回给定 key 的剩余生存时间(TTL, time to live)
TTL key
PTTL :以毫秒为单位返回 key 的剩余生存时间。
PTTL key
PERSIST :移除 key 的过期时间,key 将持久保持。
PERSIST key
rename :重命名key
rename oldkey newkey
type:返回 key 所储存的值的类型。
type key
5 Redis数据结构
Redis在存储对象时,并不是直接将数据扔进内存,而是会对对象进行各种包装:如redisObject、SDS等。
5.1 简单动态字符串
Redis 是用 C 语言写的,但是对于Redis的字符串,却不是 C 语言中的字符串(即以空字符’\0’结尾的字符数组),它是自己构建了一种名为 简单动态字符串(simple dynamic string,SDS)的抽象类型,并将 SDS 作为 Redis的默认字符串表示。
SDS 定义:
struct sdshdr{
//记录buf数组中已使用字节的数量
//等于 SDS 保存字符串的长度
int len;
//记录 buf 数组中未使用字节的数量
int free;
//字节数组,用于保存字符串
char buf[];
}
上面的定义相对于 C 语言对于字符串的定义,多出了 len 属性以及 free 属性。
用SDS保存字符串 “Redis”具体图示如下:
003.png 为什么不使用C语言字符串实现,而是使用 SDS呢?这样实现有什么好处?
1 常数复杂度获取字符串长度
由于 len 属性的存在,我们获取 SDS 字符串的长度只需要读取 len 属性,时间复杂度为 O(1)。而对于 C 语言,获取字符串的长度通常是经过遍历计数来实现的,时间复杂度为 O(n)。通过 strlen key 命令可以获取 key 的字符串长度。
2 杜绝缓冲区溢出
我们知道在 C 语言中使用 strcat 函数来进行两个字符串的拼接,一旦没有分配足够长度的内存空间,就会造成缓冲区溢出。而对于 SDS 数据类型,在进行字符修改的时候,会首先根据记录的 len 属性检查内存空间是否满足需求,如果不满足,会进行相应的空间扩展,然后在进行修改操作,所以不会出现缓冲区溢出。
3 减少修改字符串的内存重新分配次数
C语言由于不记录字符串的长度,所以如果要修改字符串,必须要重新分配内存(先释放再申请),因为如果没有重新分配,字符串长度增大时会造成内存缓冲区溢出,字符串长度减小时会造成内存泄露。
而对于SDS,由于len属性和free属性的存在,对于修改字符串SDS实现了空间预分配和惰性空间释放两种策略:①空间预分配:对字符串进行空间扩展的时候,扩展的内存比实际需要的多,这样可以减少连续执行字符串增长操作所需的内存重分配次数。②惰性空间释放:对字符串进行缩短操作时,程序不立即使用内存重新分配来回收缩短后多余的字节,而是使用 free 属性将这些字节的数量记录下来,等待后续使用。(当然SDS也提供了相应的API,当我们有需要时,也可以手动释放这些未使用的空间。)
4 二进制安全
因为C字符串以空字符作为字符串结束的标识,而对于一些二进制文件(如图片等),内容可能包括空字符串,因此C字符串无法正确存取;而所有 SDS 的API 都是以处理二进制的方式来处理 buf 里面的元素,并且 SDS 不是以空字符串来判断是否结束,而是以 len 属性表示的长度来判断字符串是否结束。
5 兼容部分 C 字符串函数
虽然 SDS 是二进制安全的,但是一样遵从每个字符串都是以空字符串结尾的惯例,这样可以重用 C 语言库<string.h> 中的一部分函数。
C字符串 | SDS |
---|---|
获取字符串长度的复杂度为O(N) | 获取字符串长度的复杂度为0(1) |
API是不安全的,可能会造成缓冲区溢出 | API是安全的,不会造成缓冲区溢出 |
修改字符串长度N次必然需要执行N次内存重分配 | 修改字符串长度N次最多需要执行N次内存重分配 |
只能保存文本数据 | 可以保存文本或者二进制数据 |
可以使用所有<string.h>库中的函数 | 可以使用一部分<string.h>库中的函数 |
一般来说,SDS 除了保存数据库中的字符串值以外,SDS 还可以作为缓冲区(buffer):包括 AOF 模块中的AOF缓冲区以及客户端状态中的输入缓冲区。
5.2 链表
链表提供了高效的节点重排能力,以及顺序性的节点访问方式,并且可以通过增删节点 来灵活地调整链表的长度。作为一种常用数据结构,链表内置在很多高级的编程语言里面,因为Redis使用的C语 言并没有内置这种数据结构,所以Redis构建了自己的链表实现。
链表在Redis中的应用非常广泛,比如列表键的底层实现之一就是链表。当一个列表键 包含了数量比较多的元素,又或者列表中包含的元素都是比较长的字符串时,Redis就会使用链表作为列表键的底层实现。此外发布与订阅、慢查询、监视器等功能也用到了链表。
每个链表节点使用一个adlist .h/listNode结构来表示
typedef struct listNode {
//前置节点
struct listNode *prev;
//后置节点
struct listNode *next;
//节点的值
void *value;
}listNode
多个listNode可以通过prev和next指针组成双端链表。
虽然仅仅使用多个listNode结构就可以组成链表,但使用adlist.h/list来持有链表的话,操作起来会更方便:
typedef struct list {
//表头节点
listNode.head;
//表尾节点
listNode.tail;
//链表所包含的节点数量
unsigned long len;
//节点值复制函数
void *(*dup)(void *ptr);
//节点值释放函数
void *(*free)(void *ptr);
//节点值对比函数
int (*match)(void *ptr,void *key);
}list;
list结构为链表提供了表头指针head、表尾指针tail,以及链表长度计数器len, 而dup、free和match成员则是用于实现多态链表所需的类型特定函数。
Redis链表实现特性总结如下:
①双向:链表具有前置节点和后置节点的引用,获取这两个节点时间复杂度都为O(1)。
②无环:表头节点的 prev 指针和表尾节点的 next 指针都指向 NULL,对链表的访问都是以 NULL 结束。
③带链表长度计数器:通过 len 属性获取链表长度的时间复杂度为 O(1)。
④多态:链表节点使用 void* 指针来保存节点值,可以保存各种不同类型的值。
5.3 字典
字典,又称为符号表(symbol table)>关联数组(associative array)或映射(map),是 一种用于保存键值对(key-value pair)的抽象数据结构。
在字典中,一个键(key)可以和一个值(value)进行关联(或者说将键映射为值), 这些关联的键和值就称为键值对。
字典经常作为一种数据结构内置在很多高级编程语言里面,但Redis所使用的C语言并 没有内置这种数据结构,因此Redis构建了自己的字典实现。
字典在Redis中的应用相当广泛,比如Redis的数据库就是使用字典来作为底层实现的, 对数据库的增、删、査、改操作也是构建在对字典的操作之上的。除了用来表示数据库之外,字典还是哈希键的底层实现之一,当一个哈希键包含的键值 对比较多,又或者键值对中的元素都是比较长的字符串时,Redis就会使用字典作为哈希键 的底层实现。除了用来实现数据库和哈希键之外,Redis的不少功能也用到了字典。
Redis的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,而每个哈希表节点就保存了字典中的一个键值对。
5.3.1 哈希表
Redis字典所使用的哈希表由dict.h/dictht结构定义:
typedef struct dictht{
//哈希表数组
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩码,用于计算索引值
//总是等于 size-1
unsigned long sizemask;
//该哈希表已有节点的数量
unsigned long used;
}dictht
table属性是一个数组,数组中的每个元素都是一个指向diet.h/dictEntry结构的指针,每个dictEntry结构保存着一个键值对。
size属性记录了哈希表的大小,也即 是table数组的大小,而used属性则记录了哈希表目前已有节点(键值对)的数量。
sizemask属性的值总是等于size-1,这 个属性和哈希值一起决定一个键应该被放到 table数组的哪个索引上面。
5.3.2 哈希表节点
哈希表节点使用dictEntry结构表示,每个dictEntry结构都保存着一个键值对:
typedef struct dictEntry {
//键
void *key;
//值
union{
void *val;
uint64_tu64;
int64_ts64;
} v;
//指向下个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
key属性保存着键值对中的键。
而v属性则保存着键值对中的值,其中键值对的值可以是一个指针,或者是一个uint64_t整数,又或者是一个int64_t整数。
next属性是指向另一个哈希表苇点的指针,这个指针可以将象个哈希值相同的键值对 连接在一次,以此来解决键冲突(collision)的问题。
5.3.3 字典
Redis中的字典由dict.h/dict结构表示:
typedef struct dict (
//类型特定函数
dictType *type;
//私有数据
void *privdata;
//哈希值
dictht ht[2];
//rehash索引
//当rehash不在进行时,值为-1
in trehashidx; /* rehashing not in progress'if rehashidx == -1 */
} dict;
type属性和privdata属性是针对不同类型的键值对,为创建多态字典而设置的:
type属性是一个指向dictType结构的指针,每个dictType结构保存了一簇用于操作特定类型键值对的函数,Redis会为用途不同的字典设置不同的类型特定函数。
而privdata属性则保存了需要传给那些类型特定函数的可选参数。
typedef struct dictType (
//计算哈希值的函数
unsigned int (*hashFunction)(const void *key);
//复制键的函数
void * (*keyDup) (void *privdata, const void *key);
//复制值的函数
void * (*valDup) (void *privdata, const void *obj);
//对比键的函数
int (*keyCompare)(void *privdata, const void *keylr const void *key2);
//销毁键的函数
void (*keyDestructor)(void *privdata, void *key);
//销毁值的函数
void (*valDestructor)(void *privdata, void *obj);
) dictType;
ht属性是一个包含两个项的数组,数组中的每个项都是_个dictht哈希表,一般情况下,字典只使用ht[0]哈希表,ht[1]哈希表只会在对ht[0]哈希表进行rehash时使用。
除了ht[1]之外,另一个和rehash有关的属性就是rehashidx,它记录了 rehash目前的进度,如果目前没有在进行rehash,那么它的值为-1。
下图展示了一个普通状态下(没有进行rehash )的字典。
004.png
5.3.4
字典被广泛用于实现Redis的各种功能,其中包括数据库和哈希键。
Redis中的字典使用哈希表作为底层实现,每个字典带有两个哈希表,一个平时使用,另一个仅在进行rehash时使用。
当字典被用作数据库的底层实现,或者哈希键的底层实现时,Redis使用MurmurHash2 算法来计算键的哈希值。
哈希表使用链地址法来解决键冲突,被分配到同一个索引上的多个键值对会连接成 一个单向链表。
在对哈希表进行扩展或者收缩操作时,程序需要将现有哈希表包含的所有键值对 rehash到新哈希表里面,并且这个rehash过程并不是一次性地完成的,而是渐进式地完成的。
5.4 跳跃表
跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。
跳跃表支持平均O(logN)、最坏O(N)复杂度的节点査找,还可以通过顺序性操作来批量处理节点。
在大部分情况下,跳跃表的效率可以和平衡树相媲美,并且因为跳跃表的实现比平衡树 要来得更为简单,所以有不少程序都使用跳跃表来代替平衡树。
Redis使用跳跃表作为有序集合键的底层实现之一,如果一个有序集合包含的元素数量比较多,又或者有序集合中元素的成员(member)是比较长的字符串时,Redis就会使用跳跃表来作为有序集合键的底层实现。
和链表、字典等数据结构被广泛地应用在Redis内部不同,Redis只在两个地方用到了跳跃表,一个是实现有序集合键,另一个是在集群节点中用作内部数据结构,除此之外,跳 跃表在Redis里面没有其他用途。
Redis 的跳跃表由redis .h/zskiplistNode 和redis .h/zskiplist 两个结构定义,其中zskiplistNode结构用于表示跳跃表节点,而zskiplist结构则用于保存跳跃表节点的相关信息,比如节点的数量,以及指向表头节点和表尾节点的指针等等。
Redis中跳跃表节点定义如下:
typedef struct zskiplistNode {
//层
struct zskiplistLevel{
//前进指针
struct zskiplistNode *forward;
//跨度
unsigned int span;
}level[];
//后退指针
struct zskiplistNode *backward;
//分值
double score;
//成员对象
robj *obj;
} zskiplistNode
仅靠多个跳跃节点就可以组成跳跃表
typedef struct zskiplist{
//表头节点和表尾节点
structz skiplistNode *header, *tail;
//表中节点的数量
unsigned long length;
//表中层数最大的节点的层数
int level;
}zskiplist;
跳跃表具有如下性质:
1、由很多层结构组成;
2、每一层都是一个有序的链表,排列顺序为由高层到底层,都至少包含两个链表节点,分别是前面的head节点和后面的nil节点;
3、最底层的链表包含了所有的元素;
4、如果一个元素出现在某一层的链表中,那么在该层之下的链表也全都会出现(上一层的元素是当前层的元素的子集);
5、链表中的每个节点都包含两个指针,一个指向同一层的下一个链表节点,另一个指向下一层的同一个链表节点;
005.png
每个跳跃表节点的层高都是1至32之间的随机数。
在同一个跳跃表中,多个节点可以包含相同的分值,但每个节点的成员对象必须是唯一的。
跳跃表中的节点按照分值大小进行排序,当分值相同时,节点按照成员对象的大小进行排序。
跳跃表的增删查:
①搜索:从最高层的链表节点开始,如果比当前节点要大和比当前层的下一个节点要小,那么则往下找,也就是和当前层的下一层的节点的下一个节点进行比较,以此类推,一直找到最底层的最后一个节点,如果找到则返回,反之则返回空。
②插入:首先确定插入的层数,有一种方法是假设抛一枚硬币,如果是正面就累加,直到遇见反面为止,最后记录正面的次数作为插入的层数。当确定插入的层数k后,则需要将新元素插入到从底层到k层。
③删除:在各个层中找到包含指定值的节点,然后将节点从链表中删除即可,如果删除以后只剩下头尾两个节点,则删除这一层。
5.5 整数集合
整数集合(intset)是集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis就会使用整数集合作为集合键的底层实现。
举个例子,如果我们创建一个只包含五个元素的集合键,并且集合中的所有元素都是整数值,那么这个集合键的底层实现就会是整数集合:
redis> SADD numbers 13579
(integer) 5
redis> OBJECT ENCODING numbers
"intset"
整数集合(intset)是Redis用于保存整数值的集合抽象数据结构,它可以保存类型为 intl6_t, int32_t或者int64_t的整数值,并且保证集合中不会出现重复元素。
每个intset.h/intset结构表示一个整数集合:
typedef struct intset {
//编码方式
uint32_t encoding;
//集合包含的元素数量
uint32_t length;
//保存元素的数组
int8_t contents[];
} intset;
contents 数组是整数集合的底层实现:整数集合的每个元素都是contents数组的一个数组项(item),各个项在数组中按值的大小从小到大有序地排列,并且数组中不包含任何重复项。
length属性记录了整数集合包含的元素数量,也即是contents数组的长度。
虽然intset结构将contents属性声明为int8_t类型的数组,但实际上contents数 组并不保存任何int8_t类型的值,contents数组的真正类型取决于encoding属性的值。
5.5.1 升级
每当我们要将一个新元素添加到整数集合里面,并且新元素的类型比整数集合现有所有元素的类型都要长时,整数集合需要先进行升级(upgrade),然后才能将新元素添加到整数 集合里面。
升级整数集合并添加新元素共分为三步进行:
1)根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间。
2)将底层数组现有的所有元素都转换成与新元素相同的类型,并将类型转换后的元素放置到正确的位上,而且在放置元素的过程中,需要 继续维持底层数组的有序性质不变。
3)将新元素添加到底层数组里面。
整数集合的升级策略有两个好处,一个是提升整数集合的灵活性,另一个是尽可能地节约内存。
5.5.2 降级
整数集合不支持降级操作,一旦对数组进行了升级,编码就会一直保持升级后的状态。
5.6 压缩列表
压缩列表(Ziplist )是列表键和哈希键的底层实现之一。当一个列表键只包含少量 列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就 会使用压缩列表来做列表键的底层实现。
例如,执行以下命令将创建一个压缩列表实现的列表键:
redis> RPUSH 1st 1 3 5 10086 "hello" "world"
(integer)6
redis> OBJECT ENCODING 1st
"ziplist"
另外,当一个哈希键只包含少量键值对,比且每个健值对的键和值要么就是小整数值, 要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做哈希键的底层实现。
举个例子,执行以下命令将创建一个压缩列表实现的哈希键:
redis> HMSET profile "name" "Jack" "age" 28 "job" "Programmer"
OK
redis> OBJECT ENCODING profile
"ziplist"
压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺 序型(sequential)数据结构。一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值。下图展示了压缩列表的各个组成部分。
006.png
其中字节数组可以是以下三种长度中的一种:
长度小于等于63 (2、1 )字节的字节数组;
长度小于等于16 383 (21 )字节的字节数组;
长度小于等于4294967295 (232-1 )字节的字节数组;
而整数值则可以是以下六种长度的其中一种:
4位长,介于0至12之间的无符号整数;
1字节长的有符号整数;
3字节长的有符号整数;
intl6_t类型整数;
int32_t类型整数;
int64_t类型整数。
每个压缩列表节点都由 previous_entry_length,encoding, content, 三个部分组成,下图所示。
007.png
节点的previous_entry_length属性以字节为单位,记录了压缩列表中前一个节点的长度。previous_entry_length属性的长度可以是1字节或者5字节:如果前一节点的长度小于254字节,那么previous_entry_length属性的长度为1字节:前一节点的长度就保存在这一个字节里面。如果前一节点的长度大于等于254字节,那么previous_entry_length属性的长度为5字节:其中属性的第一字节会被设置为OxFE (十进制值254),而之后的四个字节则用于保存前一节点的长度。
节点的encoding属性记录了节点的content属性所保存数据的类型以及长度。一字节、两字节或者五字节长,值的最高位为00、01或者10的是字节数组编码: 这种编码表示节点的content属性保存着字节数组,数组的长度由编码除去最高两 位之后的其他位记录;一字节长,值的最高位以11开头的是整数编码:这种编码表示节点的content属 性保存着整数值,整数值的类型和长度由编码除去最高两位之后的其他位记录;
节点的content属性负责保存节点的值,节点值可以是一个字节数组或者整数,值的类型和长度由节点的encoding属性决定。
6 Redis消息通信模式
Redis 常见的消息通信模式有两种:队列模式和发布订阅模式。
6.1 队列模式
Redis 队列模式其实是通过基本数据类型list(列表)的lpush和rpop来实现的,即数据从列表的一端入队,另一端出队。
注意事项:消息接收方如果不知道队列中是否有消息,会一直发送rpop命令,如果这样的话,会每一次都建
立一次连接,这样显然不好。可以使用brpop命令,它如果从队列中取不出来数据,会一直阻塞,在一定时间范围内没有取出则返回null。
6.2 发布订阅模式
发布者(pub)发送消息,订阅者(sub)接收消息,发布者和订阅者之间通过频道(Channel)进行通信。
下图展示了频道 channel1,以及订阅这个频道的三个客户端 —— client2、client5 和 client1 之间的关系:
当有新消息通过 PUBLISH 命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端:
010.png 在我们实例中我们创建了订阅频道名为 runoobChat:
redis 127.0.0.1:6379> SUBSCRIBE runoobChat
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "redisChat"
3) (integer) 1
现在,我们先重新开启个 redis 客户端,然后在同一个频道 runoobChat 发布两次消息。
redis 127.0.0.1:6379> PUBLISH runoobChat "Redis PUBLISH test"
(integer) 1
redis 127.0.0.1:6379> PUBLISH runoobChat "Learn redis by runoob.com"
(integer) 1
订阅者的客户端会显示如下消息
1) "message"
2) "runoobChat"
3) "Redis PUBLISH test"
1) "message"
2) "runoobChat"
3) "Learn redis by runoob.com"
7. Redis事务
Redis 的事务是通过MULTI 、 EXEC 、 DISCARD 和WATCH 、UNWATCH这五个命令来完成的。
Redis 的单个命令都是原子性的,所以这里需要确保事务性的对象是命令集合。
Redis 将命令集合序列化并确保处于同一事务的命令集合连续且不被打断的执行。
Redis 不支持回滚操作。
7.1 事务命令
MULTI:标记一个事务块的开始,Redis会将后续的命令逐个放入队列中。
EXEC:执行所有事务块内的命令,然后恢复正常的连接状态
DISCARD:取消事务,放弃执行事务块内的所有命令,然后恢复正常的连接状态。
WATCH:监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
UNWATCH:取消 WATCH 命令对所有 key 的监视。
7.2 事务演示
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set s1 111
QUEUED
127.0.0.1:6379> hset hash1 name zhangsan
QUEUED
127.0.0.1:6379> exec
1) OK
2) (integer) 1
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set s2 222
QUEUED
127.0.0.1:6379> hset hash2 age 20
QUEUED
127.0.0.1:6379> discard
OK
127.0.0.1:6379> exec
(error) ERR EXEC without MULTI
127.0.0.1:6379> watch s1
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set s1 555
QUEUED
#此时在没有exec之前,通过另一个命令窗口对监控的s1字段进行修改
127.0.0.1:6379> exec
(nil)
127.0.0.1:6379> get s1
"111"
011.png
7.3 事务失败处理
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set a aaa
QUEUED
127.0.0.1:6379> set b bbb
QUEUED
127.0.0.1:6379> set c ccc
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK
3) OK
#如果在 set b bbb 处失败,set a 已成功不会回滚,set c 还会继续执行
7.3.1 Redis编译期错误
127.0.0.1:6379> multi
OK
127.0.0.1:6379〉 sets s1 111
(error) ERR unknown command 'sets'
127.0.0.1:6379> set s2
(error) ERR wrong number of arguments for 'set' command
127.0.0.1:6379> set s3 333
QUEUED
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because cf previous errors.
127.0.0.l:6379> get s3
(nil)
7.3.2 Redis运行期错误
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set s4 444
QUEUED
127.0.0.1:6379> lpush s4 111 222
QUEUED
127.0.0.1:6379> exec
1)QK
2)(error) wrongtype Operation against a key holding the wrong kind of value
127.0.0.1:6379> get s4
"444"
7.4 Redis 不支持事务回滚
1.大多数事务失败是因为语法错误或者类型错误,这两种错误在开发阶段都是可以预见的
2.Redis 为了性能方面就忽略了事务回滚。
7.5 Redis事务使用场景(乐观锁)
乐观锁基于CAS(Compare And Swap)思想(比较并替换),是不具有互斥性,不会产生锁等待而消耗资源,但是需要反复的重试,但也是因为重试的机制,能比较快的响应。因此我们可以利用redis来实现乐观锁。具体思路如下:
1、利用redis的watch功能,监控这个redisKey的状态值
2、获取redisKey的值
3、创建redis事务
4、给这个key的值加一
5、然后去执行这个事务,如果key的值被修改过则回滚,key不加1
public void watch() {
try {
String watchKeys = "watchKeys";
//初始值 value=1
jedis.set(watchKeys, 1);
//监听key为watchKeys的值
jedis.watch(watchkeys);
//开启事务
Transaction tx = jedis.multi();
//watchKeys自增加一
tx.incr(watchKeys);
//执行事务,如果其他线程对watchKeys中的value进行修改,则该事务将不会执行
//通过redis事务以及watch命令实现乐观锁
List<Object> exec = tx.exec();
if (exec == null) {
System.out.println("事务未执行");
} else {
System.out.println("事务成功执行,watchKeys的value成功修改");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
jedis.close();
}
}
Redis乐观锁实现秒杀
public class Second {
public static void main(String[] arg) {
String redisKey = "second";
ExecutorService executorService = Executors.newFixedThreadPool(20);
try {
Jedis jedis = new Jedis("127.0.0.1", 6378);
// 初始值
jedis.set(redisKey, "0");
jedis.close();
} catch (Exception e) {
e.printStackTrace();
}
for (int i = 0; i < 1000; i++) {
executorService.execute(() -> {
Jedis jedis1 = new Jedis("127.0.0.1", 6378);
try {
jedis1.watch(redisKey);
String redisValue = jedis1.get(redisKey);
int valInteger = Integer.valueOf(redisValue);
String userInfo = UUID.randomUUID().toString();
if (valInteger < 20) { // 没有秒完
Transaction tx = jedis1.multi();
tx.incr(redisKey);
List list = tx.exec();
// 秒成功,如果失败返回空list而不是空
if (list != null && list.size() > 0) {
System.out.println("用户:" + userInfo + ",秒杀成功");
} else {// 版本变化,被别人抢了。
System.out.println("用户:" + userInfo + ",秒杀失败");
}
} else {// 秒完了
System.out.println("已经有20人秒杀成功,秒杀结束");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
jedis1.close();
}
});
}
executorService.shutdown();
}
}
8 缓存淘汰策略
在redis中,允许用户设置最大使用内存大小maxmemory,默认为0,没有指定最大缓存,如果有新的数据添加,超过最大内存,则会使redis崩溃,所以一定要设置。redis 内存数据集大小上升到一定大小的时候,就会实行数据淘汰策略。
redis淘汰策略配置:maxmemory-policy voltile-lru
redis 提供 6种数据淘汰策略:
1. volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
2. volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
3. volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
4. allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
5. allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
6. no-enviction(驱逐):禁止驱逐数据
redis淘汰策略支持热配置。
网友评论