Redis基础笔记


一. Redis概述安装.

1. redis安装

安装目录:/usr/local/bin

  • redis-benchmark:性能测试工具
  • redis-check-aof:修复有问题的AOF文件
  • redis-check-dump:修复有问题的dump.rdb文件
  • redis-sentinel:redis集群使用
  • redis-server:redis服务器启动命令
  • redis-cli:客户端,操作入口

2. 启动&关闭

  1. 先将redis.conf复制一份到 /etc/redis.conf
  2. 修改redis.conf中的 daemonize noyes,表示支持后台启动
  3. 进入 /usr/local/bin 目录,redis-server /etc/redis.conf,启动redis
  4. ps -ef | grep redis ,查看redis进程,127.0.0.1:6379
  5. redis-cli(客户端连接redis)之后显示 127.0.0.1:6379 表示连接上,输入ping,返回pong表示正常连接
  6. 关闭时可以1:shutdown;2:找到进程 kill - 9 进程号

3. redis相关知识

  • 默认端口6379,默认16个数据库,初始默认使用0号库
  • 使用 select 切换数据库
  • 所有库密码相同

单线程+多路IO复用

二. 五大数据类型

1. redis键(key)

  • keys *:查看当前库所有key
  • exists key:查询某个key是否存在,1存在,0不存在
  • type key:查看key是什么类型
  • del key:删除指定key的数据
  • unlink key:根据 value 选择非阻塞删除,仅将key从keyspace元数据中删除,真正的删除会在后续异步操作
  • expire key 秒:给设定的key设置过期时间(秒)
  • ttl key:查询 key 还有多少秒过期,-1表示永不过期,-2表示过期
  • dbsize: 查看当前数据库的key数量
  • flushdb:清空当前库
  • flushall:通杀全部库

2. 字符串(String)

String类型是二进制安全的,所以可以包含任何数据,如jpg图片

一个Redis 中字符串的value最多512M

  • set :添加键值对
  • get :获取键的值
  • append <增加的值>:给对应key的值增加值
  • strlen :得到值的长度
  • setnx :只有key不存在时,设置key的值,返回0设置失败,1设置成功
  • incr :将key中存的数字的值加1,只能操作数字,如果key为空,则设置值为1
  • decr :将key中存的数字的值减1,只能操作数字,如果key为空,则设置值为-1
  • incrby/decrby <步长>:将key中存储的数字值增/减步长

Redis中特有名词——原子性

incr 原子操作:不会被线程调度机制打断的操作,一旦开始就一直运行到结束

单线程中,能够在单条指令中完成的操作都可以认为是原子操作,因为中断只能发生于指令之间

多线程中,不能被其他进程/线程打断的操作就叫原子操作

Redis单命令的原子性主要得益于Redis的单线程

  • mset :同时设置一个或多个键值对
  • mget :同时获取一个或多个v
  • msetnx :当且仅当所有k都不存在时同时设置一个或多个键值对,返回0失败,1成功
  • getrange <起始位置> <结束位置>:获取值的范围,类似substring,前包后包
  • setrange <起始位置> :从起始位置开始覆盖值
  • setex <过期时间> :设置键值对时设置过期时间,单位秒
  • getset :设置新值同时获取旧值

String数据结构是简单动态字符串,类似 ArrayList,采用预分配空间的方式减少内存频繁分配,字符串长度小于1M时,扩容都是加倍现有空间,超过1M时,扩容一次只会+1M,最大长度512M

3. 列表(List)

  1. 单键多值字符串列表
  2. 底层双向链表
  • lpush/rpush :从左边/右边插入一个或多个值
  • lpop/rpop :从左边/右边吐出一个值,值在键在,值光键亡
  • rpoplpush :从k1右边吐出一个值插入到k2左边
  • lrange :按照索引下标从左到右获取元素,0和-1表示取所有值
  • lindex :按照索引下标获取元素,从左到右
  • llen :获取列表长度
  • linsert before/after :在value前面/后面插入newvalue
  • lrem n :从左边删除n 个value
  • lset :将key值中index位置的值替换为value

List数据结构为快速链表 quickList

列表元素较少时会使用一块连续的内存存储,结构是zipList,压缩列表

数量较多时才会改为quickList

Redis将链表和zipList结合起来形成 quickList,就是将多个zipList使用双向指针连接起来,既满足快速插入删除,又不会出现太大的空间冗余

4. 集合(Set)

和list相似,但Set会自动去重,Redis的Set是String类型中的无序集合,底层是一个value为null的哈希表,添加删除查找都是 o(1)

  • sadd :添加集合元素
  • smembers :取出集合的所有值
  • sismember :判断集合k中是否含有该v,有1无0
  • scard :返回集合元素个数
  • srem :删除集合某些元素
  • spop :随机从集合k中吐出一个值
  • srandmember :随机从集合中取出n个值,不会从集合中删除
  • smove :把k1中的v1取出放到k2中
  • sinter :返回两个集合交集
  • sunion :返回两个集合并集
  • sdiff :返回两个集合差集(在k1不在k2)

Set数据结构是 dirc 字典,字典是用哈希表实现的

5. 哈希(Hash)

Redis哈希是一个键值对集合,类似 Map<String, Object>

  • hset :给key集合中的 field键赋值value
  • hget :从k集合中的field中取出数据
  • hmset :批量设置hash的值
  • hexists :查看key中的field是否存在,0不存在,1存在
  • hkeys :列出该hash集合中的所有field
  • hvals :列出该hash集合中的所有value
  • hincrby :为哈希表key中的field的值加上increment
  • hsetnx :只有field不存在时会添加成功

Hash类型对应数据结构有两种,zipList(压缩列表),hashtable(哈希表),当field-value长度较短且个数较少时,使用zipList,否则使用hashtable

