Redis
@Tenone
文章目录
一、Redis 介绍
1、简介
简单来说,Redis 就是一个使用 C 语言开发的数据库,不过与传统数据库不同的是,Redis 的数据存在内存之中,所以他的读写速度非常快,因此被广泛应用于缓存方向
Redis 除了可以做缓存,还可以做分布式锁,甚至是消息队列
2、安装Redis
测试 gcc 版本
gcc --version
下载redis-6.2.6.tar.gz 放入 /opt 目录下
解压 redis-6.2.6.tar.gz ,形成 redis-6.2.6 的文件夹
进入 redis-6.2.6 目录下执行 make 命令(如果出现错误,运行 make distclean,再次执行 make 命令,然后执行 make install 命令)
安装目录在 /usr/local/bin 目录下
此时查看安装目录下的文件
Redis-benchmark :性能测试工具
redis-check-aof :修复有问题的 AOF 文件
redis-check-rdb :修复有问题的 RDB 文件
redis-sentinel :redis 集群使用
redis-server :redis 服务器启动命令
redis-cli :客户端,操作入口
3、后台启动(前台不说了)
拷贝一份 redis.conf 到其他目录,我放在了 ~/myredis 目录下
[root@localhost ~] cp /opt/redis-6.2.6/redis.conf /myredis
客户端访问
redis-server /myredis/redis.conf
ps -ef | grep redis
redis-cli -p 6379
终止进程命令
kill -9 进程号xxxx
4、Redis 涉及技术
默认 16 个数据库,数组下标从 0 开始,初始默认使用 0 号库
可以使用 select 8 来切换数据库
Redis 是单线程+多路 IO 复用技术
多路复用是指使用一个线程来检查多个文件描述符(Socket)的就绪状态,比如调用 select 和 poll 函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞直到超时。得到就绪状态后进行真正的操作可以在同一个线程里执行,也可以启动线程执行
二、常用的数据类型
keys * 查看当前库的所有key
exist key 判断某个key是否存在
type key 产看你的key是什么类型
del key 删除指定的key
expire key 10 为给定的key设置过期时间 10s
ttl key 查看还有多久过期
dbsize 查看当前数据库key的量
1、字符串(String)
String 是 Redis 中最基本的类型,是二进制安全的,意味着 Redis 的 String 可以包含任何数据,比如图片或者序列化对象
常用命令
set
- NX:当数据库中key不存在时,可以将key-value 添加到数据库
- XX:与NX相反
- EX:key的超时秒数
- PX:key的超时毫秒数,与EX互斥
get
append | 在原值末尾追加数据 |
---|---|
strlen | 获取值的长度 |
setnx | 只有在 key 不存在时,设置 key 的值 |
incr | 增 |
decr | 减少 |
incrby / decrby <步长> | 增减步长值 |
mset … | 批量 |
mget … | 批量 |
msetnx … | 仅当所有key不存在,批量 |
getrange | 获得值的范围,前包后包 |
setrange | 从start开始覆盖value |
setex | 设置过期时间value秒 |
getset | 获得旧值,设置新值 |
数据结构
String 的数据结构为简单动态字符串(SDS),内部结构类似于 Java 中的 ArrayList,采用预分配冗余空间的方式减少内存的频繁分配
如上图所示,当前字符串实际分配的空间 capacity 一般高于实际字符串长度 len ,字符串最大不超过512M
2、列表(list)
人们常说的 list 即链表,链表是一种常见的数据结构,特点在于易于数据元素的插入和删除,并且可以灵活调整链表长度,但是链表的随机访问困难,由于 C 语言并没有实现链表,所以 Redis 实现了自己的链表数据结构,Redis 的 list 底层是双向链表,既可以支持反向查找和遍历,更方便操作,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差
常用命令
命令 | 解释 |
---|---|
lpush/rpush … | 从左边/右边插入一个/多个值 |
lpop/rpop | 从左边/右边吐出一个值(值在键在) |
rpoplpush | 从key1列表右边吐出一个,插到key2左边 |
lrange | 按照索引下标获得元素(从左到右) |
lrange mylist 0 -1 | 0,-1表示获得所有 |
lindex | 按照索引下标获得元素(左到右) |
llen | 获得列表长度 |
linsert before | 在value的后面插入newvalue |
lrem | 从左边删除n个value(从左到右) |
lset | 将列表key下标为index的值替换成value |
数据结构
List的数据结构为快速链表quickLIst
首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是ziplist,也就是压缩列表
他将所有的元素紧挨着一起存储,分配的是一块连续的内存
当数据量较多时回变成qucikList快速链表
因为普通的链表需要的附加指针空间太大,会比较浪费空间,比如这个列表里存的只是int类型的数据,结构上还需要两个额外的指针 prev 和 next
Redis 将链表和 ziplist 结合起来组成了 quicklist,也就是将多个ziplist使用双向指针窜起来使用,这样既满足了快速插入删除性能,又不会出现太大的空间冗余
3、集合(set)
Redis set 类似于 Java 中的 HashSet,Redis 中的 set 类型是一种无序集合,集合中的元素没有先后顺序。当你需要存储一个列表数据,又不希望出现重复数据时,set 是一个很好的选择,并且set 提供了判断某个成员是否在一个 set 集合内的重要接口,这个也是 list 所不能提供的,可以基于 set 轻易实现交集、并集、差集的操作
比如:“你可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis 可以方便的实现共同关注、共同粉丝、共同喜好等功能 ‘’
Redis 的 Set 是 String 类型的无序集合,它底层其实是一个 value 为 null 的 hash 表,所以添加、删除、查找的复杂度都是 O(1)
一个算法,随着数据的增加,执行时间的长短,如果是 O(1),数据增加,查找数据的时间不变
常用命令
命令 | 解释 |
---|---|
sadd | 将一个或多个member元素加入key中 |
smembers | 取出该集合的所有值 |
sismember | 判断集合key是否含有value,有1无0 |
scard | 返回该集合的元素个数 |
srem | 删除集合中的某个元素 |
spop | 随机从该集合中吐出一个值(删除) |
srandmember | 随机从该集合中取出n个值(不删除) |
smove value | value把集合中一个值从一个集合移动到其他 |
sinter | 返回两个集合的交集 |
sunion | 返回两个集合的并集 |
sdiff | 返回两个集合的差集(包含key1 不包含key2) |
数据结构
Set 数据结构是dict 字典,字典是使用哈希表实现的
Java 中 HashSet 内部使用的是 HashMap ,只不过所有的 value 都指向同一个对象,Redis 的 Set 结构也是一样,内部使用 Hash 结构,所有的 value 都指向同一个内部值
4、哈希(Hash)
Redis hash 是一个键值对集合
Redis hash 是一个 String 类型的 field 和 value 的映射表,hash 特别适用于存储对象
类似 Java 里的 Map<String,Object>
用户 ID 为查找的 key
方法一:
方法二:
通过key + field 就可以操作对应属性数据,既不需要重复存储数据,也不会带来序列化和并发修改控制的问题
命令 | 解释 |
---|---|
hset | 给 key 集合中的 field 键赋值 value |
hget | 从 key1 的集合field 取值 |
hmset … | 批量设置 hash 的值 |
hexists | 查看哈希表 key 中,field是否存在 |
hkeys | 列出hash集合中国所有field |
hvals | 列出hash集合中所有value |
hincrby | 给key中的field的值加上increment |
hsetnx | 将哈希表key中field的值设为value(field不存在) |
数据结构
Hash类型对应的数据结构是两种,ziplist(压缩列表),hashtable(哈希表),当field-value 长度较短且个数较少时,使用 ziplist ,否则使用 hashtable
5、有序集合(Zset)
又叫sorted set,和 set 相比,**sorted set 增加了一个权重参数 score,使得集合中的元素能够按 score 有序排列,还可以通过 score 的范围来获取元素的列表,**有点像 Java 中 HashMap 和 TreeMap 的集合体,是一个没有重复元素的字符串集合
同时访问有序元素的中间元素也是很快的,因此你能够使用有序集合作为一个没有重复成员的智能列表
常用命令
命令 | 解释 |
---|---|
zadd … | 将一个或多个member及score加入key中 |
zrange | 返回有序集 key 中,下标在两者之间 |
zrangebyscore key min max [withscores] | 返回key score值介于【min,max】之间 |
zincrby | 为score加上增量 |
zrem | 删除该集合下,指定值的元素 |
zcount | 统计该集合,分数区间内的元素个数 |
zrank | 返回该值在集合中的排名,从0开始 |
数据结构
zset 一方面等价于 Java 中的Map<String,Double> ,另一方面又类似于 TreeSet
Zset 底层使用了两个数据结构
- hash,hash的作用就是关联元素 value 和权重 score,保障元素的唯一性,可以通过元素 value 找到相应的 score 值
- 跳跃表,跳跃表的目的在于给元素 value 排序,根据 score 的范围获取元素列表
三、Redis 配置文件介绍
redis.conf 中进行查看
bind:
默认为bind=127.0.0.1,只能接受本机的访问请求
当注释掉之后可以接受任何 ip 地址的访问
protected-mode:
本机访问保护模式 设置为 no
Port:
端口号,默认为 6379
tcp-backlog:
设置 tcp 的 backlog,backlog 其实是一个连接队列,backlog队列总和 = 未完成三次握手队列 + 已完成三次握手队列
在高并发环境下需要一个高backlog值来避免慢客户端连接问题
timeout:
一个空闲的客户端维持多少秒会永久关闭,0为不关闭
tcp-keepalive:
对访问客户端的一种心跳检测,每个 n 秒检测一次
单位为妙,建议设置为60
LIMITS区域#########
maxclients:
设置redis 可以同时和多少个客户端进行连接,默认为 10000 个
maxmemory:
- 设置redis 可以使用的内存量,一旦内存使用上限,redis就会试图移除内部数据,规则可以由maxmemory-policy 指定
- 如果redis 无法移除内存中的数据,那么redis就会对set等命令发出错误信息
- 对于get类别的信息还是会正常响应
maxmemory-policy
内存清除规则
maxmemory-samples
- 设置样本数量,LRU算法和最小TTL
- 一般设置3到7的数字,数值越小越不准确,但性能消耗越小
四、Redis 新数据类型
五、Redis __ Jedis测试
1、Jedis 的连接及 Redis 配置文件
导入 Jedis 所需要的依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.2.0</version>
</dependency>
最简单的测试方法(ping)
public class JedisDemo1 {
public static void main(String[] args) {
Jedis jedis = new Jedis("192.168.195.2",6379);
String ping = jedis.ping();
System.out.println(ping);
}
}
注意事项
修改 redis.conf 中的配置,其中bind 127.0.0.1 注释掉
protected-mode off -> on
vim redis.conf
修改配置文件后都需要重启服务
redis-server /etc/redis.conf
redis-cli -p 6379
查看防火墙并关闭防火墙或开放 redis 端口
systemctl status firewalld
systemctl stop firewalld
firewall-cmd --zone=public --add-port=6379/tcp --permanent
#重启防火墙
firewall-cmd --reload
2、重要 API 在 Jedis 中的使用
key-value、String
@Test
public void test1(){
Jedis jedis = new Jedis("192.168.195.2",6379);
//Jedis-API key-value
jedis.set("name","lucy");
String name = jedis.get("name");
System.out.println(name);
//Jedis-API String
jedis.mset("str1","v1","str2","v2");
System.out.println(jedis.mget("str1","str2"));
Set<String> keys = jedis.keys("*");
for(String key : keys){
System.out.println(key);
}
list
@Test
public void test2(){
//Jedis-API List
Jedis jedis = new Jedis("192.168.195.2",6379);
jedis.lpush("key1","lucy","marry","jack");
List<String> key1 = jedis.lrange("key1", 0, -1);
//[jack, marry, lucy]
System.out.println(key1);
}
set
@Test
public void test3(){
//Jedis-API set
Jedis jedis = new Jedis("192.168.195.2",6379);
jedis.sadd("orders","order01");
jedis.sadd("orders","order02");
jedis.sadd("orders","order03");
jedis.sadd("orders","order04");
jedis.sadd("orders","order05");
jedis.srem("orders","order02","order04");
Set<String> orders = jedis.smembers("orders");
for(String order : orders){
System.out.println(order);
}
}
hash
@Test
public void test4(){
//Jedis-API hash
Jedis jedis = new Jedis("192.168.195.2",6379);
jedis.hset("hash1", "username", "lisi");
String hget = jedis.hget("hash1", "username");
//lisi
System.out.println(hget);
Map<String,String> map = new HashMap<>();
map.put("telphone","2121312312");
map.put("address","辽宁省锦州市");
map.put("email","37472174@qq.com");
jedis.hmset("hash2",map);
List<String> hmget = jedis.hmget("hash2", "address", "email");
for(String hgets : hmget){
//辽宁省锦州市
//37472174@qq.com
System.out.println(hgets);
}
}
zset
@Test
public void test5(){
//Jedis-API zset
Jedis jedis = new Jedis("192.168.195.2",6379);
jedis.zadd("zset01",100d,"z3");
jedis.zadd("zset01",90d,"z4");
jedis.zadd("zset01",80d,"z5");
jedis.zadd("zset01",110d,"z6");
Set<String> zset01 = jedis.zrange("zset01", 0, -1);
for( String zset : zset01 ){
//5、4、3、6
System.out.println(zset);
}
}
六、Redis Jedis 实例(手机验证码)
1、要求
输入手机号,点击发送后随机生成 6 位数字码,2 分钟有效
输入验证码,点击验证,返回成功或失败
每个手机号每天只能输入 3 次
2、实现
import redis.clients.jedis.Jedis;
import java.util.Random;
public class PhoneCode {
public static void main(String[] args) {
verifyCode("13700165002");
// getRedisCode("13700165002","640057");
}
//1、生成六位验证码
public static String getCode(){
Random random = new Random();
StringBuilder stringBuilder = new StringBuilder();
for(int i = 0;i<6;i++){
int rand = random.nextInt(10);
stringBuilder.append(rand);
}
return stringBuilder.toString();
}
//2、每个收集每天只能发送三次,验证码放入redis中,设置过期时间
public static void verifyCode(String phone){
Jedis jedis = new Jedis("192.168.195.2",6379);
//手机发送次数key
String countKey = "VerifyCode"+phone+":count";
//验证码key
String codeKey = "VerifyCode"+phone+":code";
String count = jedis.get(countKey);
if(count == null){
// 没有发送次数,设置为 1
jedis.setex(countKey,24*60*60,"1");
}else if(Integer.parseInt(count)<=2){
jedis.incr(countKey);
}else if(Integer.parseInt(count)>2){
System.out.println("今天发送次数已经超过三次!");
jedis.close();
}
// 将验证码放入 redis 中
String vcode = getCode();
jedis.setex(codeKey,120,vcode);
jedis.close();
}
//3、验证码校验
public static void getRedisCode(String phone,String code){
Jedis jedis = new Jedis("192.168.195.2",6379);
String codeKey = "VerifyCode"+phone+":code";
String redisCode = jedis.get(codeKey);
if(redisCode.equals(code)){
System.out.println("成功!");
}else{
System.out.println("失败!");
}
jedis.close();
}
}
七、使用SpringBoot整合Redis
1、SpringBoot 依赖的导入
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
</parent>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-pool2 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
2、application.properties
spring.redis.host=192.168.195.2
spring.redis.port=6379
3、RedisConfig 配置文件
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @author Xkuna
* @date 2020/11/19 14:05.
*/
//配置类专属注解 并且完成自动注入
@Configuration
public class RedisConfig {
@Bean
//配置redisTemplate
// 默认情况下的模板只能支持 RedisTemplate<String,String>,
// 只能存入字符串,很多时候,我们需要自定义 RedisTemplate ,设置序列化器
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){
RedisTemplate<String,Object> template = new RedisTemplate <>();
template.setConnectionFactory(factory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
4、web层 RedisController
@RestController
public class RedisTestController {
@Autowired
private RedisTemplate redisTemplate;
@RequestMapping("testRedis")
public String testRedis(){
redisTemplate.opsForValue().set("name","lucy");
String name = (String) redisTemplate.opsForValue().get("name");
return name;//lucy
}
}
八、事务、锁机制、秒杀
事务的定义:Redis 事务是一个单独的隔离操作;事务中所有的命令都会序列化,按顺序地执行,事务在执行的过程中,不会被其他客户端发送过来的命令所打断影响
Redis 命令的主要作用就是串联多个命令防止别的命令插队
1、Multi、Exec、discard
Multi:组队阶段
Exec:执行阶段
discard:放弃组队
组队成功:
组队失败:组队提交阶段报错,即提交错误
组队失败:组队阶段没有错误,提交时某个步骤出现错误
Tips:如果执行阶段某个命令出现了错误,则只有报错的命令不会被执行,而其他的命令会执行,不会回滚!
2、悲观锁和乐观锁的引入
事务的冲突:
一个人买东西花了8000
一个人买东西花了5000
一个人买东西花了1000
因为没有加入事务,最后余额-4000
悲观锁:
顾名思议,就是很悲观,没去取到数据后都会认为别人会修改,所以每次拿到数据的时候都会上锁,这样别人想拿到这个数据 就需要block 直到它拿到锁。
传统的关系型数据库中就用到了很多这种锁机制,比如行锁、表锁等,读锁,写锁等都是在操作之前先上锁。
乐观锁:
顾名思义,就是很乐观,每次拿数据都会认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制来进行控制。
乐观锁适用于多读的应用类型,可以提高吞吐量。Redis 就是利用这种 check-and-set 的机制实现事务的。
3、WATCH key【key】
在执行 multi 之前,先执行 watch key1 ,可以监视一个或多个 key ,如果在事务执行之前这个 key 被其他命令所改动,那么事务将被打断。(nil处)
4、UNWATCH
取消 watch 命令对所有 key 的监视
如果在执行 WATCH 命令之后,EXEC 命令或 DISCARD 命令先被执行了的话,那么就不需要再执行UNWATCH 了
5、Redis 事务三特性
-
单独的隔离操作
事务中所有命令都会序列化、按顺序的执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断
-
没有隔离级别的概念
队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行
-
不保证原子性
事务中如果有一条命令执行失败,其后的命令仍然会被执行,不回滚
九、秒杀案例
1、流程
模拟高并发环境
yum install httpd-tools
设置一个模拟表单提交参数,以&结尾;存放当前目录
prodid=0101&
ab -n 2000 -c 200 -k -p ~/ postfile -T application/ x-www-form- url encoded http://192.168.195.2:8081/Seckill/doseckill
2、超卖问题
3、利用乐观锁淘汰用户,解决超卖问题
public class SecKill_redis {
public static void main(String[] args) {
Jedis jedis =new Jedis("192.168.44.168",6379);
System.out.println(jedis.ping());
jedis.close();
}
//秒杀过程
public static boolean doSecKill(String uid,String prodid) throws IOException {
//1 uid和prodid非空判断
if(uid == null || prodid == null) {
return false;
}
//2 连接redis
//Jedis jedis = new Jedis("192.168.44.168",6379);
//通过连接池得到jedis对象
JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
Jedis jedis = jedisPoolInstance.getResource();
//3 拼接key
// 3.1 库存key
String kcKey = "sk:"+prodid+":qt";
// 3.2 秒杀成功用户key
String userKey = "sk:"+prodid+":user";
//监视库存
jedis.watch(kcKey);
//4 获取库存,如果库存null,秒杀还没有开始
String kc = jedis.get(kcKey);
if(kc == null) {
System.out.println("秒杀还没有开始,请等待");
jedis.close();
return false;
}
// 5 判断用户是否重复秒杀操作
if(jedis.sismember(userKey, uid)) {
System.out.println("已经秒杀成功了,不能重复秒杀");
jedis.close();
return false;
}
//6 判断如果商品数量,库存数量小于1,秒杀结束
if(Integer.parseInt(kc)<=0) {
System.out.println("秒杀已经结束了");
jedis.close();
return false;
}
//7 秒杀过程
//使用事务
Transaction multi = jedis.multi();
//组队操作
multi.decr(kcKey);
multi.sadd(userKey,uid);
//执行
List<Object> results = multi.exec();
if(results == null || results.size()==0) {
System.out.println("秒杀失败了....");
jedis.close();
return false;
}
//7.1 库存-1
//jedis.decr(kcKey);
//7.2 把秒杀成功用户添加清单里面
//jedis.sadd(userKey,uid);
System.out.println("秒杀成功了..");
jedis.close();
return true;
}
}
4、使用乐观锁后,可能会出现库存遗留问题
这时候需要使用 LUA 脚本 进行解决,LUA 脚本是一个小巧的脚本语言,脚本语言又被称为扩建的语言,或者动态语言,是一种编程 语言,用来控制软件应用程序,脚本通常以文本(如ASCII)保存,只在 被调用时进行解释或编译
**LUA脚本在 Redis 中的优势:**将复杂的或者多步 Redis 操作写成一个脚本,一次提交给 Redis 执行,减少反复连接 Redis 的次数,提升性能
LUA脚本是类似 Redis 事务,有一定的原子性,不会被其他命令插队。
Redis2.6版本以后,通过LUA脚本解决争抢问题,实际上是 redis利用其单线程的特性,用任务队列的方式解决多任务并发问题
5、Redis连接池
节省每次 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 实例均是可用的
public class JedisPoolUtil {
private static volatile JedisPool jedisPool = null;
private JedisPoolUtil() {
}
public static JedisPool getJedisPoolInstance() {
if (null == jedisPool) {
synchronized (JedisPoolUtil.class) {
if (null == jedisPool) {
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(200);
poolConfig.setMaxIdle(32);
poolConfig.setMaxWaitMillis(100*1000);
poolConfig.setBlockWhenExhausted(true);
poolConfig.setTestOnBorrow(true); // ping PONG
jedisPool = new JedisPool(poolConfig, "192.168.44.168", 6379, 60000 );
}
}
}
return jedisPool;
}
public static void release(JedisPool jedisPool, Jedis jedis) {
if (null != jedis) {
jedisPool.returnResource(jedis);
}
}
}
加入连接池之后:
十、持久化操作
Redis 提供了两个不同形式的持久化方式
- RDB(Redis DataBase)
- AOF(Append Of File)
1、RDB
什么是RDB?
RDB 是指在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的 Snapshot 快照,它恢复时是将快照文件直接读到内存里
1.1 备份是如何执行的?
Redis 会单独创建 (fork) 一个子进程来进行持久化,会 将数据写入到一个临时文件中,待持久化的进程都结束后,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程不进行任何的 IO 操作,这就确保了极高的性能。如果需要大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那 RDB 方式要比 AOF 方式更加的高效。RDB 的缺点是最后一次持久化后的数据可能丢失。
1.2 Fork
Fork 的作用是复制一个与当前进程一样的进程,新进程的一切都与原进程一致,但是却是一个全新的进程,并作为原进程的子进程
在linux 中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会 exec 系统调用,出于效率考虑, Linux 中引入了“写时复制技术”
一般情况下,父进程与子进程共用同一段物理内存
1.3 如何触发RDB快照
命令:save VS bgsave
save:save 只管保存,其他不管,全部阻塞。手动保存
bgsave:Redis 会在后台异步进行快照操作,快照同时还可以响应客户端请求
可以通过 last save 命令获取最后一次成功执行快照的时间
命令:flushall
执行flushall命令,也会产生dump.rdb文件,但是是空的
1.4 优势
- 适合大规模的数据恢复
- 对数据完整性和一致性要求不高更适合使用
- 节省磁盘空间
- 恢复速度快
1.5 劣势
- Fork 的时候,内存中的数据被克隆了一份,大致两倍的膨胀性需要考虑
- 虽然 Redis 在 fork 时使用了 写时拷贝技术,但是如果数据庞大时还是比较消耗性能
- 在备份周期在一定间隔时间做一次备份,所以如果 Redis 意外死机的话,就会丢失最后一次快照后的所有修改
2、AOF
2.1 AOF是什么
以日志的形式来记录每个写操作(增量保存),将 Redis 执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。
2.2 持久化的流程(AOF)
1、客户端的请求写命令会被 append 追加到 AOF 缓冲区内
2、AOF 缓冲区根据 AOF持久化策略将操作 sync 同步到磁盘的 AOF 文件中
3、AOF 文件大小超过重写策略或者手动重写时,回对 AOF 文件 rewrite 重写,压缩 AOF 文件容量
4、Redis 服务重启时,会重新 load 加载 AOF 文件中的写操作达到数据恢复的目的
Tips:当AOF 和 RDB 同时开始,系统默认读取 AOF 的数据(数据不会存在丢失)
2.3 优势
- 备份机制更稳健,丢失数据概率更低
- 可读的日志文件,通过操作 AOF 稳健,可以处理误操作
2.4 劣势
- 比起 RDB 占用更多的磁盘空间
- 恢复备份速度更慢
- 每次读写都同步的话,有一定的性能压力
- 存在个别BUG,造成不能恢复
Tips:如果对数据不敏感,可以单独用 RDB,不建议单独使用 AOF,因为可能有 BUG,如果单纯内存缓存,可以都不用!
十一、Redis主从复制
1、是什么
主机数据更新后根据配置和策略自动同步到备机的master/slaver机制。Master以写为主,Slaver以读为主
2、能干啥
- 读写分离,性能拓展
- 容灾快速恢复
3、一主多从模型搭建
1、在根目录下创建 myredis 文件夹
2、将/etc/redis.conf 复制到 /myredis/redis.conf中
3、进入redis.conf 中将appendonly 关掉(no)
4、创建多个 redis 文件,并相应的写入内容
此处我们创建三个文件,分别为redis6379.conf、redis6380.conf、redis6381.conf
文件中内容为
5、启动三个服务
查看当前运行的状况:info replication,此时三者均为主机
在6380和6381中使用:salveof 127.0.0.1 6379 命令
在 6379中可以发现如下情况,即6380和6381作为6379的从服务器
4、一主二仆
特点:
从服务器宕机后,重启服务后变为主服务器,由 slaveof 后变成从服务器后,将主服务器中的数据从头开始复制,主服务器中有什么,从服务器中就有什么
大哥永远是大哥,它挂了之后重启仍然是大哥
主服务器进行写操作后,从服务器会向主服务器发送数据同步请求
5、薪火相传
增加了“小组长”的概念,一个 slaveof 是下一个 slaveof 的 Master,而原来的 Master 是这一个 Master 的大哥,
此时6379服务器的从服务器只有 6380 ,而6380 的从服务器有 6381
6、反客为主
当一个 Master 宕机后,后面的从服务器可以立刻升级为 master,其后面的 slave 不需要做任何的修改,用 slaveof no one 将葱鸡变为主机
7、主机与从机之间的复制原理
- Slave 启动成功连接到 master 后会发送一个 sync 命令
- Master 接到命令启动后台的存盘过程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,master 将传送整个数据文件到 slave,以完成一次完全同步
- 全量复制:slave 服务在接收到数据库文件数据后,将其存盘并加载到内存中
- 增量复制:Master 服务将继续将新的所有的增加设置命令依次传送给 slave ,完成同步
- 只要一方宕机,重新连接 Master ,一次完全同步(全量复制)被自动执行
8、哨兵模式(sentinel)
在 ~/myredis 下 vim 一个新的文件 ,文件名为 sentinel.conf
在文件中加入如下配置
- mymaster 为监控对象起的服务器的名称
- 1 为至少有多少个哨兵同意迁移的数量
启动哨兵
redis-sentinel /myredis/sentinel.conf
当主机宕机后,从机中会选取出新的主机,根据优先级别:slave-priority
原主机重启后会变成从机
概括为:老大哥进去了,新大哥由哨兵模式选取而出,老大哥出来的时候,只能当小弟
十二、Redis 集群
1、搭建一个集群
首先删除 RDB 文件,防止有干扰
rm -rf. dump63*
2、配置 redis6379.conf 文件,配置如下:
include /myredis/redis.conf
pidfile "/var/run/redis_6379.pid"
port 6379
dbfilename "dump6379.rdb"
cluster-enabled yes
cluster-config-file nodes-6379.conf
cluster-node-timeout 15000
cluster-enabled yes:为打开集群模式
cluster-config-file nodes-6379.conf:为设定节点配置文件名
cluster-node-timeout:为设定节点失联时间,超过该时间,集群自动进行主从切换
并将所有的配置文件修改为自己的端口名6379 -> 6380,可使用命令实现全部替换
:%s/6379/6380
3、启动redis 6个服务
Tips:一定要将以前的进程全部杀掉后再启动服务器
kill -9 xxxx
4、将六台服务器部署为一个集群
使用命令
redis-cli --cluster create --cluster-replicas 1 192.168.195.2:6379 192.168.195.2:6380 192.168.195.2:6381 192.168.195.2:6389 192.168.195.2:6390 192.168.195.2:6391
其中 1为 从机的数量,此处为一主一从
输入yes后提示配置成功,All 16384 slots covered.
5、集群的登陆命令 -c
Redis-cli -c -p 6379
集群的 cluster nodes 命令查看集群信息
6、什么是 slots
slots:插槽,一个Redis 集群中包括 16384 个插槽,数据库中的每个键都属于这 16384 个插槽中的一个
7、在集群中录入值
在 redis-cli 每次录入,查询健值, redis 都会计算出该 key 应该送往的插槽,如果不是该客户端对应服务器的插槽,redis 会报错,并告知应前往的 redis 实例地址和端口
不在一个slot 下的键值,不能使用 mget、mset 等多键操作,但是可以通过 {} 定义组的概念,从而使key 中 {} 相同内容的键值对放到一个 slot 中
8、查询集群中的值
使用命令
cluster keyslot cust
cluster getkeysinslot xxxx xx
xxxx=slot槽的键 count=返回多少个
9、故障恢复
如果主节点宕机,从节点能自动升为主节点
主节点恢复后,主节点回来变成从机
如果某一段插槽的主从都挂掉,而cluster-require-full-coverage 为 yes ,那么整个集群都挂掉
如果某一段插槽的主从都挂掉,而cluster-require-full-coverage 为 no ,那么该插槽数据全部不能使用,也无法存储
10、好处和坏处
好处
- 实现扩容
- 分摊压力
- 无中心配置相对简单
坏处
- 多键操作不被支持
- 多键的事务同样不被支持,且不支持 lua 脚本
- 老项目迁移难度较大
十三、Redis 应用问题解决
1、缓存穿透
用户访问的 key 所对应的数据无法在缓存中获取,这样所有的请求都会堆积到数据库,数据库承受了大量的请求,最终导致数据库崩溃。比如用一个不存在的用户 ID 获取用户信息,缓存和数据库中都没有这个数据,就会将大量请求压到数据库
解决方法
- 对空值缓存
如果一个查询结果返回的数据为空,我们仍然把这个空结果进行缓存,设置空结果的过期时间很短
- 设置可访问的白名单
使用 bitmaps 类型定义一个可以访问的名单,名单 ID 作为 bitmaps 的偏移量,每次访问和 bitmap 里面的 ID 进行比较,如果访问 ID 不在 bitmaps 里面,就进行拦截,不允许访问
- 采用布隆过滤器
就是把可能存在的请求的值都放在布隆过滤器中,当用户的请求过来时,先判断用户发来的请求的值是否存在于布隆过滤器中,如果不存在,直接返回请求参数错误信息给客户端,存在的话才会依次判断缓存及数据库中是否存在对应的数据
布隆过滤器会存在误判的情况,总结来说:布隆过滤器会对元素值通过哈希函数进行计算,根据哈希值放在位数组中并将下标的值置为 1 ,这时,不同的字符串可能会出现相同的哈希值
- 实时监控
人力操作,当发现 Redis 命中率下降时,排查访问对象和访问的数据,设置黑名单
2、缓存击穿
总结为:某个热点 Key 过期的瞬间,有大量的并发请求过来,这些请求发现缓存过期后都会从后端 DB 加载数据并回设缓存,DB 承受了大量的并发容易宕机
解决方法
- 预先设置热门数据
在 Redis 高峰访问之前,把一些热门数据提前存入到 Redis 中,加大这些热点数据的 Key 的时长,防止过期
- 实时调整
现场监控哪些数据热门,适当的调整过期时长
- 使用锁
3、缓存雪崩
总的来说:缓存在同一时间内大面积失效,后面的请求都落在了数据库中,造成数据库短时间内承受了大量的请求,就好比雪崩一样,数据库的压力可想而知
正常访问
缓存失效瞬间
解决方法
- 构建多级缓存架构
Nginx 缓存 + redis 缓存 + 其他缓存(ehcache等)
- 使用锁或队列
用加锁或者队列的方式保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。不适用于高并发
- 设置过期标志更新缓存
记录缓存数据是否过期,如果过期会触发通知另外的线程在后台取更新实际 Key 的缓存
- 将缓存失效时间分散开
在原有的失效时间基础上增加一个随机值,这样一个缓存的过期时间的重复率就会降低,很难印发集体失效的事件
十四、分布式锁
1、问题描述
随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多下城,多进程并且分布在不同的机器上,这使得原单机部署情况下的并发控制锁策略失败,单纯的Java API 不能提供分布式锁的能力。
分布式锁主流的实现方案
- 基于数据库实现分布式锁
- 基于缓存
- 基于 Zookeeper
每一种分布式锁解决方案都有各自的优缺点
性能方面:redis最高
可靠性:zookeeper 最高
2、使用redis 实现分布式锁
set users nx ex 12
设置key users 有锁并且有过期时间