Redis
1.什么是Redis,什么是NoSQL数据库
(1)redis
Redis是一个基于内存的key-value结构数据库,使用单线程+多路IO复用技术
基于内存存储,读写性能高
适合存储热点数据
可以用作:数据库、缓存和消息中间件
(2)NoSQL数据库
(1)减少CPU的压力,通过内存读取
(2)通过缓存,减少IO操作
(1)NoSQL(not only sql),以key-value模式存储,因此大大增加了数据库的扩展能力
(2)不遵循SQL标准,不支持ACID,远超于SQL性能
(3)适用场景:对数据高并发读写,海量数据的读写,对数据高可扩展性的
(4)不适用场景:需要事务支持,基于SQL的结构化存储,处理复杂关系
(3)redis的发布和订阅
(1)发布和订阅是一种消息通信模式,发布者发送消息,订阅者接收消息
(2)redis客户端可以订阅任意数量的频道
(3)订阅者订阅 channel1 这个频道:SUBSCRIBE channel1
发布者给 channel1 这个频道发送消息 hello :publish channel1 hello,返回值是订阅者的数量
订阅者就可以收到发布者发送的 hello
2.Redis数据类型
(1)常用数据类型
Redis存储的是key-value结构的数据,其中key是字符串类型,value有5种常用的数据类型:
(1)字符串 String
(2)哈希 hash
(3)列表 list
(4)集合 set
(5)有序集合 sorted set
(2)新数据类型
(1)Bitmaps
(2)HyperLogLog
(3)Geospatial
(1)Bitmaps
(1)Bitmaps本身不是一种数据类型,实际上它就是字符串,但是它可以对字符串的位进行操作
(2)Bitmaps单独提供了一套命令,可以把Bitmaps想象成一个以位为单位的数组,数组的每个单元只能存储
0和1,数组的下标在Bitmaps中叫做偏移量
(3)设置Bitmaps中某个偏移量的值(0/1):setbit key的名字 偏移量 value
setbit id 12 1 --> key:id,偏移量:12,value:1
(4)获取Bitmaps中某个偏移量的值:getbit key 偏移量
(5)bitcount:统计字符串被设置为1的bit数。一般情况下,给定的整个字符串都会被进行计数,通过
指定额外的start/end参数,可以让计数只在特定的位上进行
bitcount key [start end]
(6)bitop:是一个符合操作,可以做多个Bitmaps的and(交集)、or(并集)、not(非)、xor(异或)
(2)HyperLogLog
主要用于基数的计算操作(起到去重)
基数:[1,1,2,3,3,3,4,4,8] --> 的基数就是5个[1,2,3,4,8]
(1)添加指定元素:pfadd key value
(2)统计出基数的数量:pfcount key
(3)合并多个到一个中(比如一个月的活跃用户是30天的活跃用户合并来的)
pfmerge k k1 k2 -> k:合并后的 k1/k2:被合并的
(3)Geospatial
针对地理信息(经度纬度)
(1)geoadd:添加,geoadd key 经度 纬度 名称
(2)geopos:查询某个位置的经度纬度 geopos key 名称
(3)geodist:获取两个位置之间的直线距离 geodist key 名称1 名称2 单位
(4)georadius:以给定的经纬度为中心,找出某一半径内的元素
georadius key 经度 纬度
3.Redis常用命令
(1)字符串操作命令
(2)哈希操作命令
(3)列表操作命令
(4)集合操作命令
(5)有序集合操作命令
(6)通用命令
4.在java中操作Redis
(1)Jedis
<dependencies>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.2.0</version>
</dependency>
</dependencies>
//使用的话只需要将Jedis对象创建出来即可
Jedis jedis=new Jedis("localhost",6379);
//第一个参数是IP地址,第二个参数是端口号
public class JedisDemo1 {
//操作key
@Test
public void demo1(){
//创建jedis对象
Jedis jedis=new Jedis("localhost",6379);
//得到redis中所有的key
Set<String> keys = jedis.keys("*");
for (String key:keys){
System.out.println(key);
}
//向redis中添加数据
jedis.set("cw","tm");
//从redis中获取数据
String value = jedis.get("cw");
System.out.println(value);
//同时向redis中添加多个key和value
jedis.mset("k1","v1","k2","v2");
//获取
List<String> mget = jedis.mget("k1", "k2");
System.out.println(mget);
}
//操作list
@Test
public void demo2(){
//创建jedis对象
Jedis jedis=new Jedis("localhost",6379);
//添加数据 从左加
jedis.lpush("number","1","2","3","4","5","6");
//取出所有数据
List<String> number = jedis.lrange("number", 0, -1);
System.out.println(number);
//还有其他方法
}
//set 具体怎么做看文档就可以了 其它类似
}
(2)模拟验证码发送
(1)输入手机号,点击发送后随机生成6位数字验证码,2分钟有效
(2)输入验证码,点击验证,返回成功或失败
(3)每个手机号每天只能输入5次
public class PhoneCode {
public static void main(String[] args) {
//模拟验证码发送
//verifyCode("15109546897");
//校验
getRedisCode("15109546897","323559");
}
//1.生成6位数字验证码
public static String getCode(){
Random random=new Random();
String code="";
for (int i=1;i<=6;i++){
int rand = random.nextInt(10);
code+=rand;
}
return code;
}
//2.每个手机每天只能发送5次,验证码放入redis,设置过期时间120s
public static void verifyCode(String phone){
//连接redis
Jedis jedis=new Jedis("localhost",6379);
//拼接key
//手机发送次数key
String countKey=phone+"count";
//验证码key
String codeKey=phone+"code";
//每个手机每天只能发送5次
String count = jedis.get(countKey);
if (count==null){
//没有发送次数 证明是第一次发送
//设置发送次数为1 key 过期时间 值
jedis.setex(countKey,24*60*60,"1");
} else if (Integer.parseInt(count)<5) {
//发送次数+1
jedis.incr(countKey);
} else if (Integer.parseInt(count)==5) {
System.out.println("验证码发送次数已经超过5次");
jedis.close();
return;
}
//发送的验证码放到redis中
String vCode=getCode();
jedis.setex(codeKey,120,vCode);
jedis.close();
}
//3.验证码校验
public static void getRedisCode(String phone,String code){
//从redis中获取验证码
//连接redis
Jedis jedis=new Jedis("localhost",6379);
//验证码key
String codeKey=phone+"code";
String redisCode = jedis.get(codeKey);
//判断
if (redisCode==null){
System.out.println("验证码已过期,请重新获取");
return;
}
if (redisCode.equals(code)){
System.out.println("success");
}else {
System.out.println("error");
}
jedis.close();
}
}
(3)spring Data Redis
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
spring:
redis:
host: localhost
port: 6379
database: 0 #使用0号数据库
jedis:
#redis连接池配置
pool:
max-active: 8 #最大连接数
max-wait: 1ms #连接池最大阻塞等待时间
max-idle: 4 #连接池中最大空闲连接
min-idle: 0 #连接池中的最小空闲连接
5.redis事务
(1)redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行
过程中,不会被其他客户端发送来的命令请求所打断
(2)redis事务的主要作用就是串联多个命令防止别的命令插队
(1)事务基本操作
从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec后,redis会将
之前的命令队列中的命令依次执行。
组队的过程中,可以通过discard来放弃组队
错误处理
(1)组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消
(2)如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚
(2)事务冲突
多个线程都想操作某一个资源,就会存在事务冲突
可以使用以下两种方式解决:
(1)悲观锁:给数据加锁,每一个线程都会阻塞等待锁
(2)乐观锁:线程读数据不会上锁,在修改数据时会判断一下版本号,看是否有人修改过这个数据
乐观锁
在执行multi之前,先执行 watch key1 key2...,可以监视一个或多个key,如果在事务执行之前这个
或这些key被其他命令所改动,那么事务将被打断
(3)事务三特性
(1)单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行过程中,不会被其他
客户端发送来的命令请求所打断
(2)没有隔离级别概念:队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会
被实际执行
(3)不保证原子性:事务中如果有一条命令执行失败,其后地命令仍然会被执行,没有回滚
(4)秒杀案例
基本实现
public class SecKill {
//秒杀过程
public static boolean doSecKill(String uid,String prodId){
//1.uid和prodId非空判断
if (uid==null||prodId==null){
return false;
}
//2.连接redis
Jedis jedis=new Jedis("localhost",6379);
//3.拼接key
//库存key
String kcKey="sk"+prodId;
//秒杀成功用户key
String userKey="uk"+uid;
//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.如果库存数量等于0,秒杀结束
int i = Integer.parseInt(kc);
if (i<=0){
System.out.println("秒杀已经结束");
jedis.close();
return false;
}
//7.秒杀过程
//库存减一
jedis.decr(kcKey);
//秒杀成功的用户添加清单里边
jedis.sadd(userKey,uid);
System.out.println("秒杀成功");
jedis.close();
return true;
}
}
并发场景下秒杀
(1)解决连接超时问题
使用线程连接池可以解决
/**
* 单例模式 保证整个项目只有一个jedis线程连接池
*/
public class JedisPoolUtil {
private static volatile JedisPool jedisPool=null;
private JedisPoolUtil(){
}
public static JedisPool getJedisPoolInstance(){
if (jedisPool==null){
synchronized (JedisPoolUtil.class){//锁住JedisPoolUtil这个类
if (jedisPool==null){
JedisPoolConfig poolConfig=new JedisPoolConfig();
poolConfig.setMaxTotal(200);//总连接的最大连接数
poolConfig.setMaxIdle(32);//空闲连接的最大数量
poolConfig.setMaxWaitMillis(100*1000);//等待时间
poolConfig.setBlockWhenExhausted(true);//超过等待时间是否等待
poolConfig.setTestOnBorrow(true);//
//60000是超时时间
jedisPool=new JedisPool(poolConfig,"localhost",6379,60000);
}
}
}
return jedisPool;
}
public static void release(Jedis jedis){
if (jedis!=null){
jedis.close();
}
}
}
public class SecKill {
//秒杀过程
public static boolean doSecKill(String uid,String prodId){
//1.uid和prodId非空判断
if (uid==null||prodId==null){
return false;
}
//2.连接redis
//Jedis jedis=new Jedis("localhost",6379);
//通过连接池得到jedis对象
JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
Jedis jedis = jedisPoolInstance.getResource();
//3.拼接key
//库存key
String kcKey="sk"+prodId;
//秒杀成功用户key
String userKey="uk"+uid;
//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.如果库存数量等于0,秒杀结束
int i = Integer.parseInt(kc);
if (i<=0){
System.out.println("秒杀已经结束");
jedis.close();
return false;
}
//7.秒杀过程
//库存减一
jedis.decr(kcKey);
//秒杀成功的用户添加清单里边
jedis.sadd(userKey,uid);
System.out.println("秒杀成功");
jedis.close();
return true;
}
}
(2)解决超卖问题
利用乐观锁解决
public class SecKill {
//秒杀过程
public static boolean doSecKill(String uid,String prodId){
//1.uid和prodId非空判断
if (uid==null||prodId==null){
return false;
}
//2.连接redis
//Jedis jedis=new Jedis("localhost",6379);
//通过连接池得到jedis对象
JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
Jedis jedis = jedisPoolInstance.getResource();
//3.拼接key
//库存key
String kcKey="sk"+prodId;
//秒杀成功用户key
String userKey="uk"+uid;
//监视库存
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.如果库存数量等于0,秒杀结束
int i = Integer.parseInt(kc);
if (i<=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;
}
//库存减一
//jedis.decr(kcKey);
//秒杀成功的用户添加清单里边
//jedis.sadd(userKey,uid);
System.out.println("秒杀成功");
jedis.close();
return true;
}
}
(3)解决库存遗留问题
使用了乐观锁,一个线程修改版本号之后,其他线程发现版本号不一致,导致购买不成功,导致库存遗留
(1)使用LUA脚本执行复杂或多步的redis操作,写为一个脚本,一次提交给redis执行,减少反复连接
redis操作的次数,提升性能。
(2)LUA脚本类似redis事务,有一定的原子性,不会被其他命令插队,可以完成一些redis事务性的操作
(3)实际上是redis利用其单线程的特性,用任务队列的方式解决多任务并发问题
6.持久化操作
(1)RDB
(1)在指定的时间间隔内将内存中的数据集快照写入磁盘
它恢复时是将快照文件直接读到内存里
(2)redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化
过程都结束了,再用这个临时文件替换上次持久化好的文件。
(3)整个过程中,主进程是不进行任何IO操作的,提高了性能
(4)如果需要进行大规模的数据恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式
更加高效
(5)RDB的缺点是最后一次持久化后的数据可能丢失
(6)在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统
调用,出于效率考虑,Linux中引入了"写时复制技术"
(7)一般情况父进程和子进程会共用同一段物理内存,只有进程空间的各段的内容要发生变化时,才会
将父进程的内容复制一份给子进程
优点
(1)适合大规模的数据恢复
(2)对数据完整性和一致性要求不高的场景更适合使用
(3)节省磁盘空间
(4)恢复速度快
缺点
(1)fork()的时候,内存中的数据被克隆了一份,大致2被的膨胀性需要考虑
(2)虽然redis在fork时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能
(3)在备份周期在一定间隔时间做一次备份,所以如果redis意外down掉的话,就会丢失最后一次快照后
的所有修改
(2)AOF(Append Only File)
(1)以日志的形式来记录每个写操作,将redis执行过的所有写指令记录下来(读操作不记录),只许
追加文件但不可以改写文件
(2)redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件的内容将
写指令从前到后执行一次以完成数据的恢复工作
持久化流程
(1)客户端的请求写命令会被append追加到AOF缓冲区内
(2)AOF缓冲区根据AOF持久化策略将操作sync同步到磁盘的AOF文件中
(3)AOF文件大小超过重写策略或手动重写时,会对AOF文件重写,压缩AOF文件容量
(4)redis服务重启时,会重新加载AOF文件中的写操作达到数据恢复的目的
持久化策略
(1)appendfsync always:始终同步,每次redis的写入(修改)都会立刻记入日志
性能较差,但数据完整性较好
(2)appendfsync everysec:每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失
(3)appendfsync no:redis不主动进行同步,把同步时机交给操作系统
优点
(1)备份机制更稳健,丢失数据概率更低
(2)可读的日志文件,通过操作AOF文件,可以处理误操作
缺点
(1)比起RDB占用更多的磁盘空间
(2)恢复备份速度慢
(3)每次读写都同步的话,有一定的性能压力
(4)存在个别bug,造成不能恢复
(3)使用哪个
(1)如果对数据不敏感,可以单独使用RDB
(2)不建议单独使用AOF,因为可能出现bug
(3)如果只是做纯内存缓存,可以都不用
7.主从复制
(1)主机数据更新后根据配置和策略,自动同步到备机的master/slaver机制,master以写为主,
slave以读为主。一主多从
(2)读写分离,性能扩展
(3)容灾性好,挂掉之后可以快速恢复。如果一个从服务器挂掉重启之后,需要再加入到之前那个主服务器,
才会变成从服务器,会把主机现在有的数据全部复制过来。
(4)如果主服务器挂掉之后,从服务器依然是从服务器,主服务器重启之后依然是主服务器
(5)存在复制延时,由于所有的写操作都现在主服务器上,然后同步更新到从服务器,所以从主服务器
同步到从服务器有一定的延迟,当系统很繁忙时,延迟问题会更加严重,从服务器数量的增加也会
使这个问题更加严重。
(1)主从复制的原理
(1)当从服务器连接上主服务器之后,从服务器向主服务器发送进行数据同步消息
(2)主服务器接到从服务器发送过来的同步消息,会把主服务器的数据持久化到rdb文件中,把rdb
文件发送给从服务器,从服务器拿到rdb文件进行读取数据
(3)每次主服务器进行写操作之后,会主动和从服务器同步数据
(2)薪火相传
(3)哨兵模式
反客为主的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库
8.集群
(1)解决容量不够问题
(2)解决并发写操作
(3)主从模式下,如果主机发生宕机,导致IP地址发生变化,应用程序中的配置需要修改对应的主机地址、
端口信息等
(4)redis集群实现了对redis的水平扩容,即启动N个redis节点,将整个数据库分布存储在这N个
节点中,每个节点存储总数居的1/N。一个集群中至少有3个主节点
(5)redis集群通过分区来提供一定程度的可用性:即使集群中有一部分节点失效或者无法进行通讯,
集群也可以继续处理命令请求
9.缓存穿透
特点:
(1)应用服务器压力变大
(2)redis命中率降低
(3)一直查询数据库
原因:
(1)redis查询不到数据
(2)出现很多非正常的url访问(遭受到了恶意攻击)
解决方案:
(1)对空值进行缓存:如果一个查询返回的数据为空(不管数据存不存在),仍要把这个空结果进行缓存,
设置空结果的过期时间会很短,最长不超过5分钟
(2)采用布隆过滤器:将所有可能存在的数据都哈希到一个足够大的位图里,一个一定不存在的数据会被
这个bitmaps拦截掉,从而避免了对数据库的查询压力
(3)进行实时监控:当发现redis的命中率开始急速降低,需要排查访问对象和访问的数据,可以设置
黑名单限制服务
10.缓存击穿
特点:
(1)数据库访问压力瞬时增加
(2)redis中没有出现大量key过期
(3)redis正常运行
原因:
(1)redis中某个key过期了,大量请求来访问这个key
解决:
(1)预先设置热门数据:在redis高峰访问之前,把一些人们数据提前存入到redis中,加大这些热门
key的过期时间
(2)实时调整:现场监控哪些key是热门key,实时调整key的过期时间
(3)使用锁:一个线程获取锁之后去访问数据库的数据,其他线程阻塞等待,将这个线程访问到的数据
放入缓存中,其他线程去访问redis缓存
11.缓存雪崩
特点:
(1)数据库压力变大
(2)服务器崩溃
原因:
(1)在极少时间段内,出现大量key过期的现象
解决:
(1)构造多级缓存架构:nginx缓存+redis缓存+其他缓存
(2)使用锁或队列:一个线程获取锁之后去访问数据库的数据,其他线程阻塞等待,将这个线程访问到的数据
放入缓存中,其他线程去访问redis缓存。不适用于高并发情况
(3)设置过期标志更新缓存:记录缓存数据是否过期,如果过期会触发通知另外的线程去后台去更新实际key
缓存
(4)将缓存失效时间分散开:可以在原有的失效时间的基础上加一个随机值
12.分布式锁
由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制所策略失效,
单纯的 java API 并不能提供分布式锁的能力。
为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题
分布式锁主流的实现方案:
(1)基于数据库实现分布式锁
(2)基于缓存(redis等):redis性能最高
(3)基于Zookeeper,可靠性最高
(1)基于redis实现分布式锁
(1)使用setnx上锁,通过del释放锁
(2)如果锁一直没有释放,可以通过设置key的过期时间,自动释放锁
(3)上锁之后如果突然出现问题,就无法设置过期时间了
所以要在上锁的同时设置过期时间:set users 10 nx ex 12
10:值 nx:表示要上锁 ex:设置过期时间 12:过期时间
13.redis缓存删除策略
(1)LRU
最近最少使用策略
当内存不足以存储新数据时,redis会尝试删除最近最少被访问过的数据
这个策略通常适用于数据访问具有时效性的场景
(2)LFU
最不经常使用策略
这种策略会删除访问次数最少的数据
适用于数据频率访问不均匀的场景
(3)TTL
生存时间策略
当设置了缓存数据的生存时间,当数据的存活时间超过设定值,redis会自动删除这些数据
(4)随机删除
随机选择一些数据进行删除
相对简单,但可能导致缓存数据的有效性降低
(5)Partial Redis Del
如果一个键关联了多个元素,例如一个列表、集合等,当删除某个元素时,不会删除整个键,而是只删除
指定元素
14.redis集群数据hash分片算法
redis cluster 将所有数据划分为 16384 个槽位,每个节点负责其中一部分槽位,槽位的信息存储
于每个节点中
当 redis cluster 的客户端来连接集群时,它也会得到一份集群的槽位配置信息,并将其缓存在客户端
本地。这样当客户端要查找某个 key 时,可以根据槽位定位算法定位到目标节点
槽位定位算法:
cluster 默认会对 key 值使用 crc16 算法进行 hash 得到一个整数值,然后利用这个整数值对
16384 进行取模得到具体槽位。再根据槽位值和redis节点的对应关系就可以定位到key具体是落在
哪个redis节点上的
15.redis主从风暴
如果 redis 主节点有很多从节点,在某一时刻如果所有从节点都同时连接主节点,那么主节点会同时
把内存快照 RDB 发给多个从节点,这样会导致 redis 主节点压力非常大
可以采用如下的树形复制架构来解决
16.redis集群为什么至少需要三个master节点
因为新master的选举至少需要大于半数的集群master节点同一才能选举成功,如果只有两个master节点,
当一个挂了,剩下一个达不成选举条件