Redis基础

文章详细介绍了NoSQL数据库的概念,指出其在海量数据、高并发场景下的优势,并解释了不适用于事务支持的情况。接着深入讲解了Redis数据库,包括它的数据类型如String、List、Set、Hash、Zset,以及如何使用Jedis操作Redis。此外,文章还讨论了Redis的配置文件、发布订阅、事务、持久化机制(RDB和AOF)、主从复制和哨兵模式,最后提到了Redis集群的搭建和使用。
摘要由CSDN通过智能技术生成

1.NoSQL数据库简介

多服务器的用户登录session对象如何存储?
session复制到不同的服务器会造成数据冗余
session存储在文件系统或关系型数据库中IO开销大
session存储在cookie中安全性差
session存储在redis中,速度快,存在内存中不需要IO

NoSQL=not only SQL泛指非关系型数据库
不遵循SQL标准
不支持ACID
远超SQL的性能

NoSQL适用于海量数据,高并发,数据高可扩展
NoSQL不适用于事务支持,基于SQL的结构化查询

实际中用了SQL也不行或者用不着SQL时可以考虑NoSQL

2.Redis概述安装

1.开源k-v数据库
2.支持存储的V类型更多,包括string,list,hash,set,zset等
3.数据类型支持各种操作且都是原子性的
4.支持不同方式的排序
5.数据为了保证效率存在内存中,同时支持持久化
6.实现了主从同步

1)多样的数据结构以及实际应用

最近N个数据:通过List实现按自然时间排序的数据
排行榜:zset(有序集合)
验证码:Expire
计数器,秒杀:原子性,自增方法INCR,DECR
去重:set
构建队列:list
发布订阅消息系统:pub/sub模式

2)安装

#在CentOS 7.X下安装C 语言的编译环境
yum install centos-release-scl scl-utils-build
yum install -y devtoolset-8-toolchain
scl enable devtoolset-8 bash
#测试gcc版本
gcc --version

#或者直接安装gcc
yum install gcc

#解压redis压缩包
tar -zxvf redis-6.2.1.tar.gz -C /opt/software/

#进入redis目录中使用make编译 编译完成后使用make install完成安装
cd redis-6.2.1/
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:客户端,操作入口

3)可能遇到的错误

Jemalloc/jemalloc.h:没有那个文件

如果没有准备好C语言编译环境,make会报—Jemalloc/jemalloc.h:没有那个文件
解决方案:运行make distclean 完成后再次make编译

4)Redis启动

①前台启动,不推荐

​ cd /usr/local/bin/
​ redis-server

②后台启动,推荐

​ 修改配置文件中daemonize yes
​ redis-server /opt/module/redis-6.2.1/redis.conf 启动
​ redis-cli 连接redis服务

5)Redis简单介绍

①默认端口6379来源Mrez女明星,默认16个数据库。默认使用0号库 使用select dbid来切换
②统一密码管理,所有库都用同一个密码
③单线程+多路IO复用

3.五大常用数据类型

1)Redis键(key)

​ keys *:查看当前库全部key,支持模式匹配
​ exists key:查看key是否存在
​ type key:查看key类型
​ del key:删除指定key的数据
​ unlink key:根据value选择非阻塞删除,真正的删除操作在后续异步执行
​ expire key 10:设置key过期时间,单位为秒
​ ttl key:查看还有多少秒过期,-1表示永不过期,-2表示已过期

​ dbsize 查看当前数据库的key数量 flushdb 清空当前库 flushall 清空所有库

2)Redis字符串(String)

①概述

​ Sring是二进制安全的,意味着redis的string可以包含任何数据,比如jpg的图片序列化对象,redis中string的value值最多512M

②基本操作

​ set key value:添加键值对
​ get key:获取键的值
​ append key value:将值追加到原值的末尾
​ strlen key:获取值得长度
​ setnx key value:只有当key不存在时设置key的值
​ incr key:将key中存储的数字加一,具有原子性,多线程原子性指不被其他线程打断,redis单线程能在单条指令完成的都具有原子性
​ decr key:将key中存储的数字值减一,具有原子性
​ incrby/decrby key 步长:将key中存的数字加或减步长
​ mset key1 value1 key2 value2…:批量设置kv
​ mget key1 key2…:批量获取key的value
​ msetnx key1 value1 key2 value2…:批量设置kv,只有key存在时才设置,且具有原子性,只有所有key都不存在才会成功
​ getrange key begin after:截取值得范围,类似java的substring,但是包括头也包括尾
​ setrange key begin value:从指定位置开始覆写值
​ setex key 过期时间 value:在设置值得时候同时设置过期时间
​ getset key value:设置新值得同时获取旧值