6. 有序集合(Zset(sorted set))

zset和set相似,去重且有序

每个成员关联一个评分,用来按照最低分到最高分的方式排序集合中的成员,成员唯一但评分可以重复

可以根据评分或次序获取成员

  • zadd :添加成员
  • zrange [withscores] :返回有序集合中start到stop中的成员,带withscores,返回分数和值的成员,0和-1表示所有
  • zrangebyscore min max [withscores] [limit offset count]:返回key中所有score介于min和max之间(双闭)的成员,按score递增排序
  • zrevrangebyscore max- min [withscores] [limit offset count]:同上,但从大到小
  • zincrby :为成员的score加上value
  • zrem :删除集合指定值的成员
  • zcoutn :统计该集合分数区间的元素个数
  • zrank :返回该值在集合中的排名,从0开始

SortedSet(zset) 是 Redis 提供的一个特别的数据结构,一方面等价于Map<String, Double>,可以给每一个元素赋予一个权重score,又类似于TreeSet,内部元素按照score排序

底层使用了两个数据结构

  1. hash,关联value和score
  2. 跳跃表,目的在于给value排序,根据score的范围获取元素列表

三. 配置文件

前面把配置文件放到了**/etc/redis.con**f文件中

  1. 开头配置大小单位,只支持bytes,不支持bit,不区分大小写
  2. include包含其他配置文件
  3. bind 127.0.0.1:默认127.0.0.1只能接收本地的访问请求,可以注释掉
  4. protected-mode yes:开启保护模式,只能本机访问,可以改成no
  5. port 6379:端口号6379
  6. tcp-backlog 511:设置tcp的backlogbacklog是一个连接队列,backlog队列总和=未完成三次握手队列+已经完成三次握手队列,高并发环境下需要一个高backlog避免客户端连接慢的问题
  7. time-out 0:表示永不超时,单位秒
  8. tcp-keepalive 300:300秒不操作结束连接
  9. daemonize yes:设置支持后台启动
  10. pidfile /var/run/redis_6379.pid:在这保存进程号
  11. loglevel notice:设置日志级别
    • debug > verbose > notice > warning:大的信息多
  12. logfile “”:设置日志输出路径
  13. database 16
  14. 可以在security设置密码,命令中设置密码是临时的
  15. maxclients 10000:设置redis同时可以与多少个客户端进行连接,达到限制之后会拒绝新的请求,已注释
  16. maxmemory :建议必须设置,否则内存占满造成服务器宕机,设置redis可以使用的内存量,达到上限后会根据移除规则maxmemory-policy
  17. maxmemory-policy:移除规则
    1. voolaile-lru:使用LRU算法移除key,只对设置了过期时间的键
    2. allkeys-lru:在所有集合key中,使用LRU算法移除key
    3. volatile-random:在过期集合中移除随机的key,只对设置了过期时间的key
    4. allkeys-random:在所有key中随机删除key
    5. volatile-ttl:移除TTL值最小的key,即快过期的key
    6. noeviction:不移除,针对写操作,只是返回错误信息
  18. maxmemory-samples:设置样本数量,LRU算法和最小TTL算法并非精确算法,而是估算值,所以可以设置样本大小,redis默认会检查这么多个key并选择其中LRU的那个,一般设置3-7,越小越不准确,但性能消耗越小

四. 发布&订阅

Redis的发布订阅(pub/sub)是一种消息通信模式,发送者(pub)发送消息,订阅者(sub)接收消息

Redis 客户端可以订阅任意数量的频道

  • subscribe 频道名称:一个客户端订阅频道
  • publish 频道名称 信息:给频道发送信息

五. Redis6新数据类型

1. Bitmaps

合理使用位操作可以有效提高内存使用率和开发效率

Bitmaps可以实现位操作

  1. Bitmaps本身不是一种数据类型,本身是字符串,但可以对字符串的位进行操作
  2. 可以把Bitmaps想像成一个以位为单位的数组,数组的每个单元只能存0/1,数组下标叫做偏移量
  • setbit :设置Bitmaps中某个偏移量的值(0/1),offset从0开始

第一次初始化Bitmaps时,如果偏移量太大,那么整个初始化执行会比较慢,可能会导致Redis的阻塞

  • getbit :获取Bitmaps中某个偏移量的值
  • bitcount [start end]:统计字符串被设置为1的bit数,-1表示到一,-2表示倒二
  • bitop and(or/not/xor) [key…]:复合操作,交集并集非异或,结果保存在destkey中

一般用于保存活跃用户,设置为1

2. HyperLogLog

基数:不重复元素个数

  • pfadd [element…]:添加指定元素到HyperLogLog中
  • pfcount [k…]:计算HLL的近似基数
  • pfmerge [sourcekey…]:将一个或多个sk中的结果合并后存到dk中

3. Geospatial

Redis3.2增加了对GEO类型的支持,Geographic,地址信息缩写,就是元素的2维坐标,基于该类型提供了经纬度设置,查询,范围查询,距离查询,经纬度hash等操作

  • geoadd [longitude latitude member]:添加地理位置(经度,纬度,名称),两级无法直接添加,有效经度(-180-180),有效纬度(-85.05112878-85.05112878)
  • geopos [member…]:获取指定地区的坐标值
  • geodist [m(默认)|km|ft(英尺)|mi(英米)]:获取两个位置之间的直线距离
  • georadius radius m|km|ft|mi:以给定的经纬度为中心,找出某一半径的元素

六. Jedis操作Redis6

//创建一个Jedis对象
Jedis jedis = new Jedis("192.168.200.130", 6379);
//测试
String value = jedis.ping();
System.out.println(value); //打印pong表示连接成功,连接前要确定redis.conf中
1.daemonize  yes
2.bind  127.0.0.1  注释这一行
3.protected-mode  no
之后防火墙开启6379端口

