Redis内容总结
Redis相关内容
Redis是单线程的,它是基于内存操作,CPU不是Redis的性能瓶颈,他的瓶颈是根据机器的内存和网络宽带,
由于可以使用单线程实现,所以就使用了单线程。
一、Redis的命令
特别多,可以参考官网:https://redis.io/commands
中文网:http://www.redis.cn/commands.html
1、常用命令
1、启动Redis服务:进入/usr/local/bin目录 输入命令:redis-server kconfig/redis.conf 这是设置启动时执行哪个配置文件
2、连接服务:进入Redis的bin目录输入命令:redis-cli -h 连接的主机地址(如果是本地可省略) -p 端口(默认6379)
3、查看连接状态:连接redis服务后,输入命令:ping
4、存字符串值:set 名字 值
5、取值:get key名字
6、查看所有的键:keys *
7、关闭服务:shutdown
8、退出客户端:exit
9、切换数据库(有16个数据库):select 第几个数据库 例如:select 2
10、清空当前数据库值:flushdb
11、清空所有数据库值:flushall
12、判断某key是否存在:exist key名字
13、移除某个键:move key名字
14、设置某键过期时间:expire key名字
15、查看某键的剩余时间:ttl key名字
16、查看当前键的类型:type key名字
2、String类型
1、向某key后面添加一些内容,如果当前key不存在,则新建当前key:append key名字
2、在原来的数据上增加1:incr key名字 类似 i++
3、在原来的数据上减1:decr key名字 类似 i--
4、设置原数据增加多少:decrby key名字 数字 譬如:decrby name 5 表示key为name的数据在原来基础上增加了5
5、截取字符串:getrange key名字 从哪里截取 到哪里结束截取 譬如:getrange name 0 2 表示将key为name的数据从第一个数据开始截取,一直截取到第三个数据
6、获取全部字符串:getrange key名字 0 -1 类似 get key名字
7、替换指定位置的字符串:setrange key名字 从哪里替换 替换内容 譬如:有一个数据:key=a value=1234 执行命令: setrange a 1 xx 查看a结果:1xx4
分布式锁中经常使用
注意点:setnx是一个原子性操作,要么同时操作成功,要么同时设置失败!
8、创建一个key并设置过期时间(如果当前key存在会覆盖原来的):setex key名字 结束时间(单位是秒) key的值
9、当前key存在时设置key值(会创建失败!):setnx key名字 key值
10、当前key不存在时设置key值(不会创建失败!)setnx key名字 key值
11、连续创建多个key:mset key名字1 key值1 key名字2 key值2 ....
12、获取多个key的值:mget key名字1 key名字2 ....
13、设置多个值,如果某个值存在了,则所有值设置失败:mstnx key名字1 key值1 key名字2 key值2 ....
14、设置一个User对象:set user:1 {name:zhangsan,age:12} 其中user:1 中的1表示设置user对象的id
15、获取User对象中name值和age值:mget user:1:name user:1:age 也可以一个一个获取user中的属性值
16、设置User对象方式二:mset user:1name lisi user:1:age 23
先get然后set
17、如果不存在key,返回nil:getset key名字 key值
18、如果key存在,先获取原来的值,在设置新的值:getset key名字 key值
3、List类型
基本数据类型,列表。栈、队列、阻塞队列都可以通过他实现。
127.0.0.1:6379> lpush list one #将一个或多个值插到列表头部(左)
(integer) 1
127.0.0.1:6379> lpush list two
(integer) 2
127.0.0.1:6379> lpush list three
(integer) 3
127.0.0.1:6379> lrange list 0 -1 #获取list中所有的值
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> lrange list 0 1 # 获取list中0 到 1的数据
1) "three"
2) "two"
127.0.0.1:6379> rpush list right #将一个或多个值插到列表尾部部(右)
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
3) "one"
4) "right"
127.0.0.1:6379>
========================================================
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
3) "one"
4) "right"
127.0.0.1:6379> lpop list #移出list中第一个值
"three"
127.0.0.1:6379> rpop list #移除list中最后一个值
"right"
127.0.0.1:6379> lindex list 1 #通过下标获得list中某一个值
"one"
=============================================================
127.0.0.1:6379>llen list #返回列表长度
(integer) 2
============================================================
127.0.0.1:6379>lrem list 1 one #移除list中值为one(移除一个,如果有多个值,可以自己把1变为其他数字)
==============================================================
127.0.0.1:6379>
通过下标截取指定的长度,list已经被改变了,被截取了
ltrim mylist 0 -1
4、set类型
set中的值不能重复、没有顺序
1、添加值
-
添加一个set值,名字为myset:sadd myset "hello"
-
添加第二个set值:sadd myset "hello2"
2、查看set中的值
- smembers myset # 获取myset中所有数据
- scard myset #获取myset集合中内容元素的个数
- sismember myset hello #判断hello是否在myset集合中
3、移除值
- srem myset hello # 移除某一个元素为hello
- spop myset #随机移除一个值
4、随机获取一个值
- srandmember myset #后面如果加了一个数字表示随机获取几个值
5、移动集合
- smove myset myset2 "hello" #将myset集合中的hello移动到myset2
6、交集、并集、差集
假如存在两个集合:
key1:a b c
key2:a d e
- sinter key1 key2 #交集:a
- sunion key1 key2 #并集:a b c d e
- sdiff key1 key2 #差集:b d e
5、Hash(类似map集合)
map集合:key-value。本质和String类型没太大区别。它主要用来存数据库中的对象
1、存值
- hset myhash key a #创建一个myhash的Hash集合,存一个值:key为key,value为a。key 和 value可以连续设置
- hset myhash key1 b key2 c #同时创建两个值
2、获取全部数据
- hgetall myhash #获取所有key和value
- hkeys myhash #只获取所有的key
- hvals myhash #只获取所有的value
- hlen myhash #获取myhash集合中key的数量
3、删除指定值
- hdel myhash key #删除myhash中key为key的值
4、判断hash集合中某数据是否存在
- hexists myhash key1 #判断是否存在key1的key,存在返回1,反之返回0
6、zset(有序集合)
在set基础上增加了一个值,表示这个值的序号。主要用来做排行榜
1、增加
- zadd myszset 1 a #在第一个位置添加一个数据
- zadd myszset 2 b 3 c #在第二个和第三位置添加一个值
2、查看值
- zrangebyscore myzset -inf +inf #将myzset集合所有值从小到大展示出来
- zrangebyscore myzset -inf +inf withscores #将myzset集合所有值从小到大展示出来,并且显示值的序号
- zrevrange myhash 0 -1 #查看myhash 集合中所有值,从大到小进行展示
- zrevrange myhash 1 3 #查看myhash 集合中序号为1 到3之间的值
3、移除
- zrem myzset a #移除a元素
4、获取集合中的个数
- zcard myhash
二、Redis三种特殊的数据类型
- geospatial
- Hyperloglog
- Bitmaps
geospatial地理位置
可以推算地址位置信息、两地之间的距离、方圆几里的人.....只有6个命令:
- geoadd
- geodist
- geohash
- geopos
- georadius
- georadiusbymember
查地区地理位置:http://www.jsons.cn/lngcode/
1、添加地理位置geoadd
官网查看:http://www.redis.cn/commands/geoadd.html
参数:key 值(纬度、经度、名称)
-
有效的经度从-180度到180度。
-
有效的纬度从-85.05112878度到85.05112878度
- geoadd china:city 116.40 39.90 beijing #添加一个北京的地理位置
- geoadd china:city 112.98 28.19 changsha #添加一个长沙的地理位置
2、获取地理位置getpos
- getpos china:city beijing
- getpos china:city beijing changsha #获取北京和长沙的地理位置
3、返回两地理位置直接的距离geodist
- geodist china:city beijing changsha km #查看北京到长沙的直线距离,单位是千米(km)
4、查看某一经纬度附近的人georadius
- georadius china:city 110 30 1000 km #查看 以 (经度为110纬度为30 ) 为中心,半径为1000km 内所有的地理位置名字(当前数据库中存在这些位置名字)
- georadius china:city 110 30 1000 km withdist #withdist在原来基础上展示出地理名字及两地之间的距离
- georadius china:city 110 30 1000 km withcoord #withcoord在原来基础上展示出地理名字及地理位置
- 在后面可以添加一个
count 数字表示展示多少条数据
5、找指定位置周围的其他位置georadiusbymember
- georadiusbymember china:city beijing 1000 km #查看beijing周围1000km的其他位置
6、返回一个11个字符的geohash字符串geohash
- geohash china:city beijing changsha #返回的是beijing到changsha经纬度的字符串,如果两个字符串越类似,表示两距离越接近
7、geo底层就是zset,所以我们可以用zset命令来操作geo
1、查看所有地理位置
- zrange china:city 0 -1
2、删除某地理位置
- zrem china:city beijing
Hyperloglog网站计数
1、简介
他是一个计数统计的算法。占用的内存固定,只需要12KB内存。
可以用来做网页的UV(一个人访问网站多次,但还是算作一个人),有0.81%的错误率,可以忽略不计。
2、测试
- pfadd mykey a b c d e f g #创建一组元素
- pfadd mykey2 d f h i j k #创建第二组元素
- pfmerge mykey3 mykey mykey2 #合并两组元素(mykey和mykey2)
- pfcount mykey3 #统计新数据mykey3 的数量
在以后做网站访问计数,如果允许出现容错的话,推荐使用Hyperloglog
如果不允许出现容错,使用set或自己的数据类型
Bitmaps位图
统计用户信息,活跃、不活跃、登录、未登录、打卡、只有两个状态的内容都可以使用Bitmaps
Bitmaps位图,都是以二进制进行记录,只有0和1两个状态
365天 = 365 bit = 46字节
1字节 = 8bit
1、设置用户一周打卡状态
- setbit sign 0 0 # 第一个0表示星期 第二个0表示未打卡,如果是1表示打卡
- setbit sign 1 1
- setbit sign 2 0
- setbit sign 3 0
- setbit sign 4 1
- setbit sign 5 1
- setbit sign 6 0
2、查看用户某一天是否打卡
- getbit sign 3 #查看用户周四是否打卡
- getbit sign 0 #查看用户周一是否打卡
3、查看用户一周打卡次数
- bitcount sign
四、事务
1、简介
Redis单条命令是保证原子性,但是事务不保证原子性
Redis事务本质:一组命令集合,一个事务中所有命令都会被序列化,在事务执行过程中,会按照顺序执行。
Redis事务的一些特性:一次性、顺序性、排他性(执行过程中不允许其他内容干扰)
Redis事务没有隔离级别的概念(不会出现脏读、幻读、不可重复读的概念)
所有命令在事务中,并没有直接被执行,只有发起执行命令的时候才会执行
2、Redis事务三个阶段
- 开启事务
- 命令入队
- 执行事务
3、事务正常开启
- multi #开启事务
命令入队
- set k1 a #添加一个值
- set k2 b #添加一个值
- set k3 c #添加一个值
- get k1 #获取值
- get k2 #获取值
- get k3 #获取值
- exec #执行事务
4、放弃事务
- set k1 a #添加一个值
- set k2 b #添加一个值
- set k3 c #添加一个值
- get k1 #获取值
- get k2 #获取值
- get k3 #获取值
- discard #放弃事务,事务中的命令都不会执行
5、编译异常(代码出问题),
如果出现当前异常,事务中所有命令都不会被执行
6、运行时异常(例如:1/10),
如果存在,那么执行命令的时候,其他命令可以正常执行,错误命令抛出异常。
五、监控
1、悲观锁
- 很悲观,认为什么时候都会出问题,无论做啥事都加锁
2、乐观锁
- 很乐观,认为什么时候都不会出问题,都不加锁 ,更新数据的时候判断下:在此期间是否有人修改过数据
3、测试
1、模拟正常取钱过程
- set money 100 #设置一个金额
- watch money #监视money
- multi #开启事务
- decrby money 20 #花掉20元
- exec #执行事务
2、通过多线程模拟通过乐观锁操作
线程一:
- set money 100 #设置一个金额
- watch money #监视money,加锁
- multi #开启事务
- decrby money 20 #花掉20元
进入线程二:
在线程的事务还没执行完,给money加钱
- set money 120
回到线程一:
当线程二将money金额修改后再提交事务
- exec #执行事务
此时会返回一个空,执行失败:
127.0.0.1:6379> exec
(nil)
3、解决以上问题
当线程一执行失败后:
- unwatch #如果事务执行失败,先解锁
- watch money #加锁,获取新值,再次监视
- multi #开启事务
- decrby money 20 #花掉20元
- exec # 对比监视的值是否发生了变化,如果没变,则执行成功,如果变了,就执行失败,他就会有重复以上动作,重新解锁、加锁执行事务...
六、Jedis
1、简介
Jedis是Redis官方推荐的Java连接工具,是使用Java操作Redis的中间件。
2、idea连接远程Redis
-
修改Redis的配置文件:redis.conf
protected-mode no bind 0.0.0.0 #注释掉原来的 bind 127.0.0.1 -
关闭防火墙:
#关闭防火墙 sudo ufw disable #查看当前防火墙的状态 如果是inactive 说明我们的防火墙已经关闭掉了。 sudo ufw status -
打开6379端口
sudo ufw allow 6379 -
重启Redis服务
#进入/user/local/bin目录输入命令: #其中kconfig/redis.conf 是我设置要启动的Redis配置文件 redis-server kconfig/redis.conf
3、idea中通过Jedis连接Redis
-
创建空的maven项目,并导入依赖:
<dependencies> <!--jedis --> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>3.6.0</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.78</version> </dependency> </dependencies> -
创建一个连接Redis的类,连接远程Redis
package com.gk.text; import redis.clients.jedis.Jedis; public class RedisConnection { public static void main( String[] args ){ //第一个参数指的是远程服务器的ip地址,第二个是Redis默认连接端口号 Jedis jedis = new Jedis("127.0.0.1",6379); //测试是否连接成功,如果输出PONG,表示成功 System.out.println(jedis.ping()); jedis.close();//关闭连接 } }
4、使用jedis完成事务操作
import com.alibaba.fastjson.JSONObject;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
//事务测试
public class TestTransaction {
public static void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1",6379);
jedis.flushDB();
//开启一个事务
Transaction multi = jedis.multi();
try {
JSONObject jo = new JSONObject();
jo.put("name","zhangsan");
jo.put("age","12");
String s = jo.toJSONString();
//存一个user对象
multi.set("user",s);
//获取user对象中的数据
System.out.println(multi.get("user"));
//执行事务
System.out.println(multi.exec());
} catch (Exception e) {
//放弃事务
System.out.println(multi.discard());
e.printStackTrace();
}finally {
System.out.println(jedis.get("user"));
jedis.close();
}
}
}
5、SpringBoot整合Redis
SpringBoot在2.X之后,原来使用的是jedis,后面被替换为lettuce
- jedis:采用的直连,多个线程操作的话,是不安全的,如果想要避开不安全的,使用jedis pool连接池,更像BIO模式
- lettuce:采用netty,示例可以在多个线程中进行共享。不存在线程不安全,可以减少线程数据,更像NIO模式
1、创建springBoot项目(略)
2、响应依赖依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
<scope>provided</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-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
3、添加SpringBoot配置
spring:
redis:
host: 127.0.0.1 #配置远程服务器的ip地址,你也可以配置一些其他参数
4、添加一个配置类:MyRedisConfig.java
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
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;
@Configuration
public class MyRedisTemplate {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer jsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
//过期了
//om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jsonRedisSerializer.setObjectMapper(om);
//配置存的key序列化 :String类型
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
template.setKeySerializer(stringRedisSerializer);
template.setHashKeySerializer(stringRedisSerializer);
template.setValueSerializer(jsonRedisSerializer);
template.setHashValueSerializer(jsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
5、测试
进入SpringBoot的测试类中
@Autowired
private RedisTemplate<String,Object> redisTemplate;
@Test
void contextLoads() {
/*
opsForValue(): 操作字符串
opsForGeo() 操作地图
..... 操作其他的内容,自行查看
*/
//存一个值
redisTemplate.opsForValue().set("name","zhangsan");
//输出存的值
System.out.println(redisTemplate.opsForValue().get("name"));
//获取Redis连接对象
RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
connection.flushDb();
//connection.flushAll();
connection.close();
}
6、Redis工具类
在实际开发中,我们一般会通过工具类来操作redis相关命令。当前工具类操作方式,就是像之前学Redis基础命令一样,调用对应的方法就行。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@Component
public final class RedisUtil {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// =============================common============================
/**
* 指定缓存失效时间
* @param key 键
* @param time 时间(秒)
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}
// ============================String=============================
/**
* 普通缓存获取
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
* @param key 键
* @param delta 要增加几(大于0)
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
* @param key 键
* @param delta 要减少几(小于0)
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
// ================================Map=================================
/**
* HashGet
* @param key 键 不能为null
* @param item 项 不能为null
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
* @param key 键
* @param map 对应多个键值
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并设置时间
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除hash表中的值
*
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
*
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
*
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
// ============================set=============================
/**
* 根据key获取Set中的所有值
* @param key 键
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据value从一个set中查询,是否存在
*
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入set缓存
*
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 将set数据放入缓存
*
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0)
expire(key, time);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取set缓存的长度
*
* @param key 键
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除值为value的
*
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
// ===============================list=================================
/**
* 获取list缓存的内容
*
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
*
* @param key 键
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引 获取list中的值
*
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @param time 时间(秒)
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0)
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0)
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据索引修改list中的某条数据
*
* @param key 键
* @param index 索引
* @param value 值
* @return
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除N个值为value
*
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
}
七、Redis持久化
1、为什么持久化
redis里有10gb数据,突然停电或者意外宕机了,再启动的时候10gb都没了?!所以需要持久化,宕机后再通过持久化文件将数据恢复。
Redis是内存数据库,如果不将内存中的数据库状态保存到磁盘,那么一旦服务器进程退出,服务器中的数据库状态也会消失。所以Redis提供了持久化功能!
Redis持久化方式有两种:
- rdb
- AOF
2、RDB
配置文件位置:进入redis.conf文件===>在SNAPSHOTTING下
- RDB其实就是把数据以快照的形式保存在磁盘上。什么是快照呢,你可以理解成把当前时刻的数据拍成一张照片保存下来。
- RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘。也是默认的持久化方式,这种方式是就是将内存中数据以快照的方式写入到二进制文件中,默认的文件名为dump.rdb。
两种触发机制
1.save
- 同步、阻塞
- 致命的问题,持久化的时候redis服务阻塞(准确的说会阻塞当前执行save命令的线程,但是redis是单线程的,所以整个服务会阻塞),不能继对外提供请求,GG!数据量小的话肯定影响不大,数据量大呢?每次复制需要1小时,那就相当于停机一小时。
2.bgsave
- 异步、非阻塞
- 他可以一边进行持久化,一边对外提供读写服务,互不影响,新写的数据对我持久化不会造成数据影响,你持久化的过程中报错或者耗时太久都对我当前对外提供请求的服务不会产生任何影响。持久化完会将新的rdb文件覆盖之前的。
3、AOF
1.简介
以日志的形式来记录每个写操作,将Redis执行过的所有指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作
2、配置
进入redis.conf文件,在append only mode下(都是大写字母)
默认不开启,需要手动进行配置:只需要将redis.conf文件中的appendonly 改为 no
3、使用
将appendonly修改为no后,重启redis,在/usr/local/bin目录下会出现appendonly.aof文件,可以通过vim查看appendonly.aof文件里面内容(以前在redis中输入的命令)
4、注意
-
如果
appendonly.aof文件有错误(通过vim修改了其中的某些内容),那么在重启redis,连接redis的时候会出现错误:could not connect to Redis at,127.0.0.1:6379: Connection refused -
我们需要修复这个aof文件,在bin目录下,执行命令:
redis-check-aof --fix appendonly.aof
优点:
- 每次修改都同步,文件完整会更加好
- 每秒同步一次,可能会丢失一秒的数据
- 从不同步,效率最高
缺点:
- 相对于数据文件来说,aof远远大于rdb,修复的速度也比rdb慢。
- aof运行效率也比rdb慢,所以redis默认配置就是rdb持久化
八、Redis订阅
Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。
Redis客户端可以订阅任意数量的频道。
1、订阅端
输入命令:
subscribe myredis #订阅一个频道:myredis
# 如果后面发送端,发送消息后,在下面会自动接收消息
#等待读取推送的信息
1) "message” #消息
2) "myredis” #接收的是哪个频道的消息"
3) "he11o,redis" #消息的具体内容
4) (integer) 1
2、发送端
输入命令:
publish myredis "he11o,redis" #发送消息,切换到订阅端后会发现,自动接收到消息:hello,redis
九、Redis主从复制
概念
- 主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(masterleader),后者称为从节点(slave/follower);数据的复制是单向的,只能由主节点到从节点。Master以写为主,Slave以读为主。
- 默认情况下,每台Redis服务器都是主节点;且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。
作用
-
数据冗余
实现了数据的热备份,是持久化之外的一种数据冗余方式。
-
故障恢复
当主节点数据出问题时,可以由从节点提供服务,实现快速的故障恢复,实际上是一种服务冗余
-
负载均衡
配合读写分离,主节点提供写服务,从节点提供读服务,可以大大提高redis服务器的并发量。
-
高可用(集群)基石
主从复制是哨兵和集群实施的基础。
环境配置
1、第一种:一个master(主机)连接另外两个Slave(从节点)
-
开启三个机器(在Xshell上连接三次服务)
-
将redis.conf文件复制三个,redis79.cnf redis80.conf redis81.conf
-
修改每一个配置文件(redis.conf)
-
端口:
三个配置文件分别改为:79、80、81
-
pid名字:
pidfile /var/run/redis_6379.pid pidfile /var/run/redis_6380.pid pidfile /var/run/redis_6381.pid
-
日志名称:
logfile "79.log" logfile "80.log" logfile "81.log"
-
dump.rdb名字:
dbfilename dump79.rdb dbfilename dump80.rdb dbfilename dump81.rdb
-
-
启动上面三个Redis服务端,然后进行连接,查看当前服务Redis服务端信息:
info replication
可以发现每一台Redis服务都是master。
-
认主机(认老大)
一主:redis79.conf
二从:redis80.conf redis80.conf
配置方式(只需要配置从机就可以了)
#分别进入80和81主机,进入redis客户端后输入命令 slaveof 127.0.0.1 6379 #设置谁当主机 #后面可以检查当前redis服务是什么类型的机器: info replication
2、第二种:一个master连接另一个slave,在salve中连接另一个slave
在第一种的基础上,将redis81.conf从机 认 redis80.conf从机 为老大
-
在这种情况下,如果主节点(老大)断开了,此时我们可以让redis80.conf做老大
# 在redis80.conf下 连接redis客户端,输入命令: slaveof no one -
谋朝篡位
如果前面断开的老大后面连上了,此时redis80.conf依旧是老大,如果想回到最初的现象(79为老大,80、81都为从节点),就需要重新配置(让80认79为老大)
注意
- 真实的开发我们应该通过配置文件进行配置,我们当前使用的是命令,所以是暂时的。
- 主机可以写,从机不能写,只能读。主机中的数据会自动同步到其他从机里面去。
- 主机断开连接,从机依旧连接到主机,当不能进行写操作,如果主机回来了,从机依旧可以直接获取到主机写的信息。
- 如果使用命令行配置主从,如果从机重启了,从机就会重新变为主机,只要他重新配置主机,主机中的数据会立刻同步到从机中
通过配置文件进行配置
进入redis.conf文件,进行如下修改
#找到 replicaof <masterip> <masterprot> 被注释了,需要手动解除注释
#如果主机有密码,则 修改masterauth <master-password> 被注释了,需要手动解除注释
原理
- Slave(从机)启动成功连接到master后会发送一个sync同步命令
- Master(主机)接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,master将传送整个数据文件到slave,并完成一次完全同步。
- 全量复制:而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中。
- 增量复制:Master继续将新的所有收集到的修改命令依次传给slave,完成同步
- 但是只要是重新连接master,一次完全同步(全量复制)将被自动执行,我们的数据一定可以在从机中看到!/
十、哨兵模式(自动选老大模式)
产生原因
在Redis的主从复制中,当主服务器宕机后,我们需要手动去切换另一台服务器作为主机,这样做很费时费力,还会造成一段时间服务不可用。
简介
哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。
一个哨兵

配置方式:
-
进入和redis.conf同目录下(我这边是kconfig目录下)
-
创建一个文件
sentinel.conf(一定不要写错了,创建的时候,可以通过vim命令) -
修改
sentinel.conf文件#sentinel monitor 被监控的名称 host port 1 # 1 代表主机挂了,slave投票,看谁接替称为主服务器 sentinel monitor mysentinel 127.0.0.1 6379 1 -
启动
redis-sentinel文件#在/usr/local/bin目录下启动 redis-sentinel 文件 redis-sentinel kconfig/sentinel.conf
测试:
假如主机宕机,Redis会不会自动选举,如果后面主服务器重新练上,会不会还是主机
- 在主服务器连接上redis客户端后,输入
shutdown关闭连接 - 过一下后,Redis重新自动进行了一个master选举,让其他salve节点(从机)作为master节点(主机)。可以通过
info replication查看当前机器为master还是salve - 现在将之前宕机的机器进行恢复(之前master回来了),最后发现,他现在并不是之前的master了,而是一个salve。
多个哨兵

假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程(不会重新选举新的主服务器),仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover[故障转移]操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。
优缺点
优点︰
1、哨兵集群,基于主从复制模式,所有的主从配置优点,它全有
2、主从可以切换,故障可以转移,系统的可用性就会更好
3、哨兵模式就是主从模式的升级,手动到自动,更加健壮!
缺点︰
1、Redis 不好啊在线扩容的,集群容量一旦到达上限,在线扩容就十分麻烦!
2、实现哨兵模式的配置其实是很麻烦的,里面有很多选择!
哨兵模式的全部配置
#哨兵sentine1实例运行的端口默认26379
port 26379
#哨兵sentine1的工作目录
dir /tmp
#哨兵sentine1监控的redis主节点的ip port
#master-name可以自己命名的主节点名字只能由字母A-z、数字0-9、这三个字符".-_"组成。
# quorum 配置多少个sentine1哨兵统一认为master主节点失联那么这时客观上认为主节点失联了
# sentine1 monitor <master-name> <ip> <redis-port> <quorum>
sentine1 monitor mymaster 127.0.0.1 6379 2
#当在Redis实例中开启了requirepass foobaled 授权密码这样所有连接Redis实例的客户端都要提供密码#设置哨兵sentine1连接主从的密码注意必须为主从设置一样的验证密码
# sentinel auth-pass <master-namez <password>
sentinel auth-pass mymaster MySUPER--secret-0123passwOrd
#指定多少毫秒之后主节点没有应答哨兵sentine1此时哨兵主观上认为主节点下线默认30秒
# sentinel down-after-milliseconds <master-name> <mil1iseconds>
sentine1 down-after-mi11iseconds mymaster 30000
#这个配置项指定了在发生failover主备切换时最多可以有多少个slave同时对新的master进行同步,这个数字越小,完成failover所需的时间就越长,但是如果这个数字越大,就意味着越多的s1ave因为replication而不可用。可以通过将这个值设为1来保证每次只有一个slave 处于不能处理命令请求的状态。
# sentine1 para1le1-syncs <master-name> <numslaves>
sentine1 para11e1-syncs mymaster 1
#故障转移的超时时间 failover-timeout可以用在以下这些方面:
#1.同一个sentine1对同一个master两次failover之间的间隔时间。
#2,当一个slave从一个错误的master那里同步数据开始计算时间。直到slave被纠正为向正确的master那里同步数据时。
#3.当想要取消一个正在进行的failover所需要的时间。
#4 .当进行failoveriat,配置所有s1aves指向新的master所需的最大时间。不过,即使过了这个超时,s1aves依然会被正确配置为指向master,但是就不按para7le1-syncs所配置的规则来了
#默认三分钟
# sentinel failover-timeout <master-name> <mi11iseconds>
sentine1 failover-timeout mymaster 180000
# SCRIPTS EXECUTION
#配置当某一事件发生时所需要执行的脚木,可以通过脚木来通知管理员,例如当系统运行不正常时发邮件通知相关人员。#对于脚本的运行结果有以下规则;
#若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认为10
#若脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行。
#如果脚木在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同。
#一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被一个SIGKTLL信号终止,之后重新执行。
#通知型脚本:当sentine1有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等),将会去调用这个脚本,这时这个脚本应该通过邮件,SNS等方式去通知系统管理员关于系统不正常运行的信息。调用该脚本时,将传给脚本两个参数,一个是事件的类型,一个是事件的描述。如果sentine1.conf配置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则sentine1无法正常启动成功。
#通知脚本
#sentinel notification-script <master-name> <script-path>
sentinel notification-script mymaster /var/redis/notify.sh
#客户端重新配置主节点参数脚本
#当一个master由于fai1over而发生改变时,这个脚本将会被调用,通知相关的客户端关于master地址已经发生改变的信息。
#以下参数将会在调用脚本时传给脚本:
# <master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port>
#目前<state>总是“failover",
#<role>是"leader”或者"observer”中的一个。
#参数 from-ip,from-port,to-ip,to-port是用来和旧的master和新的master(即旧的slave)通信的
#这个脚本应该是通用的,能被多次调用,不是针对性的。
# sentinel client-reconfig-script <master-name> <script-path>
sentinel client-reconfig-script mymaster /var/redis/reconfig.sh
十一、缓存雪崩
1、缓存穿透
概念
当用户想要查询一个数据,会先进入缓存中进行查询(Redis),如果缓存中没有,则直接进入数据库中进行查询。此时用户查询的这个数据刚好没有,然后他就一直发送同一个查询,一直查数据库,那么可能会造成一个缓存穿透。案例:秒杀
解决方案
1.布隆过滤器
是一种数据结构,对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合直接丢弃。说白了就是一个过滤器,用来过滤不符合条件的请求
2、缓存空对象
如果当前查询的数据在缓存中、数据库中都没有,就在数据库中加一个空对象,然后将空对象存到缓存中。
但是这种方法会存在两个问题:
- 如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键;
- 即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。
2、缓存击穿(量太大,缓存过期)
概念
- 这里需要注意和缓存击穿的区别,缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
- 当某个key在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访问数据库来查询最新数据,并且回写缓存,会导使数据库瞬间压力过大。
解决方案
1、设置热点数据永不过期
从缓存层面来看,没有设置过期时间不会出现热点key过期后产生的问题,但也会有问题:如果一直缓存,Redis空间可能会满,需要清理一些key,如果设置不过期,就不好清理了。
2、加互斥锁(setnx)
分布式锁∶使用分布式锁,保证对于每个key同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可。这种方式将高并发的压力转移到了分布式锁,因此对分布式锁的考验很大。
3、缓存雪崩
概念
- 缓存雪崩,是指在某一个时间段,缓存集中过期失效。Redis 宕机!
- 产生雪崩的原因之一,比如在写本文的时候,马上就要到双十二零点,很快就会迎来一波抢购,这波商品时间比较集中的放入了缓存,假设缓存一个小时。那么到了凌晨一点钟的时候,这批商品的缓存就都过期了。而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。
双12会停掉一些服务(保证服务的高可用 )
解决方案
1、redis高可用
这个思想的含义是,既然redis有可能挂掉,那我多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。(异地多活)
2、限流降级
这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
3、数据预热
数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。

浙公网安备 33010602011771号