③数据结构

​ String数据结构为简单动态字符串,内部结构实现类似于java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配。
​ [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6oR8Bun4-1675950071529)(C:\Users\DELL\AppData\Roaming\Typora\typora-user-images\image-20230131215022807.png)]
​ 如图中所示,内部为当前字符串实际分配的空间capacity一般要高于实际字符串长度len。当字符串长度小于1M时,扩容都是加倍现有 的空间,如果超过1M,扩容时一次只会多扩1M的空间。需要注意的是字符串最大长度为512M。

3)Redis列表(List)

①概述

​ 单键多值,按照插入顺序排序,可以插入到列表的头部(左),也可插到列表的尾部(右),底层为双向链表,两端节点性能高,中间差。

②基本操作

​ lpush/rpush key value1 value2 … :从左边或从右边插入一个或多个值
​ lpop/rpop key:从左边/右边吐出一个值,值在键在,值空键亡
​ rpoplpush key1 key2:从key1右边吐出一个值插到key2左边
​ lrange key start stop:按照索引下标获得元素,前包后包
​ lindex key index:根据索引下标获取指定元素
​ llen key:获取列表长度
​ linsert key before/after value newvalue:在value前或后插入newvalue
​ lrem key n value:从左到右删除n个value
​ lset key index value:将列表下标为index的元素替换为value

③数据结构

4)Redis集合(Set)

①概述

​ Set是String类型的无序集合,他的底层是一个value为null的hash表,所以增删查的时间复杂度是O(1 )

②基本操作

​ sadd key value1 value2…:将一个或多个元素加入集合key中,已存在的元素将被忽略
​ smembers key:取出集合中所有值
​ sismember key value:判断集合key中是否含有value值,有返回1,没有返回0
​ scard key:返回集合中元素个数
​ srem key value1 value2…:删除集合中的某些元素
​ spop key num:随机从集合中吐出num个值,吐出后值删除
​ srandmember key num:随机从集合中取出num个值,取出后不影响原集合
​ smove key1 key2 value1:将集合key1中的value1移动到集合key2中
​ sinter key1 key2:返回两个集合的交集元素
​ sunion key1 key2:返回两个集合的并级元素
​ sdiff key1 key2:返回两个集合的差集元素(包含在key1中的,不包含在key2中的)

③数据结构

5)Redis哈希(Hash)

①概述

​ Redis hash是一个键值对集合,是一个String类型的field和value的映射表,hash适合存储对象,类似Java的Map(String,Object)

②基本操作

​ hset key field value:给key集合中的field键赋值为value
​ hget key field:获取key集合中的field的值
​ hmset key field1 value1 field2 value2:批量插入数据
​ hexists key field:判断key中field是否存在
​ hkeys key:列出集合key中所有field
​ hvals key:列出集合中所有value
​ hincrby key field num:集合中field字段值加num,num可以为负数
​ hsetnx key field value::给key集合中的field键赋值为value,当且仅当field不存在时成功

③数据结构

6)Redis有序集合(Zset)

①概述

​ 类似于普通集合,没有重复元素的字符串集合,但每个成员关联了一个score,按照从最低分到最高分的方式排序集合中的成员

②基本操作

​ zadd key score1 value1 score2 value2…:将一个或多个merber元素及值加入到有序key中
​ zrange key start stop [withscores]:返回有序集key中,下标在 中的元素
​ zrangebyscore key min max [withscores] [limit offset count]:返回集合中score在min和max之间的成员,按score从小到大排列
​ zrevrangebyscore key min max [withscores] [limit offset count]:同上从大到小排列
​ zincrby key num value1:给value1的score加上num
​ zrem key value:删除集合下指定值得元素
​ zcount key min max:统计集合中min和max之间的个数
​ zrank key value:返回该值在集合中的排名,从0开始

③数据结构

4.Reids配置文件

#只支持bytes不支持bit,大小写不敏感
######include
#可以通过include引入其他配置文件
include /path/commonconf