1. 案列:发送验证码

  1. 输入手机号,点击发送后随机生成6位数字码,2分钟有效
  2. 输入验证码,点击验证,返回成功或失败
  3. 每个手机号每天只能输入3次

public static void main(String[] args) {
//        verifyCode("10089");
//        getRedisCOde("10089", "979338");
    }

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

    //2. 每个手机每天只能发送3次,验证码放到redis,设置过期时间
    public static void verifyCode(String phone){
        //连接redis
        Jedis jedis = new Jedis("192.168.200.130", 6379);
        String countKey = "VerifyCode" + phone + ":count"; //手机发送次数key
        String codeKey = "VerifyCode" + phone + ":code"; //验证码 key

        String count = jedis.get(countKey);
        if(count == null){ //该手机没发送过验证码
            jedis.setex(countKey, 24*60*60, "1");
        } else if(Integer.parseInt(count) <= 2){ //发送过验证码,但不到三次
            //发送次数+1
            jedis.incr(countKey);
        } else if(Integer.parseInt(count) > 2){ //发送验证码次数超过3次
            System.out.println("今天发送次数已经超过3次");
            jedis.close();
            return;
        }

        //发送的验证码要放到redis
        String vCode = getCode();
        jedis.setex(codeKey, 120, vCode);

        jedis.close();
    }

    //3. 验证码校验
    public static void getRedisCOde(String phone, String code){
        Jedis jedis = new Jedis("192.168.200.130", 6379);
        String codeKey = "VerifyCode" + phone + ":code"; //验证码 key
        String redisCode = jedis.get(codeKey);
        //判断
        if(code.equals(redisCode)){
            System.out.println("成功");
        } else{
            System.out.println("失败");
        }
        jedis.close();
    }

七. Redis6事务操作

Redis事务是一个单独的隔离操纵:事务中的所有命令都会序列化、按顺序地执行,事务在执行的过程中,不会被其他客户端发送来的命令请求所打断

Redis事务的主要作用就是串联多个命令防止别的命令插队

1. multi,exec,discard

输入Multi命令开始,输入的命令会依次进入命令队列中,但不会执行,直到输入Exec后,Redis会将之前的命令队列中的命令依次执行,组队过程中可以通过discard放弃组队

组队中某个命令出现了错误时执行时整个所有队列都会被取消

如果执行阶段某个命令出现了错误,则只有出现错误的命令不执行,其他命令都执行

2. Redis事务冲突处理

悲观锁:每次都感觉不安全,处理之前先上锁,处理之后释放锁

乐观锁:给数据增加版本号字段,修改数据之后版本号自动改变,其他人修改数据时先检查版本号,一致则继续,不一致则重新获取数据,更新的时候会判断一下有没有人去修改这个数据,适用于多读的类型,提高吞吐量,Redis就是利用这种check-and-set机制实现事务的

在这里插入图片描述

3. watch key [key…]:乐观锁

执行multi之前,先执行watch key1 [key2…],可以监视一个或多个key,如果在事务执行之前(exec时),key别其他命令所改动,那么事务会被打断

  • unwatch:取消watch命令对所有key的监视,如果在执行watch命令之后,exec命令或discard命令先被执行的话,就不需要再执行unwatch命令了

4. Redis事务三特性

  1. 单独的隔离操作:
    • 事务中的所有命令都会被序列化,按顺序执行,事务在执行过程中,不会被其他客户端发送的命令请求打断
  2. 没有隔离级别概念
    • 队列中的命令没有提交之前不会被执行,因为事务提交前任何指令都不会被执行
  3. 不保证原子性
    • 事务中如果有一条命令执行失败,其他命令仍然会被执行,没有回滚

5. 秒杀案例

//秒杀过程
	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;
	}
  • ab -n 1000 -c 100 http://192.168…/seckill:访问1000次,其中100次是并发操作

乐观锁造成库存遗留问题

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

八. Redis持久化之RDB

Redis提供2个不同形式的持久化方式:RDB(Redis DataBase),AOF

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

备份的执行方式:

​ Redis会单独创建(fork)一个子进程进行持久化,会先将数据写入到一个临时文件中,等持久化过程结束之后,再用这个临时文件替换上次持久化好的文件,整个过程中,主进程不进行任何IO操作,确保了极高的性能,如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB比AOF高效,唯一缺点是最后一次持久化的数据可能丢失

Fork:

  • Fork作用是复制一个和当前进程一样的进程,新进程的所有数据和原进程一致,但是是一个全新的进程,并作为原进程的子进程
  • 在linux中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,linux引入了写时复制技术
  • 一般父进程和子进程会共用同一段物理内存,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程

在redis.conf配置文件名称,默认 dump.rdb

redis.conf文件中

  • dbfilename dump.rdb
  • dir ./:默认dump.rdb在redis启动目录创建
  • stop-writes-bgsave-error yes:当redis无法写入磁盘时,直接关掉Redis的写操作,推荐yes
  • rdbcompression yes:压缩文件,对于存储到磁盘中的快照,可以设置是否压缩存储,如果yes,redis会采用LZF算法进行压缩,如果不想消耗CPU进行压缩,可以关闭,推荐yes
  • rdbchecksum yes:检查完整性,在存储快照后,可以让redis使用CRC64算法进行数据校验,但会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭,推荐yes
  • save 秒 写操作次数:RDB是整个内存的压缩过的Snapshot,RDB的数据结构可以配置复合的快照触发条件,默认1分钟内改了1万次,或5分钟内改了10次,或15分钟内改了1次,就进行持久化,禁用,不设置save,或者给save 传入空字符串

