Redis学习笔记
文章目录
一、NoSQL数据库简介
1、解决问题
如何解决分布式下的session问题?
- 问题描述:假如有一个用户请求了某功能,Nginx第一次通过负载均衡,将这个请求分给了A服务器,此时存储用户信息的session位于A服务器中,而当用户再次请求时,Nginx将请求分给了B服务器,但此时B服务器没有session信息,如何解决这个问题?
- 解决方案:
- 方案一:将用户信息存储到客户端cookie中,但是这种做法不安全
- 方案二:session复制,将A服务器中的session复制给其他服务器,但是浪费时空间,并且数据冗余
- 方案三:存储到数据库中,但是存在大量IO操作,影响性能
- 方案四:存储到NoSQL数据库中,好处是不需要经过IO操作,完全可以存储在内存中,读写很快
2、NoSQL数据库
- 概念:Not Only SQL,泛指非关系型数据库,以key-value的形式存储。
- 不遵循SQL标准,不支持ACID,但是可以支持事务,查询效率高于SQL。
- 适用场景:
- 对数据高并发的读写
- 大量数据的读写
- 对数据的高可扩展性
- 不适用场景:
- 需要支持事务
- 包含复杂关系的查询
- 常用的NoSQL数据库
- Memcache:不支持持久化,支持的类型单一
- Redis:可以持久化,几乎覆盖了Memcache的所有功能,并有自己独特的功能
- MongoDB:文档型数据库,存储结构类似json,支持二进制数据和大型对象的存储
二、Redis6概述和安装
1、安装
- 进入Redis官网
- 点击即可下载
- 安装c语言的环境
sudo apt install gcc
- 解压下载好的Redis压缩文件
tar -zxvf redis-x.x.x.tar.gz
- 进入redis目录,执行make命令
make
- 安装
make install
whereis
- 其他安装方式
sudo add-apt-repository ppa:redislabs/redis
sudo apt-get update
sudo apt-get install redis
ps -ef |grep redis
whereis redis
2、注意事项
只支持Linux版本,因此不考虑在Windows下安装Redis。
3、Redis使用
- 前台启动
redis-server
- 后台启动
- 打开redis目录(我的是/etc/redis/),目录中有一个配置文件redis.conf
- 将配置文件中的deamonize设置为yes
- 命令启动并查看进程
redis-server /etc/redis/redis.conf
ps -ef | grep redis
- 用客户端访问并验证
redis-cli
ping
- redis关闭
- redis-cli shutdown
- 多实例关闭:redis-cli -p <端口号> shutdown
4、其他介绍
- 6379端口从何而来
- Alessia Merz中Merz在九键键盘中的位置
- 数据库相关
- 默认有0-15,共16个数据库
- 初始默认使用0号库
- 使用**select **来切换数据库
- 密码统一管理,所有库密码相同
- 使用dbsize查看当前数据库key的数量
- 使用flushdb清空当前库
- 使用flushall通杀全部库
- 单线程+多路IO复用
- 与Memcache不同,Memcache使用了多线程+锁的机制,而Redis使用单线程+多路IO复用
三、常用五大数据类型
1、Redis的key操作
- **keys ***查看当前库中的所有key值
- **exists **查看当前库中是否含有这个key,返回结果为包含的记录数
- **type **查看该key的数据类型
- **del **删除某个key
- **expire
- **ttl **查看某个key距离过期剩余的时间,-1表示永不过期,-2表示已经过期
2、Redis字符串String
- String类型是二进制安全的,这就意味着可以用来存储图片,或者序列化对象
- String类型的value最大可以是512M
- **set **添加一条数据,如果重复设置相同的key,则会覆盖之前的key
- **append **在key的value值后面追加数据,返回值为新数据的长度
- **strlen **获取key对应的value长度
- **setnx **添加一条数据,只有在key不存在的时候,才能设置成功
- **incr **将key对应的value中的数字值加1,要求value为字符串格式的纯数字
- **decr **将key对应的value中的数字值减1,要求value为字符串格式的纯数字
- **incrby **将key对应的value增加increment
- **decrby **将key对应的value减少decrement
- 注意:redis操作是原子性的,不会被其他进程或线程打断
- 对于java而言,如果有两个线程分别对0进行++100次,最后结果的取值范围是多少?答:2-200
- **mset **批量添加数据
- **mget **批量获取数据
- **msetnx **批量添加数据,当且仅当所有给定的key都不存在
- **getrange **获取key对应value中start到end之间的字符串,类似于Java中的subString
- **setrange **覆盖写offset之后的数据,value有多长,就覆盖多少位
- **setex **设置键值对的同时,设置过期时间
- **getset **设置新值的同时获取旧值
- 底层数据结构是简单动态字符串(Simple Dynamic String,缩写SDS),底层类似Java的ArrayList
3、Redis列表List
-
底层是双链表(实际上是快速链表),当元素比较少的时候,会使用连续的区域存储,称为压缩链表,当数据很多时,会将多个压缩链表链接起来,叫快速链表
-
一个字符串列表,单键多值,可以添加元素到头部或尾部
-
**lpush/rpush **向列表的左端/右端添加一个或多个值
-
**lpop/rpop **在某个键的左端/右端弹出一个值,会使该值消失,当值都不存在的时候,键也就不存在了
-
**lrange **查看某个键对应的值,按照索引下标查看该值得某些元素,当start=0,end=-1时,表示查看全部元素
-
**rpoplpush **从key1右边弹出一个值,压到key2的左边
-
**lindex **按照index下标获取元素,列表的下标从0开始
-
**llen **获取某个键对应的值的长度
-
**linsert before **在value前面插入newvalue
-
**lrem **从左到右删除n个value
-
**lset **将key下标为index的值替换成value
4、Redis集合Set
- 与list相似,可以去重,但是无序,底层为value=null的哈希表,添加、删除、查找的时间复杂度为O(1)
- **sadd **添加一个或多个值,如果已经存在key,则会被自动忽略掉
- **smembers **取出key对应的value中的所有值
- **sismember **判断key对应的集合中是否包含value,如果存在返回1,否则返回0
- **scard **返回该集合的元素个数
- **srem **删除key对应的集合中的某些值
- **spop **随机从key对应的集合中弹出一个值,这个值会从集合中删除
- **srandmember **随机从该集合中取出n个值,这些值不会从集合中删除
- **smove **把src集合中的value值移动到dst集合中
- **sinter **返回两个集合的交集元素
- **sunion **返回两个集合的并集元素
- **sdiff **返回两个集合的差集元素(属于key1,不属于key2的)
5、Redis哈希Hash
- Redis Hash是一个键值对集合,value是一个string类型的field和value的映射表,很适合存储对象
- **hset **给key集合中的field键赋值value
- **hget **取出key集合中field键对应的值
- **hmset **批量设置hash的值
- **hexists **判断给定的key集合中是否存在field键
- **hkeys **列出该hash集合中所有field
- **hvals **列出该hash集合中所有value
- **hincrby **为哈希表key中的域field的值加上增量increment
- **hsetnx **将哈希表key中的域field的值设置为value,当且仅当域field不存在
6、Redis有序集合Zset
- 与Set不同的是,Zset中的元素是有序的,Zset中的每个成员关联了一个评分(score),评分是排序标准,集合的成员是唯一的,但是评分是可以重复的
- **zadd **将一个或多个member元素及其score值加入到有序集key中
- **zrange [withscores]**返回有序集key中下标在start和end之间的元素,带WITHSCORES,可以让分数一起和值返回到结果集。
- **zrangebyscore [withscores] [limit offset count]**返回有序集key中,所有score值介于min和max之间([min, max])的成员,顺序为从小到大
- **zrevrangebyscore [withscores] [limit offset count]**同上,顺序为从大到小
- **zincrby **为元素的score加上增量
- **zrem **删除key集合下指定的元素
- **zcount **统计该集合中,分数区间内的元素个数
- **zrank **返回该value在集合中的排名,排名从0开始
- Zset底层使用了哈希表和跳跃表,其中哈希表的键存放value,值存放score,跳跃表用来排序,根据score对列表进行排序
四、Redis6配置文件详解
1、bind
bind 127.0.0.1表示,只支持linux本地连接redis,注释掉可以远程连接redis。
2、protected-mode
表示开启保护模式,yes表示只支持本机访问,no表示可以远程访问。
3、tcp-backlog
4、timeout
在timeout秒之内没有对redis操作的时候,redis连接会关闭,默认为0,表示永不超时。
5、tcp-keepalive
redis是通过tcp连接的,tcp会每tcp-keepalive秒进行一次检测,检测有没有对redis进行操作,如果没有则会关闭连接。
6、daemonize
是否允许redis在后台运行,yes表示允许。
7、pidfile
redis在运行时,会把端口号设置到一个文件中,这个属性表示该文件的存放路径。
8、loglevel
日志级别。
9、databases
redis数据库的个数。
五、Redis6的发布和订阅
1、概念
Redis的发布和订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者接受(sub)消息。客户端可以订阅任意多的频道。
2、订阅
3、发布
六、Redis6新数据类型
1、Bitmaps
- 不是一个数据类型,实际上就是一个字符串
- value是一个可以进行位操作的字符串,可以把Bitmaps想象成一个数组,数组中只能存储0和1,数组的下标在Bitmaps中叫偏移量
- **setbit **设置key中offset偏移量的值为value
- **getbit **获取key中offset对应的值
- **bitcount **统计key对应的value中1的个数,end=-1表示到最后一个,end=-2,表示到倒数第二个
- bitop对两个Bitmaps进行位操作
2、HyperLogLog
- 用于解决一些基数问题
- **pfadd **添加一个或多个值,可以去重
- **pfcount **统计key集合中元素的个数
- **pfmerge **将key1和key2的集合进行合并,合并结果放在key里面
3、Geospatial
- 元素到二维坐标,在地图上是经纬度
- Redis基于该类型提供了经纬度设置、查询、范围查询、距离查询、经纬度Hash等操作
- **geoadd **添加一个或多个地理位置
- geopos 取出key集合中某个元素到经纬度
- **geodist [m/km/ft/mi]**获取member1和member2之间的直线距离
七、Jedis操作Redis6
1、Jedis依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.7.0</version>
</dependency>
2、连通性测试
private static final Jedis JEDIS = new Jedis("192.168.83.75", 6379);
public static void main(String[] args) {
String value = JEDIS.ping();
System.out.println(value);
}
3、常用操作
- key操作
@Test
public void test01() {
// 删除一个键值对
JEDIS.del("k1");
// 添加一个键值对
JEDIS.set("k1", "523");【
// 查询所有key
Set<String> keys = JEDIS.keys("*");
for (String key : keys) {
System.out.println(key);
}
// 查询key的个数
System.out.println(keys.size());
// 判断是否存在某个key
System.out.println(JEDIS.exists("k1"));
// 查看key的存活时间
System.out.println(JEDIS.ttl("k1"));
// 查看key的value
System.out.println(JEDIS.get("k1"));
}
- List操作
@Test
public void test02() {
JEDIS.del("list1");
// 添加一个列表
JEDIS.lpush("list1", "Lucy", "Mary", "Jack");
// 打印列表
List<String> list1 = JEDIS.lrange("list1", 0, -1);
for (String s : list1) {
System.out.println(s);
}
// 弹出一个值
String name = JEDIS.lpop("list1");
System.out.println(name);
}
- Set操作
@Test
public void test03() {
JEDIS.del("set1");
// 添加一个set集合
JEDIS.sadd("set1", "Lucy", "Mary", "Jack");
// 打印一个Set集合
Set<String> set1 = JEDIS.smembers("set1");
for (String s : set1) {
System.out.println(s);
}
// 删除Set集合中的某个元素
JEDIS.srem("set1", "Lucy");
// 打印删除后的Set集合
Set<String> set2 = JEDIS.smembers("set1");
for (String s : set2) {
System.out.println(s);
}
}
- Hash操作
@Test
public void test04() {
JEDIS.del("user");
// 添加记录
Map<String, String> map = new HashMap<>(16);
map.put("name", "Lucy");
map.put("age", "20");
map.put("gender", "female");
JEDIS.hset("user", map);
// 查询记录
String name = JEDIS.hget("user", "name");
System.out.println(name);
}
- Zset操作
@Test
public void test05() {
JEDIS.del("zset1");
// 添加记录
Map<String, Double> map = new HashMap<>(16);
map.put("Java", 500.0);
map.put("C++", 400.0);
map.put("Python", 300.0);
JEDIS.zadd("zset1", map);
// 查询记录
Set<String> zset1 = JEDIS.zrange("zset1", 0, -1);
for (String s : zset1) {
System.out.println(s);
}
}
4、验证码功能
要求:
- 输入手机号,点击发送后随机生成6位验证码,2分钟有效
- 输入验证码,点击验证,返回成功或失败
- 每个手机号每天只能输入3次
八、Redis6整合SpringBoot
1、添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
2、配置项目配置文件
# Redis服务器地址
spring.redis.host=192.168.83.75
# Redis服务器连接端口
spring.redis.port=6379
# Redis数据库索引
spring.redis.database=0
# Redis连接超时时间(毫秒)
spring.redis.timeout=1800000
# Redis连接池最大连接数(使用负值表示没有限制)
spring.redis.lettuce.pool.max-active=20
# Redis最大阻塞等待时间(使用负值表示没有限制)
spring.redis.lettuce.pool.max-wait=-1
# Redis连接池中最大空闲连接
spring.redis.lettuce.pool.max-idle=5
# Redis连接池中最小空闲连接
spring.redis.lettuce.pool.min-idle=0
3、添加Redis配置类
/**
* @author StarKing
*/
@EnableCaching
@Configuration
public class RedisConfig {
/**
* 配置redisTemplate
* 默认情况下的模板只能支持 RedisTemplate<String,String>,
* 只能存入字符串,很多时候,我们需要自定义 RedisTemplate ,设置序列化器
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
Jackson2JsonRedisSerializer<Object> 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、测试
/**
* @author StarKing
*/
@CrossOrigin
@RestController
@RequestMapping("/assemble")
public class AssembleController {
@Autowired
private RedisTemplate redisTemplate;
@RequestMapping("/test")
public String test() {
// 设置值到Redis
redisTemplate.opsForValue().set("assemble", "true");
Object assemble = redisTemplate.opsForValue().get("assemble");
return (String) assemble;
}
}
九、Redis6事务操作
1、概念
Redis事物是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
Redis事务的主要作用就是串联多个命令防止别的命令插队。
2、Multi、Exec、Discard命令
从输入Milti命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec后,Redis会将之前的命令队列中的命令依次执行。组队过程中,可以通过Discard命令来放弃组队。
3、命令使用
4、错误处理
- 当组队阶段出现错误时,整个队列都会被取消,所有命令都不会被执行。
-
当执行阶段出现错误时,只有出错的命令不会被执行,其他的会被执行
5、事务冲突
- 解决方案
-
乐观锁命令
- **watch **用来监视一个或多个key
- 应当在开启事务(执行multi)之前执行该命令
- 如果事务在执行(exec)之前,这个key被其他命令所改动,那么事务将会被打断
- **unwatch **取消对一个或多个key的监视
-
演示
-
终端1:
-
终端2:
-
6、Redis事务三大特性
-
单独的隔离操作
事务中所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
-
没有隔离级别的概念
队列中的命令在没有提交之前都不会被执行,因为事务提交前任何指令都不会被实际执行。
-
不保证原子性
事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚。
7、秒杀案例
- 单线程下的代码
private boolean doKill(String userID, String prodID) {
userID = "user_" + userID;
// 商品数量
String prodCount = "prod_" + prodID + "_count";
// 商品清单
String boughtList = "prod_" + prodID + "_bought";
Object o = redisTemplate.opsForValue().get(prodCount);
if (o == null) {
// 秒杀还未开始
System.out.println("秒杀未开始!");
return false;
}
// 秒杀已经开始
// 判断用户有没有重复秒杀
Set<Object> members = redisTemplate.opsForSet().members(boughtList);
if (members != null) {
// 购物清单存在
Boolean isMember = redisTemplate.opsForSet().isMember(boughtList, userID);
if (isMember == null || isMember) {
// 如果用户已经买过,返回false
System.out.println("用户" + userID + "已经完成过秒杀!");
return false;
}
}
int curCount = (int) o;
// 如果已经售空,则返回false
if (curCount <= 0) {
System.out.println("商品已被秒杀完!");
return false;
}
// 开启事务,开始秒杀
// 商品数减1
redisTemplate.opsForValue().decrement(prodCount);
// 添加商品到用户购物车
redisTemplate.opsForSet().add(boughtList, userID);
// 提交事务
System.out.println("用户" + userID + "秒杀成功!");
return true;
}
-
存在的问题
在并发的情况下,会出现超卖问题。同时Redis有可能无法处理多个连接请求,因此会出现连接超时问题。
-
超卖和超时问题的解决
超卖问题可以采用同步机制解决
超时问题可以用Redis连接池来解决
十、Redis6持久化
1、两种方式
- RDB(Redis DataBase)
- AOF(Append Of File)
2、RDB
-
RDB是什么?
在指定的时间间隔内将内存中的数据集快照写入磁盘(Snapshot快照文件),恢复的时候,将快照文件读入到内存。
-
如何执行?
Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件(dump.rdb)。整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加高效。RDB的缺点是最后一次持久化后的数据可能丢失。
-
RDB相关配置
- RDB相关配置在redis.conf的SNAPSHOTTING块中
- dbfilename:持久化文件的文件名
- dir:持久化文件的路径
- stop-writes-on-bgsave-error:当Redis无法写入磁盘时,直接关闭写操作
- rdbcompression:持久化文件是否进行压缩存储
- rdbchecksum:检查数据完整性
- save:save秒钟写操作次数如果超过指定的次数,则进行一次持久化操作,手动保存
-
RDB的优点
- 适合大规模的数据恢复
- 对数据的完整性和一致性要求不高时,比较适用
- 节省磁盘空间
- 恢复速度快
-
RDB的缺点
- Fork的时候会被克隆一份相同的数据,大致2倍的膨胀性需要考虑
- 虽然Redis在fork时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能
- 在备份周期在一定间隔时间做一次备份,所以如果Redis意外down掉的话,就会丢失最后一次快照后的所有修改。
-
如何停止持久化?
动态停止RDB:redis-cli config set save “”,save后给空值,表示禁用保存策略
-
RDB的备份
拷贝rdb文件。
3、AOF
-
AOF是什么?
以日志的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话,就根据日志文件袋内容将写指令,从前到后执行一次,以完成数据的恢复工作。
-
AOF持久化流程
- 客户端的请求写命令会被append追加到AOF缓冲区;
- AOF缓冲区根据AOF持久化策略[always,everysec,no]将操作sync同步到磁盘的AOF文件中;
- AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量;
- Redis服务重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的。
-
AOF默认不开启
-
修改redis.conf文件中的appendonly为yes,开启AOF;
-
可以在redis.conf中配置文件名称,默认为appendonly.aof;
-
AOF的保存路径与RDB的路径一致;
-
重启后配置生效。
-
-
AOF和RDB同时开启,Redis采用什么策略?
系统默认取AOF的数据,因为数据不会存在丢失。
-
异常恢复
- 修改默认的appendonly no 为yes
- 如遇到AOF文件损坏,通过redis-check-aof --fix appendonly.aof进行恢复
- 恢复:重启redis,然后重新加载
-
AOF同步频率设置
- appendfsync always,始终同步,每次写入操作都会记入日志,性能较差但完整性较好
- appendfsync everysec,每秒记入一次,如果宕机可能丢失最后一秒的数据
- appendfsync no,同步时机交给操作系统
-
rewrite重写压缩
-
只记录最后的结果值操作,将多条命令简化。
-
重写条件:文件大小及基准值
-
- 优点
- 备份机制更稳健,丢失数据概率更低
- 可读的日志文件
- 缺点
- 比RDB占用空间多
- 恢复速度慢
- 同步时存在性能压力
十一、Redis6的主从复制
1、简介
主机数据更新后,根据配置和策略,自动同步到备机的master/slaver机制,Master以写为主,Slave以读为主。一主多从。
好处:
读写分离:主服务器只进行写操作,从服务器只进行读操作。
容灾的快速恢复:当某一台从服务器挂掉的时候,可以从其他的从服务器读取数据。
2、配置一主多从
- 创建一个目录myredis存放各个Redis配置文件
- 复制一份配置文件到该目录下
- 创建新配置文件redis6379.conf
- 编辑redis6379.conf,内容如下:
- 同样创建redis6380.conf、redis6381.conf
- 分别启动三个配置文件
- 启动结果如下:
- 使用info replication打印主从复制的相关信息
- 在从机上使用命令**slaveof **命令将该机设置为端口号为port的主机的从机
3、三大问题
-
一主两从
- 从服务器挂掉时:过了某段时间后,该机又重新启动,此时该机会成为主服务器,需要重新设置该机为从服务器,但是数据不会有不同步、不一致的问题。
- 主服务器挂掉时:从服务器可以知道主服务器挂掉了,自己还是从服务器。主服务器重启后,不需要从服务器做任何事情。
-
薪火相传
- 从服务器也可以有从服务器,但是从服务器的从服务器不属于主服务器的从服务器。
- 优点:有效减轻了master主机的写压力,降低中心化的风险
- 缺点:一旦中间某个主机宕机,后续从机都无法得到备份
-
反客为主
某个主服务器宕机时,可以在从机上通过slaveof no one命令将该从机设置为主机。
4、复制原理
- 从服务器连接上主服务器之后,从服务器会向主服务器发送请求数据同步的消息;
- 主服务器收到消息后,主服务器会将数据先进行持久化,然后将rdb文件发送给从服务器,从服务器会读取该rdb文件;
- 每次主服务器进行写操作之后,会和从服务器进行同步。
5、哨兵模式
- 概念:反客为主自动版,自动监控主机是否故障,如果故障则根据投票数自动将从服务器转为主服务器。
- 使用步骤:
- 在/myredis下新建sentinel.conf文件
- 在该文件中写一句sentinel monitor ,count为哨兵数量
- 启动哨兵:redis-sentinel sentinel.conf
- 主服务器恢复后,只能作从服务器。
十二、Redis6的集群
1、概念
- 存在的问题:
- 容量不够时,Redis如何扩容?
- 并发写操作很多时,Redis如何分摊?
- 主从复制、薪火相传、主机宕机导致的IP地址发生变化时,程序中配置需要对应修改,如何解决?
- 解决方案:由主机代理方式转为无中心化集群配置。
- 主机代理:客户端访问代理服务器,代理服务器寻找对应的Redis服务,进行写操作。
- 无中性化集群:各个服务器都可以作为访问入口,各个服务器可以相互访问到。
2、集群搭建
搭建一个三主三从的集群,主机端口号为6379、6380、6381,从机端口号为6389、6390、6391。
当然在工作中,每个Redis服务都会各自占用一台服务器。
-
编写redis6379.conf
-
复制其他配置文件
-
配置每个文件中的端口号等属性
-
启动六个Redis服务
-
将六个节点合成集群
redis-cli --cluster create --cluster-replicas 1 172.25.191.198:6379 172.25.191.198:6380 172.25.191.198:6381 172.25.191.198:6389 172.25.191.198:6390 172.25.191.198:6391
其中,–cluster-replicas表示选择搭建方式,1表示以最简单的方式搭建集群。
如果有以上的效果,表示已经完成了三主三从的集群搭建。
-
连接集群,进行测试
使用命令**redis-cli -c -p **连接集群。
使用cluster nodes查看集群信息。
3、集群如何分配节点?
- 一个集群中至少要有三个主节点。
- –cluster-replicas 1 表示我们希望为集群中的每个主节点创建一个从节点。
- 分配原则:尽量保证每个主数据库运行在不同的IP地址,每个从数据库和主库不在一个IP地址
4、什么是slots?
搭建集群时,成功后会显示一句**[OK] All 16384 slots covered**,这里的16384就是slot的个数,数据库中的每一个键都属于这16384个插槽的其中一个。
集群使用公式**CRC16(key) %**来计算键key属于哪个槽。
集群中每个节点负责处理一部分插槽,例如A节点负责0-5460,B节点负责5461-10922,C节点负责10923-16383。当使用set命令向数据库中插入值的时候,会用这个set的key先进行计算,然后根据计算结果,确定在哪个服务器中加入数据。
5、常用命令
- **cluster keyslot **查询某个key的slot值
- **cluster countkeysinslot **查询slot对应位置中有多少个值,注意查询前需要转到对应的服务器
6、故障恢复
主机挂掉之后,从机会上位。主机重启后,会变为从机。
如果主从机都挂掉了,集群能否正常工作取决于cluster-require-full-coverage,如果该值为yes,表示某个节点挂掉之后,整个集群会瘫痪;如果该值为no,表示只有该节点不能提供服务。
7、Jedis的集群开发
/**
* @author StarKing
*/
public class ColonyTest {
public static void main(String[] args) {
// 端口号可以是任意一个,因为是无中心化的
HostAndPort hostAndPort = new HostAndPort("172.25.191.198", 6379);
JedisCluster jedisCluster = new JedisCluster(hostAndPort);
// jedisCluster包含了所有的操作
jedisCluster.set("k1", "v1");
}
}
8、好处和不足
- 好处
- 实现了Redis扩容
- 分摊了并发压力
- 无中心化配置,相对简单
- 不足
- 不支持多键操作,如多键添加等
- 多键的Redis事务不被支持
十三、Redis6的应用问题解决
1、缓存穿透
- 问题描述
- 当应用服务器压力突然变大时,Redis缓存命中率降低,导致服务器需要不断查询数据库,数据库压力增大。此时缓存内部依旧平稳运行,但是查询的数据都是在数据库中。
- Redis查询不到数据,或者出现很多非正常URL访问时,会有缓存穿透的现象
- 解决方案
- 对空值缓存:如果一个查询返回的数据为空,那么依然将这个空结果缓存,但设置空结果的过期时间很短,最长不超过五分钟。
- 设置可访问名单:使用bitmaps定义一个可访问名单,名单id作为bitmaps的偏移量,每次访问和bitmaps里面的id进行比较,如果不在bitmaps里面,则进行拦截。
- 采用布隆过滤器
- 实时监控Redis:监控Redis的命中率,需要排查访问的对象和访问的数据,从而设置黑名单限制服务。
2、缓存击穿
- 问题描述
- 数据库访问压力瞬时增加,出现崩溃情况,但Redis里面没有出现大量key过期,Redis仍然正常运行。
- Redis某个热门key过期,大量访问使用这个key
- 解决方案
- 预先设置热门数据:在Redis访问高峰之前,把一些热门数据提前存入到Redis里面,加大这些热门数据的存活时间。
- 实时调整:现场监控哪些数据热门,实时调整这些数据的存活时间。
- 使用锁:如果查询缓存,得到结果为空,则设置锁,让其不断查询,知道查询到结果为止。
3、缓存雪崩
- 问题描述
- 服务器压力变大,服务器崩溃
- 在极少的时间段内,查询大量key的集中过期情况,Redis命中率降低,则访问数据库压力增大。
- 解决方案
- 构造多级缓存
- 使用锁或队列:保证不会有大量的线程对数据库进行一次性的读写,不适用高并发场景。
- 设置过期标志更新缓存:记录过期时间,并倒计时,当这个过期时间结束后,通知后台线程更新缓存内容。
- 将缓存失效时间分散开:比如将一个键的失效时间设置为5分01秒,另外一个设置为5分02秒,以此类推,这样就不会出现同时失效的情况。
4、分布式锁
-
问题描述
单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下单并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题,就需要一种跨JVM的互斥机制来控制共享资源的访问。
-
解决方案:
- 基于数据库实现分布式锁
- 基于缓存(Redis等)实现——性能最高
- 基于Zookeeper——可靠性最高
-
使用Redis实现分布式锁
- 方式一:通过setnx 设置锁,只有当释放(del )锁后,其他操作者才可以进行操作。但是这种方法存在问题,如果某个操作者一直占用锁,那么将会陷入无期限阻塞。
- 方式二:在方案一点基础上,设置锁的过期时间(expire ),可以解决无期限阻塞问题。此时,无法保证原子性,即setnx和expire命令不一定一起执行,可能会被打断。
- 方式三:使用命令set nx ex ,在设置键值的同时上锁,并且设置了存活时间。
-
代码实现
@CrossOrigin
@RestController
@RequestMapping
public class DistributedController {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@GetMapping("/request")
public String request() throws InterruptedException {
// setnx获取锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "10", 3, TimeUnit.SECONDS);
if (lock != null && lock) {
// 如果获取到锁
// 执行操作
System.out.println("执行了相关操作!");
// 释放锁
redisTemplate.delete("lock");
} else {
// 如果没有获取到锁,即锁被其他线程占用
TimeUnit.SECONDS.sleep(1);
return request();
}
return "";
}
}
上面的代码存在锁误删问题:假如有两台服务器,第一台获取到锁之后,进行了一段时间的卡顿,假如卡顿了4秒,而锁的存活时间为3秒,因此在第3秒的时候,锁被释放,此时B服务器获取到了该锁,需要后续进行2秒的操作,但是操作到第1秒的时候,A服务器将锁手动释放掉,也就是说此时B服务器的锁就会被释放。
锁误删问题解决方案:使用UUID解决,保证每个服务器释放的都是自己的锁,即释放锁的时候进行判断,判断当前的UUID和要释放掉UUID是否一致,如果一致才释放,需要注意的是,必须保证这个判断是一个原子操作。
那么如何实现该判断是原子操作?我们引入LUA脚本,LUA脚本是一款嵌入式语言,它的操作可以保证原子性。由此我们得到最终版本:
@CrossOrigin
@RestController
@RequestMapping
public class DistributedController {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@GetMapping("/request")
public String request() throws InterruptedException {
// setnx获取锁
String uuid = UUID.randomUUID().toString();
// 商品ID
int prodId = 10;
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock" + prodId, uuid, 3, TimeUnit.SECONDS);
if (lock != null && lock) {
// 如果获取到锁
// 执行操作
System.out.println("执行了相关操作!");
// 使用LUA脚本释放锁
String lua = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
// 使用Redis执行LUA脚本
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(lua);
redisScript.setResultType(Long.class);
redisTemplate.execute(redisScript, Collections.singletonList("lock" + prodId), uuid);
} else {
// 如果没有获取到锁,即锁被其他线程占用
TimeUnit.SECONDS.sleep(1);
return request();
}
return "true";
}
}