Redis
- 参考
- 一、NoSQL 简介
- 二、Redis简介 及 安装
- 三、Redis的五大数据类型
- 四、三种特殊数据类型
- 五、Redis的相关配置
- 六、Redis的Java客户端 Jedis
- 七、 Redis 事务
- 八、Redis 持久化
- 九、发布订阅
- 十、Redis主从复制(补充)
- 十一 哨兵
- 十二、Redis集群
- 十三、缓存穿透、击穿、雪崩
- 十四、 缓存与数据库一致性问题
- 十五、底层数据结构
- 十六 、Redis对象
参考
《Redis设计与实现》
一、NoSQL 简介
1 技术的分类
1)解决功能性的问题
Java、Servlet、Jsp、Tomcat、RDBMS、JDBC、Linux、Svn 等
2)解决扩展性的问题
Spring、 SpringMVC、SpringBoot、Hibernate、MyBatis等
3)解决性能的问题
NoSQL、Java多线程、Nginx、MQ、ElasticSearch、Hadoop等
2 WEB1.0 及WEB2.0
1)Web1.0的时代,数据访问量很有限,用一夫当关的高性能的单节点服务器可以解决大部分问题.
2)Web2.0时代的到来,用户访问量大幅度提升,同时产生了大量的用户数据,加上后来的智能移动设备的普及,所有的互联网平台都面临了巨大的性能挑战.
3 解决服务器CPU内存压力
思考: Session共享问题如何解决?
- 方案一、存在Cookie中
此种方案需要将Session数据以Cookie的形式存在客户端,不安全,网络负担效率低 - 方案二、存在文件服务器或者是数据库里
此种方案会导致大量的IO操作,效率低. - 方案三、Session复制
此种方案会导致每个服务器之间必须将Session广播到集群内的每个节点,Session数据会冗余,节点越多浪费越大,存在广播风暴问题. - 方案四、存在Redis中
目前来看,此种方案是最好的。将Session数据存在内存中,每台服务器都从内存中读取数据,速度快,结构还相对简单.
4 解决IO压力
将活跃的数据缓存到Redis中,客户端的请求先打到缓存中来获取对应的数据,如果能获取到,直接返回,不需要从MySQL中读取。如果缓存中没有,再从MySQL数据库中读取数据,将读取的数据返回并存一份到Redis中,方便下次读取.
扩展: 对于持久化的数据库来说,单个库单个表存在性能瓶颈,因此会通过水平切分、垂直切分、读取分离等技术提升性能,此种解决方案会破坏一定的业务逻辑,但是可以换取更高的性能.
5 NoSQL数据库概述
1)NoSQL(NoSQL = Not Only SQL ),意即“不仅仅是SQL”,泛指非关系型的数据库。
NoSQL 不依赖业务逻辑方式存储,而以简单的key-value模式存储。因此大大的增加了 数据库的扩展能力。
2)NoSQL的特点
不遵循SQL标准
不支持ACID
远超于SQL的性能。
3)NoSQL的适用场景
对数据高并发的读写
海量数据的读写
对数据高可扩展性的
4)NoSQL的不适用场景
需要事务支持
基于sql的结构化查询存储,处理复杂的关系,需要即席查询。
5)建议: 用不着sql的和用了sql也不行的情况,请考虑用NoSql
6 常用的缓存数据库
1)Memcached
2)Redis
- mongoDB
4)列式数据库
- 先看行式数据库
思考: 如下两条SQL的快慢
select * from users where id =3
select avg(age) from users
- 再看列式数据库
二、Redis简介 及 安装
1 Redis是什么
Redis是一个开源的key-value存储系统。和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)和hash(哈希类型)。这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。在此基础上,Redis支持各种不同方式的排序。与memcached一样,为了保证效率,数据都是缓存在内存中。区别的是Redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。
2 Redis的应用场景
1)配合关系型数据库做高速缓存
- 高频次,热门访问的数据,降低数据库IO
2)由于其拥有持久化能力,利用其多样的数据结构存储特定的数据 - 最新N个数据 通过List实现按自然事件排序的数据
- 排行榜,TopN 利用zset(有序集合)
- 时效性的数据,比如手机验证码 Expire过期
- 计数器,秒杀 原子性,自增方法INCR、DECR
- 去除大量数据中的重复数据 利用set集合
- 构建队列利用list集合
- 发布订阅消息系统 pub/sub模式
3 查看默认安装目录 /usr/local/bin
1)Redis-benchmark:性能测试工具,可以有效的测试Redis服务的性能。
redis 性能测试工具可选参数如下所示
序号 | 选项 | 描述 | 默认值 |
---|---|---|---|
1 | -h | 指定服务器主机名 | 127.0.0.1 |
2 | -p | 指定服务器端口 | 6379 |
3 | -s | 指定服务器 socket | |
4 | -c | 指定并发连接数 | 50 |
5 | -n | 指定请求数 | 10000 |
6 | -d | 以字节的形式指定 SET/GET 值的数据大小 | 2 |
7 | -k | 1=keep alive 0=reconnect | 1 |
8 | -r | SET/GET/INCR 使用随机 key, SADD 使用随机值 | |
9 | -P | 通过管道传输 请求 | 1 |
10 | -q | 强制退出 redis。仅显示 query/sec 值 | |
11 | –csv | 以 CSV 格式输出 | |
12 | -l | 生成循环,永久执行测试 | |
13 | -t | 仅运行以逗号分隔的测试命令列表。 | |
14 | -I | Idle 模式。仅打开 N 个 idle 连接并等待。 |
# 测试一:100个并发连接,100000个请求,检测host为localhost 端口为6379的redis服务器性能
redis-benchmark -h localhost -p 6379 -c 100 -n 100000
# 测试出来的所有命令只举例一个!
====== SET ======
100000 requests completed in 1.88 seconds # 对集合写入测试
100 parallel clients # 每次请求有100个并发客户端
3 bytes payload # 每次写入3个字节的数据,有效载荷
keep alive: 1 # 保持一个连接,一台服务器来处理这些请求
17.05% <= 1 milliseconds
97.35% <= 2 milliseconds
99.97% <= 3 milliseconds 100.00% <= 3 milliseconds # 所有请求在 3 毫秒内完成
53248.14 requests per second # 每秒处理 53248.14 次请求
2)Redis-check-aof:修复有问题的AOF文件,rdb和aof后面讲
3)Redis-check-dump:修复有问题的dump.rdb文件
4)Redis-sentinel:Redis集群使用
5)redis-server:Redis服务器启动命令
6)redis-cli:客户端,操作入口
4 Redis的启动
1)默认前台方式启动
- 直接执行redis-server 即可.启动后不能操作当前命令窗口
2)推荐后台方式启动
- 拷贝一份redis.conf配置文件到其他目录,例如根目录下的myredis目录 /myredis
- 修改redis.conf文件中的一项配置 daemonize 将no 改为yes,代表后台启动
- 执行配置文件进行启动 执行 redis-server /myredis/redis.conf
5 客户端访问
1)使用redis-cli 命令访问启动好的Redis
- 如果有多个Redis同时启动,则需指定端口号访问 redis-cli -p 端口号
2)测试验证,通过 ping 命令 查看是否 返回 PONG
6 关闭Redis服务
1)单实例关闭
- 如果还未通过客户端访问,可直接 redis-cli shutdown
- 如果已经进入客户端,直接 shutdown即可.
2)多实例关闭
- 指定端口关闭 redis-cli -p 端口号 shutdown
7 Redis端口号的由来
1)端口号来自一位影星的名字 . Alessia Merz
8 Redis 默认16个库
1)Redis默认创建16个库,每个库对应一个下标,从0开始.
通过客户端连接后默认进入到0 号库,推荐只使用0号库.
2)使用命令 select 库的下标 来切换数据库,例如 select 8
**9 Redis的单线程+多路IO复用技术 **
1)多路复用是指使用一个线程来检查多个文件描述符(Socket)的就绪状态,比如调用select和poll函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞直到超时。得到就绪状态后进行真正的操作可以在同一个线程里执行,也可以启动线程执行(比如使用线程池)。
2)Memcached 是 多线程 + 锁.
Redis 是 单线程 + 多路IO复用.
三、Redis的五大数据类型
1 key
命令 | 作用 |
---|---|
keys * | 查看当前库的所有键 |
exists < key > | 判断某个键是否存在 |
type < key > | 查看键的类型 |
del < key> | 删除某个键 |
expire < key> < seconds> | 为键值设置过期时间,单位秒 |
ttl < key> | 查看还有多久过期,-1表示永不过期,-2表示已过期 |
dbsize | 查看当前数据库中key的数量 |
flushdb | 清空当前库 |
Flushall | 通杀全部库 |
2 String
1)String是Redis最基本的类型,你可以理解成与Memcached一模一样的类型,一个key对应一个value
2)String类型是二进制安全的。意味着Redis的string可以包含任何数据。比如jpg图片或者序列化的对象 。
3)String类型是Redis最基本的数据类型,一个Redis中字符串value最多可以是512M
4)常用操作
命令 | 作用 |
---|---|
get < key> | 查询对应键值 |
set < key> < value> | 添加键值对 |
append < key> < value> | 将给定的追加到原值的末尾 |
strlen < key> | 获取值的长度 |
senx < key> < value> | 只有在key 不存在时设置key的值 |
incr < key> | 将key中存储的数字值增1(只能对数字值操作,如果为空,新增值为1) |
decr < key> | 将key中存储的数字值减1(只能对数字之操作,如果为空,新增值为-1) |
incrby /decrby < key> | 步长 将key中存储的数字值增减,自定义步长 |
mset < key1> < value1> < key2> < value2> | 同时设置一个或多个key-value对 |
mget < key1> < key2> < key3> | 同时获取一个或多个value |
msetnx < key1> < value1> < key2> < value2> | 同时设置一个或多个key-value对,当且仅当所有给定的key都不存在 |
getrange < key> < 起始位置> < 结束位置> | 获得值的范围,类似java中的substring |
setrange < key> < 起始位置> < value> | 用< value>覆盖< key>所存储的字符串值,从<起始位置>开始 |
setex < key> < 过期时间> < value> | 设置键值的同时,设置过去时间,单位秒 |
getset < key> < value> | 以新换旧,设置了新值的同时获取旧值 |
5)详说 incr key 操作的原子性
- 所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。
- 在单线程中, 能够在单条指令中完成的操作都可以认为是" 原子操作",因为中断只能发生于指令之间。
- 在多线程中,不能被其它进程(线程)打断的操作就叫原子操作。
- Redis单命令的原子性主要得益于Redis的单线程
3List
1)单键多值
2)Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素导列表的头部(左边)或者尾部(右边)。
3)它的底层实际是个双向链表,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差
4)
5)常用操作
命令 | 作用 |
---|---|
lpush/rpush < key> < value1> < value2> | 从左边/右边插入一个或多个值。 |
lpop/rpop < key> | 从左边/右边吐出一个值。值在键在,值光键亡。 |
rpoplpush < key1> < key2> | 从< key1>列表右边吐出一个值,插到< key2>列表左边 |
lrange < key> < start> < stop> | 按照索引下标获得元素(从左到右) |
lindex < key> < index> | 按照索引下标获得元素(从左到右) |
llen < key> | 获得列表长度 |
linsert < key> before < value> < newvalue> | 在< value>的后面插入< newvalue> 插入值 |
lrem < key> < n> < value> | 从左边删除n个value(从左到右) |
4 Set
1)Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的
2)Redis的Set是string类型的无序集合。它底层其实是一个value为null的hash表,所以添加,删除,查找的复杂度都是O(1)。
3)常用操作
命令 | 作用 |
---|---|
sadd < key> < value1> < value2> … | 将一个或多个 member 元素加入到集合 key 当中,已经存在于集合的 member 元素将被忽略。 |
smembers < key> | 取出该集合的所有值。 |
sismember < key> < value> | 判断集合< key>是否为含有该< value>值,有返回1,没有返回0 |
scard < key> | 返回该集合的元素个数。 |
srem < key> < value1> < value2> … | 删除集合中的某个元素。 |
spop < key> | 随机从该集合中吐出一个值。 |
srandmember < key> < n> | 随机从该集合中取出n个值。不会从集合中删除 |
sinter < key1> < key2> | 返回两个集合的交集元素。 |
sunion < key1> < key2> | 返回两个集合的并集元素。 |
sdiff < key1> < key2> | 返回两个集合的差集元素。 |
5 Hash
1)Redis hash 是一个键值对集合
2)Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。
3)类似Java里面的Map<String,Object>
4)分析一个问题: 现有一个JavaBean对象,在Redis中如何存?
- 第一种方案: 用户ID为key ,VALUE为JavaBean序列化后的字符串
缺点: 每次修改用户的某个属性需要,先反序列化改好后再序列化回去。开销较大
- 第二种方案: 用户ID+属性名作为key, 属性值作为Value.
缺点: 用户ID数据冗余
- 第三种方案: 通过 key(用户ID) + field(属性标签) 就可以操作对应属性数据了,既不需要重复存储数据,也不会带来序列化和并发修改控制的问题
5)常用操作
命令 | 作用 |
---|---|
hset < key> < field> < value> | 给< key>集合中的 < field>键赋值< value> |
hget < key1> < field> | 从< key1>集合 取出 value |
hmset < key1> < field1> < value1> < field2> < value2>… | 批量设置hash的值 |
hexists key < field> | 查看哈希表 key 中,给定域 field 是否存在。 |
hkeys < key> | 列出该hash集合的所有field |
hvals < key> | 列出该hash集合的所有value |
hincrby < key> < field> < increment> | 为哈希表 key 中的域 field 的值加上增量 increment |
hsetnx < key> < field> < value> | 将哈希表 key 中的域 field 的值设置为 value ,当且仅当域 field 不存在 |
6 zset (sorted set)
1)Redis有序集合zset与普通集合set非常相似,是一个没有重复元素的字符串集合。不同之处是有序集合的每个成员都关联了一个评分(score) ,这个评分(score)被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以是重复了 。
2)因为元素是有序的, 所以你也可以很快的根据评分(score)或者次序(position)来获取一个范围的元素。访问有序集合的中间元素也是非常快的,因此你能够使用有序集合作为一个没有重复成员的智能列表。
3)常用操作
命令 | 作用 |
---|---|
zadd < key> < score1> < value1> < score2> < value2>… | 将一个或多个 member 元素及其 score 值加入到有序集 key 当中 |
zrange < key> < start> < stop> [WITHSCORES] | 返回有序集 key 中,下标在< start> < stop>之间的元素,带WITHSCORES,可以让分数一起和值返回到结果集。 |
zrangebyscore key min max [ withscores ] [limit offset count] | 返回有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。有序集成员按 score 值递增(从小到大)次序排列。 |
zrevrangebyscore key max min [withscores] [limit offset count] | 同上,改为从大到小排列。 |
zincrby < key> < increment> < value> | 为元素的score加上增量 |
zrem < key> < value> | 删除该集合下,指定值的元素 |
zcount < key> < min> < max> | 统计该集合,分数区间内的元素个数 |
zrank < key> < value> | 返回该值在集合中的排名,从0开始。 |
四、三种特殊数据类型
五、Redis的相关配置
1)计量单位说明,大小写不敏感
2)include
类似jsp中的include,多实例的情况可以把公用的配置文件提取出来
3)ip地址的绑定 bind
- 默认情况bind=127.0.0.1只能接受本机的访问请求
- 不写的情况下,无限制接受任何ip地址的访问
- 生产环境肯定要写你应用服务器的地址
- 如果开启了protected-mode,那么在没有设定bind ip且没有设密码的情况下,Redis只允许接受本机的相应
4)tcp-backlog
- 可以理解是一个请求到达后至到接受进程处理前的队列.
- backlog队列总和=未完成三次握手队列 + 已经完成三次握手队列
- 高并发环境tcp-backlog 设置值跟超时时限内的Redis吞吐量决定
5)timeout
一个空闲的客户端维持多少秒会关闭,0为永不关闭。
6)tcp keepalive
对访问客户端的一种心跳检测,每个n秒检测一次,官方推荐设置为60秒
7)daemonize
是否为后台进程
8)pidfile
存放pid文件的位置,每个实例会产生一个不同的pid文件
9)log level
四个级别根据使用阶段来选择,生产环境选择notice 或者warning
10)log level
日志文件名称
11)syslog
是否将Redis日志输送到linux系统日志服务中
12)syslog-ident
日志的标志
13)syslog-facility
输出日志的设备
14)database
设定库的数量 默认16
15)security
在命令行中设置密码
16)maxclient
最大客户端连接数
17)maxmemory
设置Redis可以使用的内存量。一旦到达内存使用上限,Redis将会试图移除内部数据,移除规则可以通过maxmemory-policy来指定。如果Redis无法根据移除规则来移除内存中的数据,或者设置了“不允许移除”,
那么Redis则会针对那些需要申请内存的指令返回错误信息,比如SET、LPUSH等。
18)Maxmemory-policy
- volatile-lru:使用LRU算法移除key,只对设置了过期时间的键
- allkeys-lru:使用LRU算法移除key
- volatile-random:在过期集合中移除随机的key,只对设置了过期时间的键
- allkeys-random:移除随机的key
- volatile-ttl:移除那些TTL值最小的key,即那些最近要过期的key
- noeviction:不进行移除。针对写操作,只是返回错误信息
19)Maxmemory-samples
设置样本数量,LRU算法和最小TTL算法都并非是精确的算法,而是估算值,所以你可以设置样本的大小。
一般设置3到7的数字,数值越小样本越不准确,但是性能消耗也越小。
六、Redis的Java客户端 Jedis
1)Jedis所需要的jar包 ,可通过Maven的依赖引入
Commons-pool-1.6.jar
Jedis-2.1.0.jar
2)使用Windows环境下Eclipse连接虚拟机中的Redis注意事项
- 禁用Linux的防火墙:Linux(CentOS7)里执行命令 : systemctl stop firewalld.service
- redis.conf中注释掉bind 127.0.0.1 ,然后 protect-mode no。
3)Jedis测试连通性
package com.itheima;
import org.junit.Test;
import redis.clients.jedis.Jedis;
import java.util.List;
import java.util.Map;
public class JedisTest {
@Test
public void testJedis(){
//1.连接redis
Jedis jedis = new Jedis("127.0.0.1", 6379);
//2.操作redis
// jedis.set("name","itheima");
String name = jedis.get("name");
System.out.println(name);
//3.关闭连接
jedis.close();
}
@Test
public void testList(){
//1.连接redis
Jedis jedis = new Jedis("127.0.0.1", 6379);
//2.操作redis
jedis.lpush("list1","a","b","c");
jedis.rpush("list1","x");
List<String> list1 = jedis.lrange("list1", 0, -1);
for(String s : list1){
System.out.println(s);
}
System.out.println(jedis.llen("list1"));
System.out.println();
//3.关闭连接
jedis.close();
}
@Test
public void testHash(){
//1.连接redis
Jedis jedis = new Jedis("127.0.0.1", 6379);
//2.操作redis
jedis.hset("hash1","a1","b1");
jedis.hset("hash1","a2","a2");
jedis.hset("hash1","a3","b3");
Map<String, String> hash1 = jedis.hgetAll("hash1");
System.out.println(hash1);
System.out.println(jedis.hlen("hash1"));
System.out.println();
//3.关闭连接
jedis.close();
}
}
七、 Redis 事务
1 Redis中事务的定义
Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断
Redis事务的主要作用就是串联多个命令防止别的命令插队
2 multi 、exec、discard
1)从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,至到输入Exec后,Redis会将之前的命令队列中的命令依次执行。
2)组队的过程中可以通过discard来放弃组队。
3 事务中的错误处理
1)组队中某个命令出现了报告错误,执行时整个的所有队列会都会被取消。
2)如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。
4 为什么要做成事务?
1)想想一个场景: 有很多人有你的账户,同时去参加双十一抢购
2)通过事务解决问题
悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁
乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。
5 Redis事务的使用
1)WATCH key[key….]
在执行multi之前,先执行watch key1 [key2],可以监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
2)unwatch
取消 WATCH 命令对所有 key 的监视。
如果在执行 WATCH 命令之后, EXEC 命令或 DISCARD 命令先被执行了的话,那么就不需要再执行 UNWATCH 了。
3)三特性
- 单独的隔离操作
事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。 - 没有隔离级别的概念
队列中的命令没有提交之前都不会实际的被执行,因为事务提交前任何指令都不会被实际执行,也就不存在“事务内的查询要看到事务里的更新,在事务外查询不能看到”这个让人万分头痛的问题 - 不保证原子性
Redis同一个事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚
6 Redis事务 秒杀案例
1)解决计数器和人员记录的事务操作
2)秒杀并发模拟 ab工具
- CentOS6 默认安装 ,CentOS7需要手动安装
- 联网: yum install httpd-tools
无网络: 进入cd /run/media/root/CentOS 7 x86_64/Packages
顺序安装
apr-1.4.8-3.el7.x86_64.rpm
apr-util-1.5.2-6.el7.x86_64.rpm
httpd-tools-2.4.6-67.el7.centos.x86_64.rpm - ab –n 请求数 -c 并发数 -p 指定请求数据文件
-T “application/x-www-form-urlencoded” 测试的请求
3)超卖问题
利用watch监视(乐观锁)解决超卖
代码:
public static boolean doSecKill(String uid,String prodid) throws IOException {
//拼接key
String kcKey = "Seckill:" + prodid + ":kc";
String userKey = "Seckill:" + prodid + ":user";
Jedis jedis = new Jedis("192.168.223.132", 6379);
//监视库存
jedis.watch(kcKey);
//获取库存
String kc = jedis.get(kcKey);
//秒杀还没开始,表示为库存为null
if(kc == null) {
System.out.println("秒杀还没开始");
jedis.close();
return false;
}
//已经秒杀成功,表示为存储uid的set中已经有该用户uid
if(jedis.sismember(userKey, uid)) {
System.out.println("已经秒杀成功,不能重复秒杀");
jedis.close();
return false;
}
//判断库存,若大于0,则减库存,加人,若小于等于0,秒杀结束
if(Integer.parseInt(kc) <= 0) {
System.out.println("秒杀已结束");
jedis.close();
return false;
}
//库存大于0,减库存,加人
Transaction transaction = jedis.multi();
transaction.decr(kcKey);
transaction.sadd(userKey, uid);
List<Object> exec = transaction.exec();
if(exec == null || exec.size() == 0) {
System.out.println("秒杀失败");
jedis.close();
return false;
}
System.out.println("秒杀成功");
jedis.close();
return true;
}
但这样会导致库存遗留
4)请求超时问题
节省每次连接redis服务带来的消耗,把连接好的实例反复利用
连接池参数:
MaxTotal:控制一个pool可分配多少个jedis实例,通过pool.getResource()来获取;如果赋值为-1,则表示不限制;如果pool已经分配了MaxTotal个jedis实例,则此时pool的状态为exhausted。
maxIdle:控制一个pool最多有多少个状态为idle(空闲)的jedis实例;
MaxWaitMillis:表示当borrow一个jedis实例时,最大的等待毫秒数,如果超过等待时间,则直接抛JedisConnectionException;
testOnBorrow:获得一个jedis实例的时候是否检查连接可用性(ping());如果为true,则得到的jedis实例均是可用的;
5)遗留问题
- LUA脚本
Lua 是一个小巧的脚本语言,Lua脚本可以很容易的被C/C++ 代码调用,也可以反过来调用C/C++的函数,Lua并没有提供强大的库,一个完整的Lua解释器不过200k,所以Lua不适合作为开发独立应用程序的语言,而是作为嵌入式脚本语言。
很多应用程序、游戏使用LUA作为自己的嵌入式脚本语言,以此来实现可配置性、可扩展性。这其中包括魔兽争霸地图、魔兽世界、博德之门、愤怒的小鸟等众多游戏插件或外挂 - LUA脚本在Redis中的优势
将复杂的或者多步的redis操作,写为一个脚本,一次提交给redis执行,减少反复连接redis的次数。提升性能。
LUA脚本是类似redis事务,有一定的原子性,不会被其他命令插队,可以完成一些redis事务性的操作
但是注意redis的lua脚本功能,只有在2.6以上的版本才可以使用。 - 利用lua脚本淘汰用户,解决超卖问题。
static String secKillScript ="local userid=KEYS[1];\r\n" +
"local prodid=KEYS[2];\r\n" +
"local qtkey='Seckill:'..prodid..\":kc\";\r\n" +
"local usersKey='Seckill:'..prodid..\":user\";\r\n" +
"local userExists=redis.call(\"sismember\",usersKey,userid);\r\n" +
"if tonumber(userExists)==1 then \r\n" +
" return 2;\r\n" +
"end\r\n" +
"local num= redis.call(\"get\" ,qtkey);\r\n" +
"if tonumber(num)<=0 then \r\n" +
" return 0;\r\n" +
"else \r\n" +
" redis.call(\"decr\",qtkey);\r\n" +
" redis.call(\"sadd\",usersKey,userid);\r\n" +
"end\r\n" +
"return 1" ;
public static boolean doSecKill(String uid,String prodid) throws IOException {
//连接池(该连接池为单例)
JedisPool jedisPool = JedisPoolUtil.getJedisPoolInstance();
//获取一个连接
Jedis jedis = jedisPool.getResource();
String sha1= jedis.scriptLoad(secKillScript);
Object result= jedis.evalsha(sha1, 2, uid,prodid);
String reString=String.valueOf(result);
if ("0".equals( reString ) ) {
System.err.println("已抢空!!");
}else if("1".equals( reString ) ) {
System.out.println("抢购成功!!!!");
}else if("2".equals( reString ) ) {
System.err.println("该用户已抢过!!");
}else{
System.err.println("抢购异常!!");
}
jedis.close();
return true;
}
7 Jedis中的事务
public class TestTransaction {
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
JSONObject jsonObject = new JSONObject();
jsonObject.put("name", "sakura");
jsonObject.put("msg", "hello world");
String info = jsonObject.toJSONString();
// 开启事务
Transaction transaction = jedis.multi();
try {
transaction.set("user1", info);
transaction.set("user2", info);
// 执行事务
transaction.exec();
} catch (Exception e) {
e.printStackTrace();
// 如果出错则放弃事务
transaction.discard();
} finally {
System.out.println(jedis.get("user1"));
System.out.println(jedis.get("user2"));
// 关闭连接
jedis.close();
}
}
}
八、Redis 持久化
Redis提供了2个不同形式的持久化方式 RDB 和 AOF
1 RDB
1)在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里。
2)备份是如何执行的
Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。
3)关于fork
在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,Linux中引入了“写时复制技术”,一般情况父进程和子进程会共用同一段物理内存,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。
4)RDB保存的文件
在redis.conf中配置文件名称,默认为dump.rdb
5)RDB文件的保存路径
默认为Redis启动时命令行所在的目录下,也可以修改
6)RDB的保存策略
7)手动保存快照
save: 只管保存,其它不管,全部阻塞
bgsave:按照保存策略自动保存
8)RDB的相关配置
- stop-writes-on-bgsave-error yes
当Redis无法写入磁盘的话,直接关掉Redis的写操作 - rdbcompression yes
进行rdb保存时,将文件压缩 - rdbchecksum yes
在存储快照后,还可以让Redis使用CRC64算法来进行数据校验,但是这样做会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能
9)RDB的备份 与恢复 - 备份:先通过config get dir 查询rdb文件的目录 , 将*.rdb的文件拷贝到别的地方
- 恢复: 关闭Redis,把备份的文件拷贝到工作目录下,启动redis,备份数据会直接加载。
10)RDB的优缺点 - 优点: 节省磁盘空间,恢复速度快.
- 缺点: 虽然Redis在fork时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能。 在备份周期在一定间隔时间做一次备份,所以如果Redis意外down掉的话,就
会丢失最后一次快照后的所有修改
2 AOF
1)以日志的形式来记录每个写操作,将Redis执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,Redis启动之初会读取该文件重新构建数据,换言之,Redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。
2)AOF默认不开启,需要手动在配置文件中配置
3)可以在redis.conf中配置文件名称,默认为 appendonly.aof
AOF文件的保存路径,同RDB的路径一致
4)AOF和RDB同时开启,redis听谁的?
5)AOF文件故障备份
AOF的备份机制和性能虽然和RDB不同, 但是备份和恢复的操作同RDB一样,都是拷贝备份文件,需要恢复时再拷贝到Redis工作目录下,启动系统即加载
6)AOF文件故障恢复
如遇到AOF文件损坏,可通过
redis-check-aof --fix appendonly.aof 进行恢复
7)AOF同步频率设置
- 始终同步,每次Redis的写入都会立刻记入日志
- 每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失。
- 把不主动进行同步,把同步时机交给操作系统。
8)Rewrite - AOF采用文件追加方式,文件会越来越大为避免出现此种情况,新增了重写机制,当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集.可以使用命令bgrewriteaof。
- Redis如何实现重写
AOF文件持续增长而过大时,会fork出一条新进程来将文件重写(也是先写临时文件最后再rename),遍历新进程的内存中数据,每条记录有一条的Set语句。重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,这点和快照有点类似。 - 何时重写
重写虽然可以节约大量磁盘空间,减少恢复时间。但是每次重写还是有一定的负担的,因此设定Redis要满足一定条件才会进行重写。
系统载入时或者上次重写完毕时,Redis会记录此时AOF大小,设为base_size,如果Redis的AOF当前大小>= base_size +base_size*100% (默认)且当前大小>=64mb(默认)的情况下,Redis会对AOF进行重写。
9)AOF的优缺点
- 优点:
备份机制更稳健,丢失数据概率更低。
可读的日志文本,通过操作AOF稳健,可以处理误操作。 - 缺点:
比起RDB占用更多的磁盘空间
恢复备份速度要慢
每次读写都同步的话,有一定的性能压力。
3 RDB和AOF 用哪个好
- 官方推荐两个都启用。
- 如果对数据不敏感,可以选单独用RDB
- 不建议单独用 AOF,因为可能会出现Bug。
- 如果只是做纯内存缓存,可以都不用
九、发布订阅
十、Redis主从复制(补充)
1 什么是主从复制
主从复制,就是主机数据更新后根据配置和策略,自动同步到备机的master/slaver机制,Master以写为主,Slave以读为主。
2 主从复制的目的
1)读写分离,性能扩展
2)容灾快速恢复
3)
3 主从配置
1)原则: 配从不配主
2)步骤: 准备三个Redis实例,一主两从
拷贝多个redis.conf文件include
开启daemonize yes
Pid文件名字pidfile
指定端口port
Log文件名字
Dump.rdb名字dbfilename
Appendonly 关掉或者换名字
3)info replication 打印主从复制的相关信息
4)slaveof < ip> < port> 成为某个实例的从服务器
4 一主二从模式演示
1)相关问题:
- 切入点问题?slave1、slave2是从头开始复制还是从切入点开始复制?比如从k4进来,那之前的123是否也可以复制
- 从机是否可以写?set可否?
- 主机shutdown后情况如何?从机是上位还是原地待命
- 主机又回来了后,主机新增记录,从机还能否顺利复制
- 其中一台从机down后情况如何?依照原有它能跟上大部队吗?
5 复制原理
1)旧复制原理
- 每次从机联通后,都会给主机发送sync指令
- 主机立刻进行存盘操作,发送RDB文件,给从机
- 从机收到RDB文件后,进行全盘加载
- 之后每次主机的写操作,都会立刻发送给从机,从机执行相同的命令
2)新复制原理
6 心跳检测
7 薪火相传模式演示
1)上一个slave可以是下一个slave的Master,slave同样可以接收其他slaves的连接和同步请求,那么该slave作为了链条中下一个的master, 可以有效减轻master的写压力,去中心化降低风险.
中途变更转向:会清除之前的数据,重新建立拷贝最新的
风险是一旦某个slave宕机,后面的slave都没法备份
2)反客为主(小弟上位)
当一个master宕机后,后面的slave可以立刻升为master,其后面的slave不用做任何修改。
用 slaveof no one 将从机变为主机。
3)哨兵模式 sentinel (推举大哥)
反客为主的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库.
配置哨兵
- 调整为一主二从模式
- 自定义的/myredis目录下新建sentinel.conf文件
- 在配置文件中填写内容
sentinel monitor mymaster 127.0.0.1 6379 1
其中mymaster为监控对象起的服务器名称, 1 为 至少有多少个哨兵同意迁移的
数量。 - 启动哨兵
执行redis-sentinel /myredis/sentinel.conf
8 故障恢复
十一 哨兵
十二、Redis集群
问题:
1)容量不够,redis如何进行扩容?
2)并发写操作, redis如何分摊?
1 什么是集群
1)Redis 集群实现了对Redis的水平扩容,即启动N个redis节点,将整个数据库分布存储在这N个节点中,每个节点存储总数据的1/N。
2)Redis 集群通过分区(partition)来提供一定程度的可用性(availability): 即使集群中有一部分节点失效或者无法进行通讯, 集群也可以继续处理命令请求
2 集群方案
3 安装ruby环境
1)能上网:
执行yum install ruby
执行yum install rubygems
2)不能上网:
-
cd /run/media/root/CentOS 7 x86_64/Packages 获取如下rpm包
-
拷贝到/opt/rpmruby/目录下,并cd到此目录
-
执行:rpm -Uvh *.rpm --nodeps –force 按照依赖安装各个rpm包
-
按照依赖安装各个rpm包
-
执行在opt目录下执行 gem install --local redis-3.2.0.gem
4 准备6个Redis实例
1)准备6个实例 6379,6380,6381,6389,6390,6391
拷贝多个redis.conf文件
开启daemonize yes
Pid文件名字
指定端口
Log文件名字
Dump.rdb名字
Appendonly 关掉或者换名字
2)再加入如下配置
cluster-enabled yes 打开集群模式
cluster-config-file nodes-端口号.conf 设定节点配置文件名
cluster-node-timeout 15000 设定节点失联时间,超过该时间(毫秒),集群自动进行主从切换
5 合体
1)将6个实例全部启动,nodes-端口号.conf文件都生成正常
2)合体
- 进入到 cd /opt/redis-3.2.5/src
- 执行
./redis-trib.rb create --replicas 1
192.168.31.211:6379 192.168.31.211:6380 192.168.31.211:6381
192.168.31.211:6389 192.168.31.211:6390 192.168.31.211:6391 - 注意: IP地址修改为当前服务器的地址,端口号为每个Redis实例对应的端口号.
6 集群操作
1)以集群的方式进入客户端
redis-cli -c -p 端口号
2)通过cluster nodes 命令查看集群信息
3)redis cluster 如何分配这六个节点
一个集群至少要有三个主节点。
选项 --replicas 1 表示我们希望为集群中的每个主节点创建一个从节点。
分配原则尽量保证每个主数据库运行在不同的IP地址,每个从库和主库不在一个IP地址上。
4)什么是slots
- 一个 Redis 集群包含 16384 个插槽(hash slot), 数据库中的每个键都属于这 16384 个插槽的其中一个, 集群使用公式 CRC16(key) % 16384 来计算键 key 属于哪个槽, 其中 CRC16(key) 语句用于计算键 key 的 CRC16 校验和 。
- 集群中的每个节点负责处理一部分插槽。 举个例子, 如果一个集群可以有主节点, 其中:
节点 A 负责处理 0 号至 5500 号插槽。
节点 B 负责处理 5501 号至 11000 号插槽。
节点 C 负责处理 11001 号至 16383 号插槽
5)在集群中录入值 - 在redis-cli每次录入、查询键值,redis都会计算出该key应该送往的插槽,如果不是该客户端对应服务器的插槽,redis会报错,并告知应前往的redis实例地址和端口.
- redis-cli客户端提供了 –c 参数实现自动重定向。
如 redis-cli -c –p 6379 登入后,再录入、查询键值对可以自动重定向。 - 不在一个slot下的键值,是不能使用mget,mset等多键操作。
- 可以通过{}来定义组的概念,从而使key中{}内相同内容的键值对放到一个slot中去
6)查询集群中的值 - CLUSTER KEYSLOT < key> 计算键 key 应该被放置在哪个槽上。
- CLUSTER COUNTKEYSINSLOT < slot> 返回槽 slot 目前包含的键值对数量
- CLUSTER GETKEYSINSLOT < slot> < count> 返回 count 个 slot 槽中的键
7)故障恢复 - 如果主节点下线?从节点能否自动升为主节点?
- 主节点恢复后,主从关系会如何?
- 如果所有某一段插槽的主从节点都当掉,redis服务是否还能继续?
redis.conf中的参数 cluster-require-full-coverage
7 集群的Jedis开发
public class JedisClusterTest {
public static void main(String[] args) {
Set<HostAndPort> set =new HashSet<HostAndPort>();
set.add(new HostAndPort("192.168.31.211",6379));
JedisCluster jedisCluster=new JedisCluster(set);
jedisCluster.set("k1", "v1");
System.out.println(jedisCluster.get("k1"));
}
}
8 Redis集群的优缺点
- 优点
实现扩容
分摊压力
无中心配置相对简单 - 缺点
多键操作是不被支持的
多键的Redis事务是不被支持的。lua脚本不被支持。
由于集群方案出现较晚,很多公司已经采用了其他的集群方案,而代理或者客户端分片的方案想要迁移至redis cluster,需要整体迁移而不是逐步过渡,复杂度较大。