######网络相关配置
#默认情况bind 127.0.0.1只能接受本机的访问请求,不写的情况下,无限制接受任何ip地址的访问
#如果开启了protected-mode yes,那么在没有设定bind ip且没有设密码的情况下,Redis只允许接受本机的响应
bind 127.0.0.1
#设置tcp的backlog,backlog其实是一个连接队列,backlog队列总和=未完成三次握手队列 + 已经完成三次握手队列。
#在高并发环境下你需高backlog值来避免慢客户端连接问题。默认为511
tcp-backlog 511 
#timeout一个空闲的客户端维持多少秒会关闭,0表示关闭该功能。即永不关闭。
timeout 0
#对访问客户端的一种心跳检测,每个n秒检测一次。单位为秒,如果设置为0,则不会进行Keepalive检测,建议设置成60
tcp-keepalive 0

#####GENERAL通用
#daemonize是否为后台进程,设置为yes ,守护进程,后台启动
daemonize yes 
#pidfile存放pid文件的位置,每个实例会产生一个不同的pid文件
pidfile /var/run/redis.pid
#loglevel指定日志记录级别,Redis总共支持四个级别:debug、verbose、notice、warning,默认notice,生产选notice或warning
loglevel notice
#databases设定库的数量 默认16
databases 16
#logfile指定日志文件路径,默认为空
logfile ""

#####SECURITY安全
#requirepass设定密码
requirepass 123456

#####LIMITS限制
#maxclients设置redis同时可以与多少个客户端进行连接,默认10000
maxclients 10000
#maxmemory设置redis可以使用的内存量一旦到达内存使用上限,redis将会试图移除内部数据,移除规则可以通过maxmemory-policy来指定
#建议必须设置,否则,将内存占满,造成服务器宕机 ,默认不限制
maxmemory <bytes>
#maxmemory-policy 内存使用上限redis移除内部数据移除规则
#volatile-lru:使用LRU算法移除key,只对设置了过期时间的键;(最近最少使用)
#allkeys-lru:在所有集合key中,使用LRU算法移除key
#volatile-random:在过期集合中移除随机的key,只对设置了过期时间的键
#allkeys-random:在所有集合key中,移除随机的key
#volatile-ttl:移除那些TTL值最小的key,即那些最近要过期的key
#noeviction:不进行移除。针对写操作,只是返回错误信息

5.Redis发布订阅

​ Reids客户端可以订阅任意数量的频道,并且可以向频道发布消息,所有订阅者都将收到推送

​ subscribe channelname:订阅该名称的频道
​ publish channelname message:向该频道发消息

6.Redis6新数据类型

1)Bitmaps

2)Hyperloglog

3)Geospatial

7.Jedis操作Redis

1)连接redis

<!--Jedis需要的依赖-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.2.0</version>
</dependency>
public class JedisDemo1 {
    public static void main(String[] args) {
        //创建Jedis对象
        Jedis jedis = new Jedis("192.168.10.100", 6379);
        jedis.auth("123456");
        String ping = jedis.ping();
        System.out.println(ping);
        jedis.quit();
        jedis.close();
    }
}
//注,需要虚拟机关闭防火墙,且redis配置文件中bind注掉或者指定访问ip,protected-mode设为no
//关闭防火墙命令 systemctl stop firewalld

2)使用Jedis操作Redis

public class JedisDemo1 {
    public static void main(String[] args) {
        //创建Jedis对象
        Jedis jedis = new Jedis("192.168.10.100", 6379);
        jedis.auth("123456");
        Set<String> keys = jedis.keys("*");
        jedis.set("name","tom");
        jedis.mset("name","tom","age","22");
        //使用Jedis对象点方法操作Redis,方法名与Redis命令行中类似
        jedis.quit();
        jedis.close();
    }
}

3)模拟手机验证码发送

//1、输入手机号,点击发送后随机生成6位数字码,2分钟有效
//2、输入验证码,点击验证,返回成功或失败
//3、每个手机号每天只能输入3次
/**
 * @program: demo
 * @description: 模拟手机验证码发送
 * @author: Yosun
 * @create: 2023-02-03 22:38
 **/
public class JedisDemo2 {
    //1、输入手机号,点击发送后随机生成6位数字码,2分钟有效
    //2、输入验证码,点击验证,返回成功或失败
    //3、每个手机号每天只能输入3次
    public static void main(String[] args) throws Exception {

        String phone = getPhone();
        String code = getCode();
        String s = sendCode(phone, code);
        System.out.println(check(phone, s));
        s = sendCode(phone, code);
        System.out.println(check(phone, s));
        s = sendCode(phone, code);
        System.out.println(check(phone, "123456"));
        s = sendCode(phone, code);
        System.out.println(check(phone, s));

    }