save:save只管保存,其他不管,全部阻塞,手动保存,不建议

bgsave:redis会在后台异步进行快照操作,快照同时还可以响应客户端请求,可以通过lastsave命令获取最后依次成功执行快照的时间

执行flushall也会产生dump.rdb,但里面是空的

1. RDB优势

  1. 适合大规模的数据恢复
  2. 对数据完整性和一致性要求不高更适合使用
  3. 节省磁盘空间
  4. 恢复速度快

2. RDB劣势

  1. Fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑
  2. 虽然Redis在fork时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能。
  3. 在备份周期在一定间隔时间做一次备份,所以如果Redis意外down掉的话,就会丢失最后一次快照后的所有修改。

3. RDB的备份

先通过config get dir 查询rdb文件的目录

将*.rdb的文件拷贝到别的地方

rdb的恢复

  • 关闭Redis
  • 先把备份的文件拷贝到工作目录下 cp dump2.rdb dump.rdb
  • 启动Redis, 备份数据会直接加载,要改回dump.rdb

4. RDB总结

动态停止RDB:redis-cli config set save “”#save后给空值,表示禁用保存策略

在这里插入图片描述

九. Redis持久化之AOF

Append Only FIle

日志的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来(读操作不记录), 只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作

1. AOF流程

  1. 客户端的请求写命令会被append追加到AOF缓冲区内;
  2. AOF缓冲区根据AOF持久化策略[always,everysec,no]将操作sync同步到磁盘的AOF文件中;
  3. AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量;
  4. Redis服务重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cYsSpGUi-1661829993633)(C:\Users\shang\Desktop\Redis\分析图\04.png)]

2.AOF启动,修复,恢复

AOF默认不开启,可以在redis.conf中配置文件名称,默认为 appendonly.aof,AOF文件的保存路径,同RDB的路径一致。AOF和RDB同时开启,系统默认取AOF的数据(数据不会存在丢失)

