技术分类
Web1.0的时代,数据访问量很有限,用一夫当关的高性能的单点服务器可以解决大部分问题
随着Web2.0的时代的到来,用户访问量大幅度提升,同时产生了大量的用户数据。加上后来的智能移动设备的普及,所有的互联网平台都面临了巨大的性能挑战。
NoSQL( NoSQL = Not Only SQL
),意即不仅仅是 SQL,泛指非关系型的数据库
。NoSQL 不依赖业务逻辑方式存储,而以简单的 key-value
模式存储。因此大大的增加了数据库的扩展能力。
即席
查询用不着sql的和用了sql也不行的情况,请考虑用NoSQL
非常庞大
的表,可以用普通的计算机
处理超过10亿行数据
,还可以处理有数百万列的元素表Apache Cassandra是一款免费的开源NoSQL数据库,其设计目的在于管理由大量商用服务器构建起来的庞大集群上的海量数据集(数据量通常达到PB级别)
。在众多显著特性当中,Cassandra最为卓越的长处是对写入及读取操作进行规范调整,而且其不强调主集群的设计思路能够以相对直观的方式简化各集群的创建于扩展流程
https://db-engines.com/en/ranking
开源
的key-value
存储系统string
(字符串)、list
(链表)、set
(集合)、zset
(sorted set–有序集合)和hash
(哈希类型)原子性
的缓存在内存
中数据写入磁盘
或者把修改操作写入追加的记录文件官网下载:https://redis.io/
make
命令(只是编译好)如果没有准备好C语言编译环境,make会报错,-Jemalloc/jemalloc.h:没有那个文件
解决办法:运行make distclean命令,后在进行make操作
make install
命令进行安装默认:usr/local/bin
查看默认安装目录: redis-benchmark:性能测试工具,可以在自己本子运行,看看自己本子性能如何
redis-check-aof:修复有问题的AOF文件,rdb和aof后面讲
redis-check-dump:修复有问题的dump.rdb文件 redis-sentinel:Redis集群使用
redis-server:Redis服务器启动命令 redis-cli:客户端,操作入口
命令行窗口不能关闭,否则服务器停止
cd /usr/local/bin
redis-server
cp redis.conf /myredis/redis_oa.conf
redis-server /myredis/redis_oa.conf
6379
来切换数据库。如: select 8dbsize
查看当前数据库的key的数量flushdb
清空当前库flushall
通杀全部库Redis是单线程+多路IO复用技术
多路复用是指使用一个线程来检查多个文件描述符(Socket)的就绪状态,比如调用select和poll函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞直到超时。得到就绪状态后进行真正的操作可以在同一个线程里执行,也可以启动线程执行(比如使用线程池。
与Memcache三点不同: 支持多数据类型,支持持久化,单线程+多路IO复用
命令 | 含义 |
---|---|
keys * | 查看当前库所有key(匹配:keys *1) |
exists key | 判断某个key是否存在(返回表示存在,返回0表示不存在) |
type key | 查看你的key是什么类型 |
del key | 删除指定的key数据(立即删除) |
unlink key | 根据value选择非阻塞删除(仅将keys从keyspace元数据中删除,真正的删除会在后续异步操作 ) |
expire key 10 | 10秒钟:为给定的key设置过期时间 |
ttl key | 查看还有多少秒过期,-1表示永不过期,-2表示已过期 |
selec id | 命令切换数据库 |
dbsize | 查看当前数据库的key的数量 |
flushdb | 清空当前库 |
flushall | 通杀全部库 |
二进制安全
的。意味着Redis的string可以包含任何数据。比如jpg图片或者序列化的对象。命令 | 含义 |
---|---|
set
|
添加键值对 |
get
|
查询对应键值 |
append
|
将给定的 追加到原值的末尾 |
strlen
|
获得值的长度 |
setnx
|
只有在 key 不存在时 设置 key 的值 |
incr
|
将 key 中储存的数字值增1 |
只能对数字值操作,如果为空,新增值为1 | |
decr
|
将 key 中储存的数字值减1只能对数字值操作,如果为空,新增值为-1 |
incrby / decrby <步长> |
将 key 中储存的数字值增减。自定义步长。 |
mset
|
同时设置一个或多个 key-value对 |
mget
|
同时获取一个或多个 value |
msetnx
|
同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在 |
getrange <起始位置><结束位置>` |
同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在 |
setrange <起始位置> |
用 覆写 所储存的字符串值,从<起始位置>开始(索引从0开始) |
setex <过期时间>
|
设置键值的同时,设置过期时间,单位秒 |
getset
|
设置键值的同时,设置过期时间,单位秒 |
原子性
所谓原子操作是指不会被线程调度机制打断的操作;
这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。
String的数据结构为简单动态字符串(Simple Dynamic String,缩写SDS)。是可以修改的字符串,内部结构实现上类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配
如图中所示,内部为当前字符串实际分配的空间capacity一般要高于实际字符串长度len。当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时一次只会多扩1M的空间。需要注意的是字符串最大长度为512M
双向链表
,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差命令 | 含义 |
---|---|
lpush/rpush
|
从左边/右边插入一个或多个值 |
lpop/rpop
|
从左边/右边吐出n个值。值在键在,值光键亡 。 |
rpoplpush
|
从 列表右边吐出一个值,插到 列表左边 |
lrange
|
按照索引下标获得元素(从左到右) |
lrange mylist 0 -1 | 0左边第一个,-1右边第一个,(0-1表示获取所有) |
lindex
|
按照索引下标获得元素(从左到右) |
llen
|
获得列表长度 |
linsert before
|
在 的前面插入 插入值 |
lrem
|
从左边删除n个value(从左到右) |
lset
|
将列表key下标为index的值替换成value |
自动排重
的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的它底层其实是一个value为null的hash表
,所以添加,删除,查找的复杂度都是O(1)
命令 | 含义 |
---|---|
sadd
|
将一个或多个 member 元素加入到集合 key 中,已经存在的 member 元素将被忽略 |
smembers
|
取出该集合的所有值 |
sismember
|
判断集合 是否为含有该 值,有1,没有0 |
scard
|
返回该集合的元素个数 |
srem
|
删除集合中的某个元素 |
srem
|
删除集合中的某个元素 |
spop
|
随机从该集合中吐出一个值 |
srandmember
|
随机从该集合中取出n个值。不会从集合中删除 |
smove value |
把集合中一个值从一个集合移动到另一个集合 |
sinter
|
返回两个集合的交集元素 |
sunion
|
返回两个集合的并集元素 |
sdiff
|
返回两个集合的差集 元素(key1中的,不包含key2中的) |
用户ID为查找的key,存储的value用户对象包含姓名,年龄,生日等信息,如果用普通的key/value结构来存储主要有以下2种存储方式:
命令 | 含义 |
---|---|
hset
|
给 集合中的 键赋值
|
hget
|
从 集合 取出 value |
hmset
|
批量设置hash的值 |
hexists
|
查看哈希表 key 中,给定域 field 是否存在 |
hkeys
|
列出该hash集合的所有field |
hvals
|
列出该hash集合的所有value |
hincrby
|
为哈希表 key 中的域 field 的值加上增量 1 -1 |
hsetnx
|
将哈希表 key 中的域 的值设置为 value,当且仅当域 不存在 |
评分(score)
,这个评分(score)被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以是重复了命令 | 含义 |
---|---|
zadd
|
将一个或多个 member 元素及其 score 值加入到有序集 key 当中 |
zrange [WITHSCORES] |
返回有序集 key 中,下标在 之间的元素。带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
|
为元素的score加上增量 |
zrem
|
删除该集合下,指定值的元素 |
zcount
|
统计该集合,分数区间内的元素个数 |
zrank
|
返回该值在集合中的排名,从0开始 |
SortedSet(zset)是Redis提供的一个非常特别的数据结构,一方面它等价于Java的数据结构Map
zset底层使用了两个数据结构
简介
有序集合在生活中比较常见,例如根据成绩对学生排名,根据得分对玩家排名等。对于有序集合的底层实现,可以用数组、平衡树、链表等。数组不便元素的插入、删除;平衡树或红黑树虽然效率高但结构复杂;链表查询需要遍历所有效率低。Redis采用的是跳跃表。跳跃表效率堪比红黑树,实现远比红黑树简单。
实例
对比有序链表和跳跃表,从链表中查询出51
从此可以看出跳跃表比有序链表效率要高
配置大小单位,开头定义了一些基本的度量单位,只支持bytes,不支持bit。大小写不敏感
类似jsp中的include,多实例的情况可以把公用的配置文件提取出来
将本机访问保护模式设置no(允许其他机器访问)
一个空闲的客户端维持多少秒会关闭,0表示关闭该功能。即永不关闭
设定库的数量 默认16,默认数据库为0,可以使用SELECT
命令在连接上指定数据库id
必须设置
,否则,将内存占满,造成服务器宕机现代计算机用二进制(位) 作为信息的基础单位, 1个字节等于8位, 例如“abc”字符串是由3个字节组成, 但实际在计算机存储时将其用二进制表示, “abc”分别对应的ASCII码分别是97、 98、 99, 对应的二进制分别是01100001、 01100010和01100011,如下图
合理地使用操作位能够有效地提高内存使用率和开发效率
Redis提供了Bitmaps这个“数据类型”可以实现对位的操作
字符串(key-value)
, 但是它可以对字符串的位进行操作可以把想象成一个以位为单位的数组,数组的每个单元只能存储0和1,数组的下标在中叫做偏移量
格式
setbit
设置Bitmaps中某个偏移量的值(0或1)
*offset偏移量从0开始
实例
每个独立用户是否访问过网站存放在Bitmaps中, 将访问的用户记做1, 没有访问的用户记做0, 用偏移量作为用户的id。
设置键的第offset个位的值(从0算起) , 假设现在有20个用户,userid=1, 6, 11, 15, 19的用户对网站进行了访问, 那么当前Bitmaps初始化结果如图
users:20201106代表2020-11-06这天的独立访问用户的Bitmaps
注:
格式
getbit
获取Bitmaps中某个偏移量的值
获取键的第offset位的值(从0开始算)
实例
获取id=8的用户是否在2020-11-06这天访问过, 返回0说明没有访问过
注:因为100根本不存在,所以也是返回0
格式
bitcount
[start end]
统计字符串从start字节到end字节比特值为1的数量
实例
计算2020-11-06这天的访问量
start和end代表起始和结束字节数, 下面操作计算用户id在第1个字节到第3个字节之间的独立访问用户数, 对应的用户id是 11,15,9
举例: K1 【01000001 01000000 00000000 00100001】,对应【0,1,2,3】
1、bitcount K1 1 2 : 统计下标1、2字节组中bit=1的个数,即01000000 00000000
–》bitcount K1 1 2 --》1
2、bitcount K1 1 3 : 统计下标1、2字节组中bit=1的个数,即01000000 00000000 00100001
–》bitcount K1 1 3 --》3
3、bitcount K1 0 -2 : 统计下标0到下标倒数第2,字节组中bit=1的个数,即01000001 01000000 00000000
–》bitcount K1 0 -2 --》3
注意:redis的setbit设置或清除的是bit位置,而bitcount计算的是byte位置。
bitop and(or/not/xor)
[key…]
bitop是一个复合操作, 它可以做多个Bitmaps的and(交集) 、 or(并集) 、 not(非)
假设网站有1亿用户, 每天独立访问的用户有5千万, 如果每天用集合类型和Bitmaps分别存储活跃用户可以得到表
很明显, 这种情况下使用Bitmaps能节省很多的内存空间, 尤其是随着时间推移节省的内存还是非常可观的
但Bitmaps并不是万金油, 假如该网站每天的独立访问用户很少, 例如只有10万(大量的僵尸用户) , 那么两者的对比如下表所示, 很显然, 这时候使用Bitmaps就不太合适了, 因为基本上大部分位都是0。
在工作当中,我们经常会遇到与统计相关的功能需求,比如统计网站PV(PageView页面访问量),可以使用Redis的incr、incrby轻松实现。但像UV(UniqueVisitor,独立访客)、独立IP数、搜索记录数等需要去重和计数的问题如何解决?这种求集合中不重复元素个数的问题称为基数问题
解决基数问题有很多种方案:
(1)数据存储在MySQL表中,使用distinct count计算不重复个数
(2)使用Redis提供的hash、set、bitmaps等数据结构来处理
以上的方案结果精确,但随着数据不断增加,导致占用空间越来越大,对于非常大的数据集是不切实际的。
能否能够降低一定的精度来平衡存储空间?
Redis推出了HyperLogLog
HyperLogLog不能像集合那样,返回输入的各个元素。
什么是基数?
比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素个数)为5。 基数估计就是在误差可接受的范围内,快速计算基数。
Redis 3.2 中增加了对GEO类型的支持。GEO,Geographic,地理信息的缩写。该类型,就是元素的2维坐标,在地图上就是经纬度。redis基于该类型,提供了经纬度设置,查询,范围查询,距离查询,经纬度Hash等常见操作
引入jar包依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.3.0</version>
</dependency>
例子
public class JedisDemo1 {
public static void main(String[] args) {
//创建Jedis对象
Jedis jedis = new Jedis("192.168.80.128", 6379);
//测试
String ping = jedis.ping();
System.out.println(ping);
}
}
//操作key string
@Test
public void demo1() {
//创建Jedis对象
Jedis jedis = new Jedis("192.168.80.128", 6379); //添加
jedis.set("name","lucy");
//获取
String name = jedis.get("name");
System.out.println(name);
//设置多个key-value
jedis.mset("k1","v1","k2","v2");
List<String> mget = jedis.mget("k1", "k2");
System.out.println(mget);
Set<String> keys = jedis.keys("*");
for(String key : keys) {
System.out.println(key);
}
jedis.close();
}
//操作list
@Test
public void demo2() {
//创建Jedis对象
Jedis jedis = new Jedis("192.168.80.128", 6379);
jedis.lpush("key1","lucy","mary","jack");
List<String> values = jedis.lrange("key1", 0, -1);
System.out.println(values);
jedis.close();
}
//操作set
@Test
public void demo3() {
//创建Jedis对象
Jedis jedis = new Jedis("192.168.80.128", 6379);
jedis.sadd("names","lucy");
jedis.sadd("names","mary");
Set<String> names = jedis.smembers("names");
System.out.println(names);
jedis.close();
}
//操作hash
@Test
public void demo4() {
//创建Jedis对象
Jedis jedis = new Jedis("192.168.80.128", 6379);
jedis.hset("users","age","20");
String hget = jedis.hget("users", "age");
System.out.println(hget);
jedis.close();
}
//操作zset
@Test
public void demo5() {
//创建Jedis对象
Jedis jedis = new Jedis("192.168.80.128", 6379);
jedis.zadd("china",100d,"shanghai");
Set<String> china = jedis.zrange("china", 0, -1);
System.out.println(china);
jedis.close();
}
1、输入手机号,点击发送后随机生成6位数字码,2分钟内有效
2、输入验证码,点击验证,返回成功或失败
3、每个手机号每天只能输入三次
public class PhoneCode {
public static void main(String[] args) {
// 1、输入手机号获取验证码
String code = getCode("12345678906");
System.out.println(code);
// 2、验证
// String s = verifyCode("13618638760", "507273");
// System.out.println(s);
}
/**
* 生成六位数字验证码,并将验证码返回出去
* @return
*/
public static String getCode(String phoneNumber) {
// 验证:手机号是否已经超过了三次,从redis中取出 "Verify:"+phoneNumber+":count"
Jedis jedis = new Jedis("192.168.80.128",6379);
String codetKey = "Verify:" + phoneNumber + ":code";
String countKey = "Verify:" + phoneNumber + ":count";
String count = jedis.get(countKey);
if(null==count) {
// 没有发送,生成验证码并返回
String code = getRandomCode();
// 将次数加1,并且将验证码存入redis
jedis.setex(countKey,24*60*60,"1");
jedis.setex(codetKey,60*2,code);
return code;
}else if(Integer.valueOf(count)<3) {
String code = getRandomCode();
jedis.incrBy(countKey,1);
jedis.set(codetKey,code);
jedis.expire(codetKey,60);
return code;
}else {
return "一个手机一天之内不能获取三次验证码";
}
}
/**
* 每个手机每天只能发送三次,验证码放到redis中,设置过期时间120
* @return
*/
public static String getRandomCode () {
Random random = new Random();
String code = "";
for(int i = 0;i<6;i++){
int index = random.nextInt(10);
code += index;
}
return code;
}
/**
* 得到redis中的验证码
*/
public static String verifyCode(String phoneNumber,String code) {
Jedis jedis = new Jedis("192.168.80.128",6379);
String codetKey = "Verify:" + phoneNumber + ":code";
String jedisCode = jedis.get(codetKey);
if(code.equals(jedisCode)) {
return "验证成功";
}else {
return "验证失败";
}
}
}
1、引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
2、生成配置文件
package com.atguigu.redis_springboot.config;
import com.alibaba.fastjson.support.spring.FastJsonRedisSerializer;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
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.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
@ConditionalOnClass(RedisOperations.class)
public class RedisConfig {
@Bean(name = "redisTemplate")
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory
factory) {
//设置序列化
FastJsonRedisSerializer fastJsonRedisSerializer = new FastJsonRedisSerializer(Object.class);
// 配置redisTemplate
RedisTemplate<String, Object> redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(factory);
RedisSerializer stringSerializer = new StringRedisSerializer();
// key序列化
redisTemplate.setKeySerializer(stringSerializer);
// value序列化
redisTemplate.setValueSerializer(fastJsonRedisSerializer);
// Hash key序列化
redisTemplate.setHashKeySerializer(stringSerializer);
// Hash value序列化
redisTemplate.setHashValueSerializer(fastJsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
3、测试
@SpringBootTest
class RedisSpringbootApplicationTests {
@Autowired
private RedisTemplate redisTemplate;
@Test
void contextLoads() {
redisTemplate.opsForValue().set("k1","v1");
System.out.println(redisTemplate.opsForValue().get("k1"));
}
}
串联多个命令
防止别的命令插队。例子
例子
场景:有很多人有你的账户,同时去参加双十一抢购
一个请求想给金额减8000
一个请求想给金额减5000
悲观锁(Pessimistic Lock)
, 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制
,比如行锁
,表锁
等,读锁
,写锁等
,都是在做操作之前先上锁。
乐观锁(Optimistic Lock)
, 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。
在执行multi之前,先执行watch key1 [key2],可以监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
例子
Redis提供了2种不同形式的持久化方式
在指定的时间间隔
内将内存中的数据集快照
写入磁盘, 也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里
先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件
如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效
RDB的缺点是最后一次持久化后的数据可能丢失
Fork
的作用是复制一个与当前进程一样的进程
。新进程的所有数据(变量、环境变量、程序计数器等)数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程
写时复制技术
”一般情况父进程和子进程会共用同一段物理内存
,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程在redis.conf中配置文件名称,默认为dump.rdb
当Redis无法写入磁盘的话,直接关掉Redis的写操作。推荐yes
默认是1分钟内改了1万次,或5分钟内改了10次,或15分钟内改了1次
禁用: 不设置save指令,或者给save传入空字符串
bgsave:Redis会在后台异步进行快照操作, 快照同时还可以响应客户端请求
。lastsave
命令获取最后一次成功执行快照的时间fulshall也会产生dump.rbg文件,不过是空的,没有意义
写时拷贝技术
,但是如果数据庞大时还是比较消耗性能在备份周期在一定间隔时间做一次备份,所以如果Redis意外down掉的话,就会丢失最后一次快照后的所有修改
日志
的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来(读操作不记录
),只许追加文件但不可以改写文件
,redis启动之初会读取该文件重新构建数据appendonly.aof
AOF和RDB同时开启,系统默认取AOF的数据(数据不会存在丢失)
AOF文件损坏
,通过/usr/local/bin/redis-check-aof–fix appendonly.aof
进行恢复everysec
同步时机交给操作系统
AOF文件持续增长而过大时,会fork出一条新进程来将文件重写(也是先写临时文件最后再rename),redis4.0版本后的重写,是指上就是把rdb 的快照,以二级制的形式附在新的aof头部,作为已有的历史数据,替换掉原来的流水账操作
no-appendfsync-on-rewrite
no-appendfsync-on-rewrite=yes
,不写入aof文件只写入缓存,用户请求不会阻塞,但是在这段时间如果宕机会丢失这段时间的缓存数据。(降低数据安全性,提高性能)no-appendfsync-on-rewrite=no
, 还是会把数据往磁盘里刷,但是遇到重写操作,可能会发生阻塞。(数据安全,但是性能降低)触发机制,何时重写
例如:文件达到70MB开始重写,降到50MB,下次什么时候开始重写?
100MB
系统载入时或者上次重写完毕时,Redis会记录此时AOF大小,设为base_size,如果Redis的AOF当前大小>= base_size +base_size*100% (默认)且当前大小>=64mb(默认)的情况下,Redis会对AOF进行重写。
性能建议
主机数据更新后根据配置和策略, 自动同步到备机的master/slaver机制,Master以写为主,Slave以读为主
作用
######################redis6379.conf#######################
include /myredis/redis.conf
pidfile /var/run/redis_6379.pid
port 6379
dbfilename dump6379.rdb
######################redis6380.conf#######################
include /myredis/redis.conf
pidfile /var/run/redis_6380.pid
port 6380
dbfilename dump6380.rdb
######################redis6381.conf#######################
include /myredis/redis.conf
pidfile /var/run/redis_6381.pid
port 6381
dbfilename dump6381.rdb
info replication # 打印主从复制的相关信息
slaveof
# 在从机上配置主机的ip和端口 在6380和6381上执行: slaveof 127.0.0.1 6379
切入点问题?slave1、slave2是从头开始复制还是从切入点开始复制?比如从k4进来,那之前的k1,k2,k3是否也可以复制?
从机会全量复制主机的内容,k1,k2,k3,k4都会复制
从机是否可以写?set可否?
从机只可读,不可写
主机shutdown后情况如何?从机是上位还是原地待命?
主机shutdown后,从机原地待命,等待主机重新启动,一切回复正常
主机又回来了后,主机新增记录,从机还能否顺利复制?
可以复制,因为主机重启后和之前一样,主机写内容,会同步到从机中
其中一台从机down后情况如何?依照原有它能跟上大部队吗?
从机down后,会脱离大部队,如果重新启动,还想同步主机内容的话,需要重新执行命令slaveof
反客为主的自动版
,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库
sentinel monitor mymaster 127.0.0.1 6379 1
#其中mymaster为监控对象起的服务器名称, 1 为至少有多少个哨兵同意迁移的数量。
由于所有的写操作都是先在Master上操作,然后同步更新到Slave上,所以从Master同步到Slave机器有一定的延迟,当系统很繁忙的时候,延迟问题会更加严重,Slave机器数量的增加也会使这个问题更加严重
private static JedisSentinelPool jedisSentinelPool=null;
public static Jedis getJedisFromSentinel(){
if(jedisSentinelPool==null){
Set<String> sentinelSet=new HashSet<>();
sentinelSet.add("192.168.11.103:26379");
JedisPoolConfig jedisPoolConfig =new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(10); //最大可用连接数
jedisPoolConfig.setMaxIdle(5); //最大闲置连接数
jedisPoolConfig.setMinIdle(5); //最小闲置连接数
jedisPoolConfig.setBlockWhenExhausted(true); //连接耗尽是否等待
jedisPoolConfig.setMaxWaitMillis(2000); //等待时间
jedisPoolConfig.setTestOnBorrow(true); //取连接的时候进行一下测试 ping pong
jedisSentinelPool=new JedisSentinelPool("mymaster",sentinelSet,jedisPoolConfig);
return jedisSentinelPool.getResource();
}else{
return jedisSentinelPool.getResource();
}
}
容量不够,redis如何进行扩容?
并发写操作, redis如何分摊?
另外,主从模式,薪火相传模式,主机宕机,导致ip地址发生变化,应用程序中配置需要修改对应的主机地址、端口等信息
之前通过代理主机来解决,但是redis3.0
中提供了解决方案。就是无中心化集群
配置
搭建结果:制作6个实例,6379、6380、6381、6389、6390、6391,上下对应主从
include /myredis/redis.conf
pidfile "/var/run/redis_6391.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 15000 设定节点失联时间,超过该时间(毫秒),集群自动进行主从切换。
注:%s/6379/6380命令可以将文本中的6379替换成6380
cd /opt/redis-6.2.1/src
2、运行集成集群命令
redis-cli --cluster create --cluster-replicas 1 192.168.80.128:6379 192.168.80.128:6380 192.168.80.128:6381 192.168.80.128:6389 192.168.80.128:6390 192.168.80.128:6391
注:
ip一定要真实ip,不能是localhost或者127.0.0.1
–replicas 1 采用最简单的方式配置集群,一台主机,一台从机,正好三组。
# 连接Redis
redis-cli -c -p 6379
# 查看集群信息
cluster nodes
16384
个插槽(hash slot), 数据库中的每个键都属于这 16384 个插槽的其中一个CRC16(key) %16384
来计算键key属于哪个槽.其中 CRC16(key) 语句用于计算键 key 的 CRC16 校验和例子
如果一个集群可以有主节点, 其中:
节点 A 负责处理 0 号至 5460 号插槽
节点 B 负责处理 5461 号至 10922 号插槽
节点 C 负责处理 10923 号至 16383 号插槽
CLUSTER GETKEYSINSLOT
返回count个slot槽中的值
如果主节点下线,从节点能否自动升为主节点?
主节点恢复后,主从关系会如何?
主节点回来变成从机
如果所有某一段插槽的主从节点都宕掉,redis服务是否还能继续?
cluster-require-full-coverage 为yes
,那么 ,整个集群都挂掉luster-require-full-coverage 为no
,那么,该插槽数据全都不能使用,也无法存储 public class JedisClusterDemo {
public static void main(String[] args) {
HostAndPort hostAndPort = new HostAndPort("192.168.80.128",6379);
JedisCluster jedisCluster = new JedisCluster(hostAndPort);
jedisCluster.set("k1","v1");
String k1 = jedisCluster.get("k1");
System.out.println(k1);
}
}
好处
不足
key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会压到数据源,从而可能压垮数据源。比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。
key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮
key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
缓存雪崩与缓存击穿的区别在于这里针对很多key缓存,前者则是某一个key过期
随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题。
分布式锁主流的实现方案:
每一种分布式锁解决方案都有各自的优缺点:
SETNX KEY VALUE # 设置锁
expire users 30
del key # 删除锁
问题:如果设置时间和上锁分开进行的话,可能存在上完锁,服务器down了,就没有设置过期时间
解决办法:上锁和设置过期时间同时
set key value nx ex time
@GetMapping("testLock")
public void testLock(){
//1获取锁,setne ,顺便设置过期时间
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111",3,TimeUnit.SECONDS);
//2获取锁成功、查询num的值
if(lock){
Object value = redisTemplate.opsForValue().get("num");
//2.1判断num为空return
if(StringUtils.isEmpty(value)){
return;
}
//2.2有值就转成成int
int num = Integer.parseInt(value+"");
//2.3把redis的num加1
redisTemplate.opsForValue().set("num", ++num);
//2.4释放锁,del
redisTemplate.delete("lock");
}else{
//3获取锁失败、每隔0.1秒再获取
try {
Thread.sleep(100);
testLock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
代码除了修改的设置过期时间问题,还存在以下问题:
解决方法:
@GetMapping("testLock")
public void testLock(){
String uuid = UUID.randomUUID().toString();
//1获取锁,setne ,顺便设置过期时间
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,3,TimeUnit.SECONDS);
//2获取锁成功、查询num的值
if(lock){
...
String lockUuid = (String)redisTemplate.opsForValue().get("lock");
if(uuid.equals(lockUuid)){
//2.4释放锁,del
redisTemplate.delete("lock");
}
}else{
...
}
}
问题:
解决方法
@GetMapping("testLockLua")
public void testLockLua() {
//1 声明一个uuid ,将做为一个value 放入我们的key所对应的值中
String uuid = UUID.randomUUID().toString();
//2 定义一个锁:lua 脚本可以使用同一把锁,来实现删除!
String skuId = "25"; // 访问skuId 为25号的商品 100008348542
String locKey = "lock:" + skuId; // 锁住的是每个商品的数据
// 3 获取锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent(locKey, uuid, 3, TimeUnit.SECONDS);
// 第一种: lock 与过期时间中间不写任何的代码。
// redisTemplate.expire("lock",10, TimeUnit.SECONDS);//设置过期时间
// 如果true
if (lock) {
// 执行的业务逻辑开始
// 获取缓存中的num 数据
Object value = redisTemplate.opsForValue().get("num");
// 如果是空直接返回
if (StringUtils.isEmpty(value)) {
return;
}
// 不是空 如果说在这出现了异常! 那么delete 就删除失败! 也就是说锁永远存在!
int num = Integer.parseInt(value + "");
// 使num 每次+1 放入缓存
redisTemplate.opsForValue().set("num", String.valueOf(++num));
/*使用lua脚本来锁*/
// 定义lua 脚本
String script = "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(script);
// 设置一下返回值类型 为Long
// 因为删除判断的时候,返回的0,给其封装为数据类型。如果不封装那么默认返回String 类型,
// 那么返回字符串与0 会有发生错误。
redisScript.setResultType(Long.class);
// 第一个要是script 脚本 ,第二个需要判断的key,第三个就是key所对应的值。
redisTemplate.execute(redisScript, Arrays.asList(locKey), uuid);
} else {
// 其他线程等待
try {
// 睡眠
Thread.sleep(1000);
// 睡醒了之后,调用方法。
testLockLua();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
acl setuser user2 on >password ~cached:* +get
Redis6终于支撑多线程了,告别单线程了吗?
客户端交互部分的网络IO交互处理模块多线程,而非执行命令多线程
执行命令仍然是单线程
整体设计如下:
注: 另外,多线程IO默认也是不开启的,需要再配置文件中配置 io-threads-do-reads yes io-threads 4
之前老版Redis想要搭集群需要单独安装ruby环境,Redis 5 将 redis-trib.rb 的功能集成到 redis-cli 。另外官方 redis-benchmark 工具开始支持 cluster 模式了,通过多线程的方式对多个分片进行压测压。