    /*
    * 获取手机号
    * */
    public static String getPhone(){
        Scanner scanner = new Scanner(System.in);
        System.out.print("请输入手机号码:");
        return scanner.nextLine();
    }

    /*
    * 生成验证码
    * */
    public static String getCode(){
        Random random = new Random();
        String code = "";
        for (int i = 0; i <6 ; i++) {
            code += random.nextInt(10);
        }
        return code;

    }
    /*
     * 发送验证码
     * */
    public static String sendCode(String phone,String code) throws Exception {
        Jedis jedis = new Jedis("192.168.10.100", 6379);
        jedis.auth("123456");
        String count = jedis.get(phone + "count");
        if(count ==null){
            //还未发送,进行第一次发送
            jedis.setex(phone,120,code);
            jedis.setex(phone+"count",24*60*60,"1");
        }else if (Integer.parseInt(count)>=3){
            System.out.println("今日已达到发送上限,请明日再来");
            return;
        }else {
            jedis.setex(phone,120,code);
            jedis.incr(phone+"count");
        }

        jedis.quit();
        jedis.close();
        return code;
    }

    /*
     * 验证码校验
     * */
    public static boolean check(String phone,String code){
        Jedis jedis = new Jedis("192.168.10.100", 6379);
        jedis.auth("123456");
        boolean res= code.equals(jedis.get(phone));
        jedis.quit();
        jedis.close();
        return res;
    }
}

8.Springboot整合Redis

①引入依赖

<!-- redis -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- spring2.X集成redis所需common-pool2-->
<dependency>
	<groupId>org.apache.commons</groupId>
	<artifactId>commons-pool2</artifactId>
	<version>2.6.0</version>
</dependency>

②application.properties配置redis配置

#设置pom中Springboot版本为2.2.1.RELEASE
#Redis服务器地址
spring.redis.host=192.168.10.100
#Redis服务器连接端口
spring.redis.port=6379
#Redis数据库索引(默认为0)
spring.redis.database= 0
#连接超时时间(毫秒)
spring.redis.timeout=1800000
#连接池最大连接数(使用负值表示没有限制)
spring.redis.lettuce.pool.max-active=20
#最大阻塞等待时间(负数表示没限制)
spring.redis.lettuce.pool.max-wait=-1
#连接池中的最大空闲连接
spring.redis.lettuce.pool.max-idle=5
#连接池中的最小空闲连接
spring.redis.lettuce.pool.min-idle=0

③ 添加redis配置类

@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer 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);
        template.setConnectionFactory(factory);
		//key序列化方式
        template.setKeySerializer(redisSerializer);
		//value序列化
        template.setValueSerializer(jackson2JsonRedisSerializer);
		//value hashmap序列化
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        return template;
 }

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer 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);
		// 配置序列化(解决乱码的问题),过期时间600秒
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(600))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
                .disableCachingNullValues();
        RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
        return cacheManager;
    }
}

④RedisTestController中添加测试方法

@RestController
@RequestMapping("/redisTest")
public class RedisTestController {
    @Autowired
    private RedisTemplate redisTemplate;

    @GetMapping
    public String testRedis() {
        //设置值到redis
        redisTemplate.opsForValue().set("name","lucy");
        //从redis获取值
        String name = (String)redisTemplate.opsForValue().get("name");
        return name;
    }
}

9.Redis6的事务和锁机制

1)Redis中事务的定义

​ redis事务是一个单独的隔离操作,事务中的所有命令都会序列化,按顺序执行,事务在执行过程中不会被其他客户端送来的命令请求所打断,事务的主要作用就是串联多个命令,防止其他命令插队

2)Multi、Exec、discard

​ 从输入multi命令开始,之后输入的命令都将按顺序进入命令队列,但不会执行,直到输入exec后,命令队列中的命令开始按顺序执行,组队的过程中可以通过discard放弃组队。

3)事务的错误处理

如果组队中某个命令出现了错误,则所有命令都不会执行;如果执行阶段产生错误,则只有发生错误的那条执行失败,其他命令依旧会执行。

4)事务的冲突问题

①悲观锁

​ 每次操作都认为有其他人会操作数据,故每次操作前都上锁,解锁后别人才可以操作,缺点是效率低,适用于写多的场景

②乐观锁

​ 为数据加一个版本号,所有人都能得到数据,每个人在修改时不光修改数据,也同步修改版本号,其他人操作时检查当前数据版本号和数据库中版本号是否匹配,适用于读多写少的场景

③乐观锁在Redis的实现