AOF的备份机制和性能虽然和RDB不同, 但是备份和恢复的操作同RDB一样,都是拷贝备份文件,需要恢复时再拷贝到Redis工作目录下,启动系统即加载。

  1. 正常恢复

    • 修改默认的appendonly no,改为yes

    • 将有数据的aof文件复制一份保存到对应目录(查看目录:config get dir)

    • 恢复:重启redis然后重新加载

  2. 异常恢复

    • 修改默认的appendonly no,改为yes

    • 如遇到AOF文件损坏**,通过/usr/local/bin/**redis-check-aof–fix appendonly.aof进行恢复

    • 备份被写坏的AOF文件

    • 恢复:重启redis,然后重新加载

3. AOF同步频率

  • appendfsync always:始终同步,每次Redis的写入都会立刻记入日志;性能较差但数据完整性比较好

  • appendfsync everysec:每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失。

  • appendfsync no:redis不主动进行同步,把同步时机交给操作系统。

4. Rewrite压缩

AOF采用文件追加方式,文件会越来越大,为避免出现此种情况,新增了重写机制, 当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩, 只保留可以恢复数据的最小指令集.可以使用命令bgrewriteaof

原理

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

no-appendfsync-on-rewrite

如果 no-appendfsync-on-rewrite=yes ,不写入aof文件只写入缓存,用户请求不会阻塞,但是在这段时间如果宕机会丢失这段时间的缓存数据。(降低数据安全性,提高性能)

如果 no-appendfsync-on-rewrite=no, 还是会把数据往磁盘里刷,但是遇到重写操作,可能会发生阻塞。(数据安全,但是性能降低)

触发机制,何时重写

Redis会记录上次重写时的AOF大小,默认配置是当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时触发

重写虽然可以节约大量磁盘空间,减少恢复时间。但是每次重写还是有一定的负担的,因此设定Redis要满足一定条件才会进行重写。

auto-aof-rewrite-percentage:设置重写的基准值,文件达到100%时开始重写(文件是原来重写后文件的2倍时触发)

auto-aof-rewrite-min-size:设置重写的基准值,最小文件64MB。达到这个值开始重写。

例如:文件达到70MB开始重写,降到50MB,下次什么时候开始重写?100MB

系统载入时或者上次重写完毕时,Redis会记录此时AOF大小,设为base_size,

如果Redis的AOF当前大小>= base_size +base_size*100% (默认)且当前大小>=64mb(默认)的情况下,Redis会对AOF进行重写。

重写流程:

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

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

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

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YBZga4Pr-1661829993634)(C:\Users\shang\Desktop\Redis\分析图\05.png)]

优势:

  • 备份机制更稳健,丢失数据概率更低。
  • 可读的日志文本,通过操作AOF稳健,可以处理误操作。

劣势:

  • 比起RDB占用更多的磁盘空间(还要存指令)。
  • 恢复备份速度要慢。
  • 每次读写都同步的话,有一定的性能压力。
  • 存在个别Bug,造成恢复不能。

5. 总结

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V1sPoRrL-1661829993634)(C:\Users\shang\Desktop\Redis\分析图\06.png)]

官方推荐两个都启用。如果对数据不敏感,可以选单独用RDB。不建议单独用 AOF,因为可能会出现Bug。如果只是做纯内存缓存,可以都不用。

官网建议:

  • RDB持久化方式能够在指定的时间间隔能对你的数据进行快照存储
  • AOF持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以redis协议追加保存每次写的操作到文件末尾.
  • Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大
  • 只做缓存:如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化方式.
  • 同时开启两种持久化方式
  • 在这种情况下,当redis重启的时候会优先载入AOF文件来恢复原始的数据, 因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整.
  • RDB的数据不实时,同时使用两者时服务器重启也只会找AOF文件。那要不要只使用AOF呢?
  • 建议不要,因为RDB更适合用于备份数据库(AOF在不断变化不好备份), 快速重启,而且不会有AOF可能潜在的bug,留着作为一个万一的手段。
  • 性能建议

因为RDB文件只用作后备用途,建议只在Slave上持久化RDB文件,而且只要15分钟备份一次就够了,只保留save 900 1这条规则。 如果使用AOF,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只load自己的AOF文件就可以了。 代价,一是带来了持续的IO,二是AOF rewrite的最后将rewrite过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的。 只要硬盘许可,应该尽量减少AOF rewrite的频率,AOF重写的基础大小默认值64M太小了,可以设到5G以上。 默认超过原大小100%大小时重写可以改到适当的数值。

十. Redis主从复制

主机数据更新后根据配置和策略, 自动同步到备机的master/slaver机制,Master以写为主,Slave以读为主

优点:

  • 读写分离,性能扩展
  • 容灾快速恢复

>[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jWoMSaDQ-1661829993634)(C:\Users\shang\Desktop\Redis\分析图\07.png)]

主从复制流程:

  1. 拷贝多个redis.conf文件include(写绝对路径)
  2. 开启daemonize yes
  3. Pid文件名字pidfile
  4. 指定端口port
  5. Log文件名字
  6. dump.rdb名字dbfilename
  7. Appendonly 关掉或者换名字

例如,新建redis6379.conf,填写

include /myredis/redis.conf
pidfile /var/run/redis_6379.pid
port 6379
dbfilename dump6379.rdb

之后以同样形式添加其他端口的conf文件

  • info replication:打印主从复制的相关信息

配从不配主

slaveof

成为某个实例的从服务器

  1. 在主机上写操作,从机上读操作
  2. 主机挂掉重启后,主机还是主机
  3. 从机重启需要重新配置主机,也可以将配置添加到文件中,永久生效

1. 常用3招

  1. 一主二仆
  2. 薪火相传:上一个Slave可以是下一个slave的Master,Slave同样可以接收其他 slaves的连接和同步请求,那么该slave作为了链条中下一个的master, 可以有效减轻master的写压力,去中心化降低风险。
    • 用 slaveof ,中途变更转向:会清除之前的数据,重新建立拷贝最新的风险是一旦某个slave宕机,后面的slave都没法备份,主机挂了,从机还是从机,无法写数据了
  3. 反客为主:当一个master宕机后,后面的slave可以立刻升为master,其后面的slave不用做任何修改。用 slaveof no one 将从机变为主机。

2. 复制原理

  • Slave启动成功连接到master后会发送一个sync命令
  • Master接到命令启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令, 在后台进程执行完毕之后,master将传送整个数据文件到slave,以完成一次完全同步
  • 全量复制:而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中。
  • 增量复制:Master继续将新的所有收集到的修改命令依次传给slave,完成同步
  • 但是只要是重新连接master,一次完全同步(全量复制)将被自动执行

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bK9geHUQ-1661829993635)(C:\Users\shang\Desktop\Redis\分析图\08.png)]

3. 哨兵模式

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

使用步骤:

调整为一主二仆模式,自定义的/myredis目录下新建sentinel.conf文件,内容sentinel monitor mymaster 127.0.0.1 6379 1,其中mymaster为监控对象起的服务器名称, 1 为至少有多少个哨兵同意迁移的数量。

  • 执行redis-sentinel /myredis/sentinel.conf :启动哨兵

主机挂掉后,会从从机选新的主机

replica-priority 10

设置从机的优先级,值越小,优先级越高,用于选举主机时使用。默认100

复制延时:

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

故障恢复:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DiZl7sRK-1661829993636)(C:\Users\shang\Desktop\Redis\分析图\09.png)]

优先级在redis.conf中默认:replica-priority 100,值越小优先级越高

偏移量是指获得原主机数据最全的

每个redis实例启动后都会随机生成一个40位的runid

/*主从复制*/
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如何进行扩容?

并发写操作, redis如何分摊?

另外,主从模式,薪火相传模式,主机宕机,导致ip地址发生变化,应用程序中配置需要修改对应的主机地址、端口等信息。

之前通过代理主机来解决,但是redis3.0中提供了解决方案。就是无中心化集群配置。


Redis集群:

Redis 集群实现了对Redis的水平扩容,即启动N个redis节点,将整个数据库分布存储在这N个节点中,每个节点存储总数据的1/N。

Redis 集群通过分区(partition)来提供一定程度的可用性(availability): 即使集群中有一部分节点失效或者无法进行通讯, 集群也可以继续处理命令请求。

1. 集群步骤

  1. rdb,aof文件都删除掉。
  2. 制作6个实例,6379,6380,6381,6389,6390,6391,配置信息文件
配置其中之一
include /home/bigdata/redis.conf
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  设定节点失联时间,超过该时间(毫秒),集群自动进行主从切换。
  1. 启动6个redis服务
  2. 组合之前,请确保所有redis实例启动后,nodes-xxxx.conf文件都生成正常。
  3. 合体:cd /opt/redis-6.2.1/src
  4. 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,此处使用真实ip
  5. [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8dcJR95P-1661829993636)(C:\Users\shang\Desktop\Redis\分析图\10.png)]
    此时普通方式登录,可能直接进入读主机,存储数据时,会出现MOVED重定向操作。所以,应该以集群方式登录。-c 采用集群策略连接,设置数据会自动切换到相应的写主机
  6. cluster nodes:查看集群信息

