redis基础
1、redis简介(省略)–redis是单线程
1.1、redis快的原因:内存中操作 + io多路复用技术
io多路复用技术解释:
上面文字描述中的进入一个地方是指:
2、redis 多样的数据结构存储持久化数据(redis支持的使用场景)
3、linux下安装redis
3.1 安装redis之前,下载安装最新版的gcc编译器
安装C 语言的编译环境:
yum install centos-release-scl scl-utils-build
yum install -y devtoolset-8-toolchain
scl enable devtoolset-8 bash
测试 gcc版本
3.2 安装redis
3.3 安装目录查看及文件讲解
3.4 前台启动
3.5 后台启动
前提条件:
后台启动:
redis-server /myredis/redis.conf (这里的 redis.conf 是从安装目录下面复制到myredis中来的)
3.6 redis关闭(redis-cli shutdown或者shutdown)
4、redis介绍及相关知识
redis使用到的是:
单线程+多路io复用
特点:
支持多数据类型,支持持久化,单线程+多路IO复用
5、获得redis常见数据类型操作命令的网站地址
http://www.redis.cn/commands.html
6、常用5大类型和 key,value的操作命令
6.1、 key的命令
6.2、String(字符串)
6.2.1 简介
6.2.2 常用命令
6.2.3 string的数据结构
1、String的数据结构为简单动态字符串(Simple Dynamic String,缩写SDS)。是可以修改的字符串,内部结构实现上类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配.
2、 如图中所示,内部为当前字符串实际分配的空间capacity一般要高于实际字符串长度len。当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时一次只会多扩1M的空间。需要注意的是字符串最大长度为512M。
6.3、List 列表
6.3.1 简介
1、Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。
2、它的底层实际是个双向链表,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差。
6.3.2 常用命令
6.3.3 数据结构
List的数据结构为快速链表quickList。
6.4、 Set 集合
6.4.1 简介
6.4.2 常用命令
6.4.3 数据结构
6.5、Hash(哈希)
6.5.1、简介
使用场景:
6.5.2、常用命令
6.5.3、数据结构
6.6、Zset(Redis有序集合)
6.6.1 简介
6.6.2 命令
6.6.3 数据结构
redis高级
7、redis配置文件(看文档)
8、使用本地的redis desktop manager连接远程linux中的redis
需要修改redis.conf,将bind 127.0.0.1注释掉(表示只能本机连接);将protected-mode 改成no(后台启动)
9、redis的发布和订阅
- 定义:
- :redis的发布和订阅
- 案例图解:
订阅者:
发布者:
10、redis的新数据类型
10.1、 Bitmaps(以位为单位的数组, 数组的每个单元只能存储0和1, 数组的下标在Bitmaps中叫做偏移量。)
10.1.1 简介
10.1.2 命令
10.1.2.1 setbit设置值
10.1.2.2 getbit获取值
10.1.2.3 bitcount 统计字符串被设置为1的bit数。
10.1.2.4 bitop (bitop是一个复合操作, 它可以做多个Bitmaps的and(交集) 、 or(并集) 、 not(非) 、 xor(异或) 操作并将结果保存在destkey中。)
and的测试:
10.2、HyperLogLog (去重,然后统计数据,唯一的优势是需要的容量小)
10.2.1 简介
10.2.2 命令
10.3、Geospatial (地理信息)-没啥用
11、jedis的入门+整合和redission的锁学习在(jedis 6—redisson的使用)文章中.
12、redis事务
1、Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
2、Redis事务的主要作用就是串联多个命令防止别的命令插队。
总结:Redis的事务可以看做一个队列,将需要执行的命令放在队列中。
12.1 、Multi(开启事务)、Exec(执行事务)、discard(取消事务)
- 开启事务,执行事务成功的案例:
- 开启事务,在事务中报错,则会在事务执行的时候报错:
- 开启事务,放在队列中的命令有错误的,执行事务时,会将队列中其他的命令执行,错误的命令报错:
13、乐观锁和悲观锁
注意:redis是单线程的,但是他不能处理多线程的并发问题,还是需要通过乐观锁或者悲观锁来处理
13.1 悲观锁(概念跟java中的悲观锁一样)
13.2 乐观锁(跟java中一样,根据版本号机制)
13.3 watch key[key …] (监控一个key/多个key。**如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断,事务就算执行了,里面的命令也没有用)
- 成功案例:
- 失败案例:
13.4、unwatch取消watch命令对所有key的监视
14、事务的三大特性(单独的隔离操作:因为是单线程,所以会顺序执行;没有隔离级别概念;不保证原子性)
实际案例:秒杀案例(看秒杀案例专题文章)–学完分布式锁,我觉得也可以用分布式锁+lua
15、持久化操作RDB和AOF
15.1、RDB (看文档)
在指定的时间间隔内将内存中的数据集快照写入磁盘, 也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里
15.2、AOF
15.2.1、什么是AOF
15.2.2、持久化流程
15.2.3、恢复数据
Redis服务重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的;
15.2.4、开启aof
15.2.5、修复aof文件(使用redis-check-aof)
15.2.6、aof同步频率
15.2.7、Rewrite压缩(看文档)
15.3、RDB和AOF哪个好(存储在磁盘上的话,2个都使用)
官方推荐两个都启用。
如果对数据不敏感,可以选单独用RDB。
不建议单独用 AOF,因为可能会出现Bug。
如果只是做纯内存缓存,可以都不用。
16、主从复制
16.1、正常是1主多从,考虑到1主的安全性,可以搭建集群,每一个节点里面都是1主多从的结构。
16.2、搭建1主2从
16.2.1、前置准备
16.2.2、配置1主机,2从机的.conf文件
16.2.3、启动服务,查看进程
16.2.4、info replication查看主机运行情况
16.2.5、slaveof 配从(库)不配主(库)
16.2.6、一点注意事项
16.3、主从复制原理
16.4、薪火相传
16.5、反客为主(slaveof no one)
17、哨兵模式(sentinel):当主机a挂掉之后,选择一个从机b作为主机,其他的从机中的master的地址+端口就变成了从机(主机)b,当主机a再次启动之后,会变成从机,其中的master也变成了从机(主机)b
17.1、前期准备配置
17.2、启动哨兵
17.3、当主机挂掉,从机选举中产生新的主机
17.4、哨兵模式有一个缺点,就是会复制延时(差不多会丢失10秒的数据)
17.5、设置主机挂掉之后,哪个从机变成master(slave-priority 100,值越小优先级越高)
17.6、java代码:哨兵sentinel + sentinenl池 + lua脚本(解决库存剩余容量的问题)的实际代码案例
service层
package com.bear.service;
import com.bear.config.JedisPoolUtil;
import org.springframework.stereotype.Service;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisSentinelPool;
import java.io.IOException;
/**
* <简述>
* <详细描述>
*
* @author LiuShanshan
* @version $Id$
*/
@Service
public class SewckillServiceTwo {
String secKillScript ="local userid=KEYS[1];\r\n" +
"local prodid=KEYS[2];\r\n" +
"local qtkey='sk:'..prodid..\":qt\";\r\n" +
"local usersKey='sk:'..prodid..\":usr\";\r\n" +
"local userExists=redis.call(\"sismember\",usersKey,userid);\r\n" +
"if tonumber(userExists)==1 then \r\n" +
" return 2;\r\n" +
"end\r\n" +
"local num= redis.call(\"get\" ,qtkey);\r\n" +
"if tonumber(num)<=0 then \r\n" +
" return 0;\r\n" +
"else \r\n" +
" redis.call(\"decr\",qtkey);\r\n" +
" redis.call(\"sadd\",usersKey,userid);\r\n" +
"end\r\n" +
"return 1" ;
public boolean doSecKill(String prodid,String uid) throws IOException {
// 从redis线程池里面获取redis线程
JedisSentinelPool jedisPoolInstance = JedisPoolUtil.getJedisFromSentinel();
Jedis jedis = jedisPoolInstance.getResource();
// JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
// Jedis jedis = jedisPoolInstance.getResource();
//String sha1= .secKillScript;
String sha1= jedis.scriptLoad(secKillScript);
Object result= jedis.evalsha(sha1, 2, uid,prodid);
String reString=String.valueOf(result);
if ("0".equals( reString ) ) {
System.err.println("已抢空!!");
}else if("1".equals( reString ) ) {
System.out.println("抢购成功!!!!");
}else if("2".equals( reString ) ) {
System.err.println("该用户已抢过!!");
}else{
System.err.println("抢购异常!!");
}
jedis.close();
return true;
}
}
util层(包装sentinel池)
package com.bear.config;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.JedisSentinelPool;
import java.util.HashSet;
import java.util.Set;
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, "120.48.77.231", 6379, 60000 );
}
}
}
return jedisPool;
}
private static JedisSentinelPool jedisSentinelPool = null;
public static JedisSentinelPool getJedisFromSentinel() {
if (jedisSentinelPool == null) {
Set<String> sentinelSet = new HashSet<>();
sentinelSet.add("120.48.77.231:26379");
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(200); //最大可用连接数
jedisPoolConfig.setMaxIdle(32); //最大闲置连接数
jedisPoolConfig.setMinIdle(5); //最小闲置连接数
jedisPoolConfig.setBlockWhenExhausted(true); //连接耗尽是否等待
jedisPoolConfig.setMaxWaitMillis(100*1000); //等待时间
jedisPoolConfig.setTestOnBorrow(true); //取连接的时候进行一下测试 ping pong
return new JedisSentinelPool("mymaster", sentinelSet, jedisPoolConfig, 60000);
}
return jedisSentinelPool;
}
}
17.7、17.6遇到的坑,我的在linux中的sentile的配置主机master的配置里面的ip是linux的本地地址(127.0.0.1),要换成linux的真实地址
todo:上面说的坑,其实细想,应该是启动1个master和多个slave的时候,就应该设置他们的ip是真实ip(不使用127.0.0.1),然后在sentinel.conf中设置master的ip为真实ip。如果master挂掉,选举其他的slave为master,也能不用再改sentinel.conf中的配置将新的master的ip变成真实ip了。
18、集群(此处案例为1主1从,有三组)
18.1、什么是集群
18.2、配置
18.3 、启动6个redis服务
18.4、将6个节点合成一个集群
根据虚拟机里面的真实ip来:
redis-cli --cluster create --cluster-replicas 1 192.168.0.4:6379 192.168.0.4:6380 192.168.0.4:6381 192.168.0.4:6389 192.168.0.4:6390 192.168.0.4:6391
18.5、进入集群
18.6、cluster nodes 命令查看集群信息
18.7、slots插槽(数据库中的每个键都属于这 16384 个插槽的其中一个)
18.8、在集群中录入值
18.9、cluster 查询集群中的值
18.10、故障修复(某一个master挂掉之后,下面的从slave会变成master。等挂掉的master重启之后,会变成slave)
注意:如果主节点下线?从节点能否自动升为主节点?注意:15秒超时
18.11、java代码
public class JedisClusterTest {
public static void main(String[] args) {
Set<HostAndPort> set =new HashSet<HostAndPort>();
set.add(new HostAndPort("120.48.77.231",6379));
JedisCluster jedisCluster=new JedisCluster(set);
jedisCluster.set("k1", "v1");
System.out.println(jedisCluster.get("k1"));
}
}
19、redis应用问题解决
19.1、缓存穿透(黑客恶意攻击)
19.1.1 定义(redis缓存中没有,数据库中查询也没有,一直重复这种操作,最终导致数据库压力过大,崩溃)—多见于黑客攻击
19.1.2、解决方案
19.2、缓存击穿(某个key过期)
19.2.1、定义(key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端数据库加载数据并回设到redis缓存,这个时候大并发的请求可能会瞬间把后端数据库压垮。)
19.2.2、解决方案
19.3、缓存雪崩(有多个key过期)
19.3.1、定义
19.3.2、解决方案
20、分布式锁
20.1、上锁:
20.2、释放锁:
20.3、java代码中对redis分布式锁的操作
第一种:加锁+解锁+uuid防止误删
第二种:加锁+解锁+uuid防止误删+lua脚本保证删除的原子性
package com.atguigu.redis_springboot.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@RestController
@RequestMapping("/redisTest")
public class RedisTestController {
@Autowired
private RedisTemplate redisTemplate;
/**
*<简述> 优化之LUA脚本保证删除的原子性
*<详细描述>
* @author Liushanshan
* @param
* @return void
*/
@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();
}
}
}
/**
*<简述> 加锁,释放锁;加入uuid防误删
*<详细描述>
* @author Liushanshan
* @param
* @return void
*/
@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){
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
//判断比较uuid值是否一样
String lockUuid = (String)redisTemplate.opsForValue().get("lock");
if(lockUuid.equals(uuid)) {
redisTemplate.delete("lock");
}
}else{
//3获取锁失败、每隔0.1秒再获取
try {
Thread.sleep(100);
testLock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@GetMapping
public String testRedis() {
//设置值到redis
redisTemplate.opsForValue().set("name","lucy");
//从redis获取值
String name = (String)redisTemplate.opsForValue().get("name");
return name;
}
}