​ whatch key [key …]:在执行multi之前,先执行watch key1 [key2],可以监视一个(或多个) key ,如果在事务执行之前这个(或这些) key被其他命令所改动,那么事务将被打断。

​ unwatch:取消 WATCH 命令对所有 key 的监视。如果在执行 WATCH 命令之后,EXEC 命令或DISCARD 命令先被执行了的话,那么就不需要再执行UNWATCH 了。

5)秒杀案例

//简单版  存在超时和超卖问题
//加入连接池	解决超时问题
//加入事务-乐观锁 解决超卖问题,但引入库存遗留问题
//使用lua脚本的方式解决库存遗留问题

public class Test {
    public static void main(String[] args) {
		String userid = new Random().nextInt(50000) +"" ;
		String prodid =request.getParameter("prodid");
		
		boolean isSuccess=SecKill_redis.doSecKill(userid,prodid);
		boolean isSuccess= SecKill_redisByScript.doSecKill(userid,prodid);
    }
}

//普通乐观锁版
public class SecKill_redis {

	public static void main(String[] args) {
		Jedis jedis =new Jedis("192.168.44.168",6379);
		System.out.println(jedis.ping());
		jedis.close();
	}

	//秒杀过程
	public static boolean doSecKill(String uid,String prodid) throws IOException {
		//1 uid和prodid非空判断
		if(uid == null || prodid == null) {
			return false;
		}

		//2 连接redis
		//Jedis jedis = new Jedis("192.168.44.168",6379);
		//通过连接池得到jedis对象
		JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
		Jedis jedis = jedisPoolInstance.getResource();

		//3 拼接key
		// 3.1 库存key
		String kcKey = "sk:"+prodid+":qt";
		// 3.2 秒杀成功用户key
		String userKey = "sk:"+prodid+":user";

		//监视库存
		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 判断如果商品数量,库存数量小于1,秒杀结束
		if(Integer.parseInt(kc)<=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;
		}

		//7.1 库存-1
		//jedis.decr(kcKey);
		//7.2 把秒杀成功用户添加清单里面
		//jedis.sadd(userKey,uid);

		System.out.println("秒杀成功了..");
		jedis.close();
		return true;
	}
}



//lua脚本版
public class SecKill_redisByScript {
	private static final  org.slf4j.Logger logger =LoggerFactory.getLogger(SecKill_redisByScript.class) ;
	public static void main(String[] args) {
		JedisPool jedispool =  JedisPoolUtil.getJedisPoolInstance();
		Jedis jedis=jedispool.getResource();
		System.out.println(jedis.ping());	
		Set<HostAndPort> set=new HashSet<HostAndPort>();
	//	doSecKill("201","sk:0101");
	}
	
	static 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" ;
			 
	static String secKillScript2 = 
			"local userExists=redis.call(\"sismember\",\"{sk}:0101:usr\",userid);\r\n" +
			" return 1";

	public static boolean doSecKill(String uid,String prodid) throws IOException {

		JedisPool jedispool =  JedisPoolUtil.getJedisPoolInstance();
		Jedis jedis=jedispool.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;
	}
}


//Jedis连接池
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, "192.168.44.168", 6379, 60000 );
				}
			}
		}
		return jedisPool;
	}

	public static void release(JedisPool jedisPool, Jedis jedis) {
		if (null != jedis) {
			jedisPool.returnResource(jedis);
		}
	}

LUA脚本是类似redis事务,有一定的原子性,不会被其他命令插队,可以完成一些redis事务性的操作。

但是注意redis的lua脚本功能,只有在Redis 2.6以上的版本才可以使用。

利用lua脚本淘汰用户,解决超卖问题。

redis 2.6版本以后,通过lua脚本解决争抢问题,实际上是redis 利用其单线程的特性,用任务队列的方式解决多任务并发问题

10.Redis持久化之RDB

1)什么是RDB

​ RDB:指定的时间间隔内将内存中的数据集快照写入磁盘

2)备份是如何执行的

​ Redis会单独创建一个子进程(Fork)进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件,整个过程中,主进程是不进行任何io操作的,确保了高性能,如果需要进行大规模数据恢复吗,且对数据完整性不是特别敏感,那选择RDB要比AOF更加高效。

3)Fork

​ Fork的作用是复制一个与当前进程一样的进程,新进程的所有数据(变量,环境变量,程序计数器等)与原进程一致,作为原进程的子进程

4)配置