2. 集群信息

  1. redis cluster 如何分配这六个节点?

    • 一个集群至少要有三个主节点。选项 –cluster-replicas 1 表示我们希望为集群中的每个主节点创建一个从节点。分配原则尽量保证每个主数据库运行在不同的IP地址,每个从库和主库不在一个IP地址上。
  2. slots:一个 Redis 集群包含 16384 个插槽(hash slot), 数据库中的每个键都属于这 16384 个插槽的其中一个, 集群使用公式 CRC16(key) % 16384 来计算键 key 属于哪个槽, 其中 CRC16(key) 语句用于计算键 key 的 CRC16 校验和 。

    集群中的每个节点负责处理一部分插槽。 举个例子, 如果一个集群可以有主节点, 其中:

    节点 A 负责处理 0 号至 5460 号插槽。

    节点 B 负责处理 5461 号至 10922 号插槽。

    节点 C 负责处理 10923 号至 16383 号插槽

3. 集群操作

  1. 在集群中录入值

    • 在redis-cli每次录入、查询键值,redis都会计算出该key应该送往的插槽,如果不是该客户端对应服务器的插槽,redis会报错,并告知应前往的redis实例地址和端口。

      redis-cli客户端提供了 –c 参数实现自动重定向。

      如 redis-cli -c –p 6379 登入后,再录入、查询键值对可以自动重定向。

      不在一个slot下的键值,是不能使用mget,mset等多键操作。

      可以通过{}来定义组的概念,从而使key中{}内相同内容的键值对放到一个slot中去。

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VsNoe22m-1661829993637)(C:\Users\shang\Desktop\Redis\分析图\11.png)]

  2. cluster getkeysinslot :返回 count 个 slot 槽中的键。

4. 故障恢复

​ 如果主节点下线?从节点能自动升为主节点注意:15秒超时

主节点恢复后,主节点回来变成从机。

如果所有某一段插槽的主从节点都宕掉,redis服务是否还能继续?

如果某一段插槽的主从都挂掉,而cluster-require-full-coverage 为yes ,那么 ,整个集群都挂掉

如果某一段插槽的主从都挂掉,而cluster-require-full-coverage 为no ,那么,该插槽数据全都不能使用,也无法存储。

redis.conf中的参数 cluster-require-full-coverage

5. 集群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"));
  }
}

6. 集群总结

好处:

  1. 实现扩容
  2. 分摊压力
  3. 无中心配置相对简单

不足:

  1. 多键操作是不被支持的
  2. 多键的Redis事务是不被支持的。lua脚本不被支持
  3. 由于集群方案出现较晚,很多公司已经采用了其他的集群方案,而代理或者客户端分片的方案想要迁移至redis cluster,需要整体迁移而不是逐步过渡,复杂度较大。

十二. Redis应用问题解决

1. 缓存穿透

key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会压到数据源,从而可能压垮数据源。比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-juTE4lqM-1661829993638)(C:\Users\shang\Desktop\Redis\分析图\12.png)]

解决方案:

一个一定不存在缓存及查询不到的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。

解决方案:

(1) **对空值缓存:**如果一个查询返回的数据为空(不管是数据是否不存在),我们仍然把这个空结果(null)进行缓存,设置空结果的过期时间会很短,最长不超过五分钟

(2) 设置可访问的名单(白名单):

使用bitmaps类型定义一个可以访问的名单,名单id作为bitmaps的偏移量,每次访问和bitmap里面的id进行比较,如果访问id不在bitmaps里面,进行拦截,不允许访问。

(3) 采用布隆过滤器:(布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数)。

布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。)

将所有可能存在的数据哈希到一个足够大的bitmaps中,一个一定不存在的数据会被 这个bitmaps拦截掉,从而避免了对底层存储系统的查询压力。

(4) **进行实时监控:**当发现Redis的命中率开始急速降低,需要排查访问对象和访问的数据,和运维人员配合,可以设置黑名单限制服务

2. 缓存击穿

key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Sc0Xr5u3-1661829993638)(C:\Users\shang\Desktop\Redis\分析图\13.png)]

解决方案:

key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题。

解决问题:

(1)预先设置热门数据:**在redis高峰访问之前,把一些热门数据提前存入到redis里面,加大这些热门数据key的时长

(2)实时调整:**现场监控哪些数据热门,实时调整key的过期时长

(3)使用锁:**

​ (1) 就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db。

​ (2) 先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX)去set一个mutex key

​ (3) 当操作返回成功时,再进行load db的操作,并回设缓存,最后删除mutex key;

​ (4) 当操作返回失败,证明有线程在load db,当前线程睡眠一段时间再重试整个get缓存的方法。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qSbO7ixj-1661829993639)(C:\Users\shang\Desktop\Redis\分析图\14.png)]

3. 缓存雪崩

key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

缓存雪崩与缓存击穿的区别在于这里针对很多key缓存,前者则是某一个key正常访问

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aP61TyRN-1661829993639)(C:\Users\shang\Desktop\Redis\分析图\15.png)]

缓存失效瞬间

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KZfNwP9d-1661829993640)(C:\Users\shang\Desktop\Redis\分析图\16.png)]

解决方案:

缓存失效时的雪崩效应对底层系统的冲击非常可怕!

解决方案:

(1) **构建多级缓存架构:**nginx缓存 + redis缓存 +其他缓存(ehcache等)

(2) 使用锁或队列:

用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。不适用高并发情况

(3) 设置过期标志更新缓存:

记录缓存数据是否过期(设置提前量),如果过期会触发通知另外的线程在后台去更新实际key的缓存。

(4) 将缓存失效时间分散开:

比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

4. 分布式锁

随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!

分布式锁主流的实现方案:

  1. 基于数据库实现分布式锁

  2. 基于缓存(Redis等)

  3. 基于Zookeeper

每一种分布式锁解决方案都有各自的优缺点:

  1. 性能:redis最高

  2. 可靠性:zookeeper最高

这里,我们就基于redis实现分布式锁。

解决方案:

redis:命令