#数据库持久化文件名
dbfilename dump.rdb
#数据库持久化文件路径
dir ./
#当持久化写入操作发生错误时是否继续写入yes :不继续写入
stop-writer-on-bgsave-error yes
#持久化文件是否进行压缩存储,压缩算法为Lzf
rdbcompression yes
#持久化之后是否检查数据完整性 性能消耗大约百分之10,CRC64算法
rdbchecksum yes 
#设置何时持久化,redis6默认关闭,save 900 100含义为如果在900秒中有100个key发生了变化,那么久进行持久化 操作
save 900 100
#一般推荐使用bgsave,会在后台进行异步快照操作,快照同时还可以响应客户端请求
bgsave 900 100
#可以通过lastsave命令获取最后一次成功执行快照的时间

5)优势、劣势

​ 优:适合大规模的数据恢复
​ 对完整性、一致性要求不高的场景适用
​ 节省磁盘空间
​ 恢复速度快
​ 劣:Fork时,内存中的数据被克隆了一份,大致2倍的膨胀性需要注意
​ 虽然fork使用了写时拷贝技术,但数据大时性能消耗需要考虑
​ 周期性备份,Redis挂掉的话会损失最后一次快照的所有修改

6)RDB持久化流程图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pmPoOI4g-1675950071531)(C:\Users\DELL\AppData\Roaming\Typora\typora-user-images\image-20230205195847826.png)]

7)RDB备份恢复

​ 将备份好的数据库文件放在备份文件的存放地址,并改成和备份文件相同的名字,启动redis,就可以恢复数据

11.Redis持久化之AOF

1)什么是AOF

​ 全称为Append Only File,以日志的形式记录每个写操作,读操作不记录,只许追加文件但不可以改写文件,redis启动之初会读取该文件将所有指令执行一遍,重新构建数据
​ AOF默认不开启

#默认不开启
appendonly yes
#生成文件名
appendfilename "appendonly.aof"

2)AOF备份恢复

​ 同RDB

3)AOF异常恢复

​ 在AOF文件损坏后,可以使用启动目录下的redis-check-aof --fix filename进行修复

4)AOF配置

#AOF同步频率设置
#always始终同步,每次写入都立刻记入日志,性能较差,但完整性好
#everysec 每秒一次,宕机会丢失一秒数据
#no 不主动同步,同步时机交给操作系统
appendfsync always

5)Rewrite压缩

重写压缩是什么?

​ 将set a a1
​ set b b1两条命令压缩为set a a1 b b1

原理

​ AOF文件过大时,fork一条新进程出来将文件重写(也是先写临时文件再rename)
​ Redis4.0后的重写 ,是指上就是把rdb 的快照,以二级制的形式附在新的aof头部,作为已有的历史数据,替换掉原来的流水账操作。

#不写入aof文件只写入缓存,用户请求不会阻塞,但是在这段时间如果宕机会丢失这段时间的缓存数据。(降低数据安全性,提高性能)
no-appendfsync-on-rewrite=yes
#还是会把数据往磁盘里刷,但是遇到重写操作,可能会发生阻塞。(数据安全,但是性能降低)
no-appendfsync-on-rewrite=no

何时重写

​ Redis会记录上次重写时的AOF大小,默认配置是当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时触发
​ 或者输入命令bgrewriteaof
​ auto-aof-rewrite-percentage:设置重写的基准值,文件达到100%时开始重写(文件是原来重写后文件的2倍时触发)
​ auto-aof-rewrite-min-size:设置重写的基准值,最小文件64MB。达到这个值开始重写。
AOF当前大小>= base_size +base_size*100% (默认)且当前大小>=64mb(默认)

重写流程

​ (1)bgrewriteaof触发重写,判断是否当前有bgsave或bgrewriteaof在运行,如果有,则等待该命令结束后再继续执行。

​ (2)主进程fork出子进程执行重写操作,保证主进程不会阻塞。

​ (3)子进程遍历redis内存中数据到临时文件,客户端的写请求同时写入aof_buf缓冲区和aof_rewrite_buf重写缓冲区保证原AOF文件完整以及新AOF文件生成期间的新的数据修改动作不会丢失。

​ (4)①.子进程写完新的AOF文件后,向主进程发信号,父进程更新统计信息。
​ ②.主进程把aof_rewrite_buf中的数据写入到新的AOF文件。

​ (5)使用新的AOF文件覆盖旧的AOF文件,完成AOF重写。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ln7zi3gB-1675950071532)(C:\Users\DELL\AppData\Roaming\Typora\typora-user-images\image-20230205211541371.png)]

6)优势、劣势

​ 优: 备份机制更稳健,丢失数据概率更低。
​ 可读的日志文本,通过操作AOF稳健,可以处理误操作。
​ 劣:比起RDB占用更多的磁盘空间。
​ 恢复备份速度要慢。
​ 每次读写都同步的话,有一定的性能压力。
​ 存在个别Bug,造成恢复不能。

12.Redis主从复制

1)是什么,以及能干嘛

​ 主机数据更新后根据配置和策略,自动同步到备机的master/slave机制,Master以写为主,Slave以读为主
​ 可以做到读写分离,性能扩展
​ 容灾快速恢复
​ [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZGl76fIw-1675950071533)(C:\Users\DELL\AppData\Roaming\Typora\typora-user-images\image-20230206211845375.png)]

2)怎么玩

​ ①拷贝多个redis.conf文件
​ ②配置一主两从,在配置文件中配置

daemonize yes
pidfile 6379.pid/6380.pdi/6381.pid
port 6379/6380/6381
dbfilename 6379.rdb/6380.rdb/6381.rdb
appendonly no

​ ③启动三台Redis,使用命令info replication查看当前服务器主从情况
​ ④配从(库)不配主(库)slaveof 成为某个实例的从服务器
​ ⑤在主机上可以读写数据,在从机上写数据会报错
​ ⑥主机挂掉,重启就行,一切如初,从机重启需重设:slaveof 127.0.0.1 6379,可以将配置增加到文件中。永久生效
从服务器挂掉后,不会重新加入master,需要重新执行命令,执行后从服务器重新复制主服务器的数据
主服务器挂掉之后小弟不会上位,重启后依然是主服务器

3)主从复制原理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3D6M1QkZ-1675950071534)(E:\尚硅谷\java\尚硅谷Redis6视频课程\笔记\分析图\03-主从复制原理.png)]

4)薪火相传

​ 配置: a slaveof b
​ b slaveof c
​ 相当于主服务器是c,使用薪火相传模式
​ 风险是一旦某个slave宕机,后面的slave都没法备份

5)反客为主

​ 大哥挂掉后在从机执行slaveof no one 将从机变为主机

6)复制延时

​ 由于所有的写操作都是先在Master上操作,然后同步更新到Slave上,所以从Master同步到Slave机器有一定的延迟,当系统很繁忙的时候,延迟问题会更加严重,Slave机器数量的增加也会使这个问题更加严重

13.Redis哨兵模式(反客为主自动版)

1) 是什么

反客为主的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库
image-20230206222300807

2)怎么玩

​ ①在一主二从模式下新建sentinel.conf文件,名字绝不能错
​ ②配置哨兵,填写内容sentinel monitor mymaster 127.0.0.1 6379 1
​ 其中mymaster为监控对象起的服务器名称, 1 为至少有多少个哨兵同意迁移的数量。
​ ③redis-sentinel /myredis/sentinel.conf 启动哨兵,默认端口为26379
​ ④当主机挂掉,从机选举中产生新的主机
​ 哪个从机会被选举为主机呢?根据优先级别:slave-priority 原主机重启后会变为从机。
image-20230206225921214

​ 优先级在redis.conf中默认:slave-priority 100(老版本Redis该配置项名有些变化),值越小优先级越高
​ 偏移量是指获得原主机数据最全的
​ 每个redis实例启动后都会随机生成一个40位的runid

3)Java代码连接Redis的哨兵模式

private static JedisSentinelPool jedisSentinelPool=null;

public static  Jedis getJedisFromSentinel(){
	if(jedisSentinelPool==null){
            Set<String> sentinelSet=new HashSet<>();
            sentinelSet.add("192.168.11.103:26379");//ip连接改为哨兵端口

            JedisPoolConfig jedisPoolConfig =new JedisPoolConfig();
            jedisPoolConfig.setMaxTotal(10); //最大可用连接数
		   jedisPoolConfig.setMaxIdle(5); //最大闲置连接数
		   jedisPoolConfig.setMinIdle(5); //最小闲置连接数
		   jedisPoolConfig.setBlockWhenExhausted(true); //连接耗尽是否等待
		   jedisPoolConfig.setMaxWaitMillis(2000); //等待时间
		   jedisPoolConfig.setTestOnBorrow(true); //取连接的时候进行一下测试 ping pong
		   //sentinel.conf中配置的名称mymaster
		   jedisSentinelPool=new JedisSentinelPool("mymaster",sentinelSet,jedisPoolConfig);
	 	   return jedisSentinelPool.getResource();
	}else{
		   return jedisSentinelPool.getResource();
    }
}