# set sku:1:info “OK” NX PX 10000

EX second :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。

PX millisecond :设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。

NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。

XX :只在键已经存在时,才对键进行设置操作。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V6N7PoTc-1661829993640)(C:\Users\shang\Desktop\Redis\分析图\17.png)]

  1. 多个客户端同时获取锁(setnx)

  2. 获取成功,执行业务逻辑{从db获取数据,放入缓存},执行完成释放锁(del)

  3. 其他客户端等待重试

@GetMapping("testLock")
public void testLock(){
    //1获取锁,setne
    Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111");
    //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();
        }
    }
}
基本实现。
问题:setnx刚好获取到锁,业务逻辑出现异常,导致锁无法释放
解决:设置过期时间,自动释放锁。

5. 优化之设置锁的过期时间

设置过期时间有两种方式:

  1. 首先想到通过expire设置过期时间(缺乏原子性:如果在setnx和expire之间出现异常,锁也无法释放)

  2. 在set时指定过期时间(推荐)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rivSseqV-1661829993641)(C:\Users\shang\Desktop\Redis\分析图\18.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-klvfFAjc-1661829993641)(C:\Users\shang\Desktop\Redis\分析图\19.png)]

压力测试肯定也没有问题。自行测试

问题:可能会释放其他服务器的锁。

场景:如果业务逻辑的执行时间是7s。执行流程如下

  1. index1业务逻辑没执行完,3秒后锁被自动释放。

  2. index2获取到锁,执行业务逻辑,3秒后锁被自动释放。

  3. index3获取到锁,执行业务逻辑

  4. index1业务逻辑执行完成,开始调用del释放锁,这时释放的是index3的锁,导致index3的业务只执行1s就被别人释放。

最终等于没锁的情况。

解决:setnx获取锁时,设置一个指定的唯一值(例如:uuid);释放前获取这个值,判断是否自己的锁

6. 优化之UUID防误删

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W6njlt8S-1661829993642)(C:\Users\shang\Desktop\Redis\分析图\20.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WKX5CHMQ-1661829993642)(C:\Users\shang\Desktop\Redis\分析图\21.png)]

问题:删除操作缺乏原子性。

场景:

  1. index1执行删除时,查询到的lock值确实和uuid相等

uuid=v1

set(lock,uuid);]

  1. index1执行删除前,lock刚好过期时间已到,被redis自动释放

在redis中没有了lock,没有了锁。

]

  1. index2获取了lock

index2线程获取到了cpu的资源,开始执行方法

uuid=v2

set(lock,uuid);

  1. index1执行删除,此时会把index2的lock删除

index1 因为已经在方法中了,所以不需要重新上锁。index1有执行的权限。index1已经比较完成了,这个时候,开始执行

删除的index2的锁!

7. 优化之lUA脚本保证删除原子性

@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();
        }
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vvAQESKN-1661829993644)(C:\Users\shang\Desktop\Redis\分析图\22.png)]

项目中使用

  1. 定义key,key应该是为每个sku定义的,也就是每个sku有一把锁。
String locKey ="lock:"+skuId; // 锁住的是每个商品的数据
Boolean lock = redisTemplate.opsForValue().setIfAbsent(locKey, uuid,3,TimeUnit.SECONDS);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pV858jVt-1661829993645)(C:\Users\shang\Desktop\Redis\分析图\23.png)]

8. 总结

  1. 加锁

    // 1. 从redis中获取锁,set k1 v1 px 20000 nx
    String uuid = UUID.randomUUID().toString();
    Boolean lock = this.redisTemplate.opsForValue()
          .setIfAbsent("lock", uuid, 2, TimeUnit.SECONDS);
    
    
  2. 使用LUA释放锁

    // 2. 释放锁 del
    String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    // 设置lua脚本返回的数据类型
    DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
    // 设置lua脚本返回类型为Long
    redisScript.setResultType(Long.class);
    redisScript.setScriptText(script);
    redisTemplate.execute(redisScript, Arrays.asList("lock"),uuid);
    
    
  3. 重试

    Thread.sleep(500);
    testLock();
    
    

为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件

  • 互斥性。在任意时刻,只有一个客户端能持有锁。
  • 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
  • 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
  • 加锁和解锁必须具有原子性。

十三. Redis6.0新功能

1. ACL

Redis ACL是Access Control List(访问控制列表)的缩写,该功能允许根据可以执行的命令和可以访问的键来限制某些连接。

在Redis 5版本之前,Redis 安全规则只有密码控制 还有通过rename 来调整高危命令比如 flushdb , KEYS* , shutdown 等。Redis 6 则提供ACL的功能对用户进行更细粒度的权限控制 :

(1)接入权限:用户名和密码

(2)可以执行的命令

(3)可以操作的 KEY