14.Redis集群

1)集群可能出现的问题

​ 主从模式,薪火相传,主机宕机,导致ip发生变化,应用程序中需要修改对应的主机地址、端口等信息
​ 之前通过代理主机来解决,但是在Redis3.0中提供了无中心化集群配置

2)集群搭建

port 6379
pidfile "/var/run/redis_6379.pid"
dbfilename "dump6379.rdb"
dir "/home/bigdata/redis_cluster"
logfile "/home/bigdata/redis_cluster/redis_err_6379.log"
cluster-enabled yes
#设定节点配置文件名
cluster-config-file nodes-6379.conf
#设定节点失联时间,超过该毫秒,集群自动进行主从切换 
cluster-node-timeout 15000
#启动Redis,确保nodes-xxxx.conf文件生成正常
#执行如下命令建立集群
cd /opt/redis-6.2.1/src
redis-cli --cluster create --cluster-replicas 1 192.168.11.101:6379 192.168.11.101:6380 192.168.11.101:6381 192.168.11.101:6389 192.168.11.101:6390 192.168.11.101:6391
#--replicas 1 采用最简单的方式配置集群,一台主机,一台从机,正好三组。
#执行后看到all 16384 slots covered表示集群搭建成功
#-c采用集群策略连接,设置数据会自动切换到相应的写主机
redis-cli -c -p 6379
#通过cluster nodes 命令查看集群信息
cluster nodes

3)集群详情

①redis cluster 如何分配这六个节点

​ 一个集群至少要有三个主节点。
​ 选项 --cluster-replicas 1 表示我们希望为集群中的每个主节点创建一个从节点。
分配原则尽量保证每个主数据库运行在不同的IP地址,每个从库和主库不在一个IP地址上。

②什么是slots

​ 一个 Redis 集群包含 16384 个插槽(hash slot), 数据库中的每个键都属于这 16384 个插槽的其中一个
​ 集群使用公式 CRC16(key) % 16384 来计算键 key 属于哪个槽, 其中 CRC16(key) 语句用于计算键 key 的 CRC16 校验和
redis-cli -c –p 6379 登入后,再录入、查询键值对可以自动重定向。
​ 不在一个slot下的键值,是不能使用mget,mset等多键操作
​ [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PAb8ln3R-1675950071534)(C:\Users\DELL\AppData\Roaming\Typora\typora-user-images\image-20230207213541707.png)]
​ 可以通过{}来定义组的概念,从而使key中{}内相同内容的键值对放到一个slot中去。
​ [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oOLTKdIi-1675950071535)(C:\Users\DELL\AppData\Roaming\Typora\typora-user-images\image-20230207213555540.png)]

4)集群命令

​ 1.获取key对应的插槽值
​ cluster keyslot cust
​ 2.计算插槽中有几个键
​ cluster countkeysinslot 12706 如果该插槽不在执行命令的服务器上,则返回0
​ 3.返回插槽中键的数量
​ cluster getkeysinslot 4847 10

5)集群的故障恢复

​ 主节点挂掉后从节点上位,主节点重连后变为从节点
如果所有某一段插槽的主从节点都宕掉,redis服务是否还能继续?
​ 如果某一段插槽的主从都挂掉,而cluster-require-full-coverage 为yes ,那么 ,整个集群都挂掉
​ 如果某一段插槽的主从都挂掉,而cluster-require-full-coverage 为no ,那么,该插槽数据全都不能使用,也无法存储

6)集群的Jedis开发

public class JedisClusterTest {
  public static void main(String[] args) { 
     Set<HostAndPort>set =new HashSet<HostAndPort>();
     set.add(new HostAndPort("192.168.31.211",6379));
     JedisCluster jedisCluster=new JedisCluster(set);
     jedisCluster.set("k1", "v1");
     System.out.println(jedisCluster.get("k1"));
  }
}

7)集群优势、劣势

​ 优势:
​ 实现扩容
​ 分摊压力
​ 无中心配置相对简单
​ 不足:
​ 多键操作是不被支持的
多键的Redis事务是不被支持的。lua脚本不被支持
​ 由于集群方案出现较晚,很多公司已经采用了其他的集群方案,而代理或者客户端分片的方案想要迁移至redis cluster,需要整体迁移而不是逐步过渡,复杂度较大。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值