参考官网:https://redis.io/topics/acl

  • acl list:展现用户权限列表

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tivuY211-1661829993645)(C:\Users\shang\Desktop\Redis\分析图\24.png)]

  • acl cat:查看添加权限指令类别

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HRm53jzM-1661829993646)(C:\Users\shang\Desktop\Redis\分析图\25.png)]加参数类型名可以查看类型下具体命令[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wDwJ2SG3-1661829993646)(C:\Users\shang\Desktop\Redis\分析图\26.png)]
    在这里插入图片描述

  • acl whoami:命令查看当前用户

  • aclsetuser:创建和编辑用户ACL

    • ACL规则

      下面是有效ACL规则的列表。某些规则只是用于激活或删除标志,或对用户ACL执行给定更改的单个单词。其他规则是字符前缀,它们与命令或类别名称、键模式等连接在一起。

      ACL规则
      类型参数说明
      启动和禁用用户on激活某用户账号
      off禁用某用户账号。注意,已验证的连接仍然可以工作。如果默认用户被标记为off,则新连接将在未进行身份验证的情况下启动,并要求用户使用AUTH选项发送AUTH或HELLO,以便以某种方式进行身份验证。
      权限的添加删除+将指令添加到用户可以调用的指令列表中
      -从用户可执行指令列表移除指令
      +@添加该类别中用户要调用的所有指令,有效类别为@admin、@set、@sortedset…等,通过调用ACL CAT命令查看完整列表。特殊类别@all表示所有命令,包括当前存在于服务器中的命令,以及将来将通过模块加载的命令。
      -@从用户可调用指令中移除类别
      allcommands+@all的别名
      nocommand-@all的别名
      可操作键的添加或删除~添加可作为用户可操作的键的模式。例如~*允许所有的键
    • 通过命令创建新用户默认权限

      acl setuser user1

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EHsOXHvp-1661829993647)(C:\Users\shang\Desktop\Redis\分析图\27.png)]
      在上面的示例中,我根本没有指定任何规则。如果用户不存在,这将使用just created的默认属性来创建用户。如果用户已经存在,则上面的命令将不执行任何操作。

    • 设置有用户名、密码、ACL权限、并启用的用户

      acl setuser user2 on >password ~cached:* +get

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qMGDebRk-1661829993647)(C:\Users\shang\Desktop\Redis\分析图\28.png)]

2. IO多线程

Redis6终于支撑多线程了,告别单线程了吗?

IO多线程其实指客户端交互部分网络IO交互处理模块多线程,而非执行命令多线程。Redis6执行命令依然是单线程。

原理架构:

Redis 6 加入多线程,但跟 Memcached 这种从 IO处理到数据访问多线程的实现模式有些差异。Redis 的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程。之所以这么设计是不想因为多线程而变得复杂,需要去控制 key、lua、事务,LPUSH/LPOP 等等的并发问题。整体的设计大体如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AWyS4xAc-1661829993648)(C:\Users\shang\Desktop\Redis\分析图\29.png)]

另外,多线程IO默认也是不开启的,需要再配置文件中配置

io-threads-do-reads yes

io-threads 4

3. 工具支持Cluster

之前老版Redis想要搭集群需要单独安装ruby环境,Redis 5 将 redis-trib.rb 的功能集成到 redis-cli 。另外官方 redis-benchmark 工具开始支持 cluster 模式了,通过多线程的方式对多个分片进行压测。

4. Redis新功能持续关注

Redis6新功能还有:

1、RESP3新的 Redis 通信协议:优化服务端与客户端之间通信

2、Client side caching客户端缓存:基于 RESP3 协议实现的客户端缓存功能。为了进一步提升缓存的性能,将客户端经常访问的数据cache到客户端。减少TCP网络交互。

3、Proxy集群代理模式:Proxy 功能,让 Cluster 拥有像单实例一样的接入方式,降低大家使用cluster的门槛。不过需要注意的是代理不改变 Cluster 的功能限制,不支持的命令还是不会支持,比如跨 slot 的多Key操作。

4、Modules API

Redis 6中模块API开发进展非常大,因为Redis Labs为了开发复杂的功能,从一开始就用上Redis模块。Redis可以变成一个框架,利用Modules来构建不同系统,而不需要从头开始写然后还要BSD许可。Redis一开始就是一个向编写各种系统开放的平台。

acl setuser user1

[外链图片转存中...(img-EHsOXHvp-1661829993647)]在上面的示例中,我根本没有指定任何规则。如果用户不存在,这将使用just created的默认属性来创建用户。如果用户已经存在,则上面的命令将不执行任何操作。
  • 设置有用户名、密码、ACL权限、并启用的用户

    acl setuser user2 on >password ~cached:* +get

    [外链图片转存中…(img-qMGDebRk-1661829993647)]

2. IO多线程

Redis6终于支撑多线程了,告别单线程了吗?

IO多线程其实指客户端交互部分网络IO交互处理模块多线程,而非执行命令多线程。Redis6执行命令依然是单线程。

原理架构:

Redis 6 加入多线程,但跟 Memcached 这种从 IO处理到数据访问多线程的实现模式有些差异。Redis 的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程。之所以这么设计是不想因为多线程而变得复杂,需要去控制 key、lua、事务,LPUSH/LPOP 等等的并发问题。整体的设计大体如下:

[外链图片转存中…(img-AWyS4xAc-1661829993648)]

另外,多线程IO默认也是不开启的,需要再配置文件中配置

io-threads-do-reads yes

io-threads 4

3. 工具支持Cluster

之前老版Redis想要搭集群需要单独安装ruby环境,Redis 5 将 redis-trib.rb 的功能集成到 redis-cli 。另外官方 redis-benchmark 工具开始支持 cluster 模式了,通过多线程的方式对多个分片进行压测。

4. Redis新功能持续关注

Redis6新功能还有:

1、RESP3新的 Redis 通信协议:优化服务端与客户端之间通信

2、Client side caching客户端缓存:基于 RESP3 协议实现的客户端缓存功能。为了进一步提升缓存的性能,将客户端经常访问的数据cache到客户端。减少TCP网络交互。

3、Proxy集群代理模式:Proxy 功能,让 Cluster 拥有像单实例一样的接入方式,降低大家使用cluster的门槛。不过需要注意的是代理不改变 Cluster 的功能限制,不支持的命令还是不会支持,比如跨 slot 的多Key操作。

4、Modules API

Redis 6中模块API开发进展非常大,因为Redis Labs为了开发复杂的功能,从一开始就用上Redis模块。Redis可以变成一个框架,利用Modules来构建不同系统,而不需要从头开始写然后还要BSD许可。Redis一开始就是一个向编写各种系统开放的平台。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值