1.数据类型
1.1Geospatial地理位置
- 存储地理位置(经度、纬度、名称)
- 城市距离计算、附近的人
- Geo的底层其实就是ZSet实现
1.2Hyperloglog基数统计
- Hyperloglog在Redis中是基数统计的算法,基数就是不重复的元素
- 比如一个人访问一个网站多次,但还是算一个人,Hyperloglog优点就是占用内存非常小,比set去重方法远远小,是首选,Hyperloglog常常用于计数。但它错误率有0.81%,如果允许容错就用Hyperloglog,一点都不允许就用set(例如保存用户id)
1.3Bitmaps位图
- 个人理解每一位都相当于一个Boolean判断,所有只有两种状态的都可以用Bitmaps
- 例如疫情14e人就直接存储14e个0,如果感染就设置为1
- 其他场景如:活跃用户、用户是否登录、某用户365天内打卡天数(key是用户id,365天就是365位,365/8≈46B,也就是说一个用户一年只要46B存储就够了,十分小,省内存。)
1.3String
- 可以细分为3种 String、int、float,对于字符串是用byte[]存储,对于int和float是直接转化为二进制存储,节省空间
- int和float支持自增自减
- 如果愿意的话可以把图片本身转为byte[]存入redis,但不建议,一般还是存储一个地址
1.5Hash
- 哈希表
- 相比String存一个json,存Hash更方便修改某一个特定的字段,但是也看业务场景需不需要频繁修改
1.6List
最后一个是阻塞获取,等到list中有元素时获取,当然不能超过最大超时时间
1.7Set
底层用到了散列值,Set可以有如下功能。可通过集合交并差的运算来实现例如:共同好友等功能
1.8SortedSet
- 与Java中的TreeSet的红黑树实现不同,SortedSet的排序依据是每个元素的score属性,其底层是跳表(增加查询速度)+hash表(保证不重复)
- 元素少的时候是压缩表ZipList 元素多的时候是跳表SkipList
2.Redis事务?
-
准确来说Redis不支持事务,因为事务需要满足ACID原则,而Redis是单线程,没有隔离性;运行错误时不保证原子性。Reids事务本质只是一串连续执行的命令
-
Java没有相关Redis事务的API,因此一般在Java中也是用的lua脚本
-
开启事务
multi
、 执行业务、提交exec
或放弃discard
2.1语句错误(类似编译错误)
比如在事务中途get一个不存在的值,会导致整个事务回滚
2.2运行错误
-
比如对String类型做自增操作,语法上没有问题,事务提交后除了这一句其他的全部成功
-
这也是跟Mysql不一样的地方,Mysql绝不允许事务的原子性出现问题
2.3watch监视(乐观锁)
-
该命令可以为 Redis事务提供 check-and-set (CAS)行为。
-
我们可以使用 watch命令来监视一个 或多个key,如果被监视的 key在事务执行前被修改过那么本次事务将会被取消,也就是所谓的回滚
-
只有确保被监视的 key,在事务开始前到执行 这段时间内未被修改过事务才会执行成功(类似乐观锁)
-
如果一次事务中存在被监视的 key,无论此次事务执行成功与否,该 key的监视都将会在执行后失效 也就是说监视是一次性的。
3.Redis持久化
- 如果redis只做缓存,数据敏感性不高,可以不选择持久化,直接性能拉满
- 如果同时开启两种持久化方案,优先AOF增量恢复(更完整)(重启redis即恢复)
- 一般Master节点用AOF尽可能保证完整性,Slave节点用RDB保证高效性(通常十几分钟备份一次就够了)
- RDB是通过主进程创建的fork子进程先写入临时文件再持久化;而AOF是同步的,每次写操作先写入缓冲区,到达刷盘频率后持久化;
redis持久化是指在指定的时间间隔内将内存中的数据集快照(snapshotting)写入磁盘
恢复时是将快照文件读入内存 redis提供了两种持久化方式
- RDB(Redis DataBase)(默认)
- AOF(Append of File)
3.1RDB全量备份
- redis会单独创建一个子进程(使用fork函数)来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能
- RDB的缺点是最后一次持久化后的数据可能丢失;优点是大规模恢复数据速度快
3.1.1触发方式
- 可以手动触发
save
(会阻塞Redis)或bgsave
(fork子进程) - 也可以在Redis配置文件中设置自动触发,如
save 20 3
表示在20秒内如果至少有3个key发生变化,则保存
3.1.2其他配置
-
stop-writes-on-bgsave-error
:默认值为yes。当启用了RDB且最后一次后台保存数据失败,Redis是否停止接收数据。用户可以感知是否持久化失败 -
rdbcompression
:默认值是yes。对于存储到磁盘中的快照,可以设置是否进行压缩存储。压缩会消耗大量cpu资源 -
rdbchecksum
:默认值是yes。在存储快照后,我们还可以让redis使用CRC64算法来进行数据校验,但是这样做会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能。消耗一定的性能保证持久化正确性 -
dbfilename
:设置快照的文件名,默认是 dump.rdb -
dir
:设置快照文件的存放路径,这个配置项一定是个目录,而不能是文件名。默认是和当前配置文件保存在同一目录。
3.1.3数据恢复
将备份文件 (dump.rdb) 移动到 redis 安装目录并启动服务即可,redis就会自动加载文件数据至内存了。Redis 服务器在载入 RDB 文件期间,会一直处于阻塞状态,直到载入工作完成为止。
3.2AOF增量备份
- 以日志的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来(读操作不记录)
- 只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。
3.2.1持久化过程
- 可选每次都追加、每秒追加、从不追加
- 如果是每秒追加,这一秒如果产生了巨量的并发redis写入,会导致这个AOF内存缓冲区也特别庞大(这是额外的AOF消耗,不算上redis数据本身)
- 每次追加是同步的,redis每次修改命令,会将持久化命令写到缓冲区里面;等达到落盘条件之后;从缓冲区进行落盘操作
3.2.2数据恢复
- 文件大、备份满、但是安全
- 不能单独使用AOF,(但Redis默认单独使用RDB)
- AOF用的少
4.分布式锁概述
4.1概念
- 分布式锁:满足分布式系统or集群下的多进程可见 且 互斥 的锁
- 分布式锁可以用MySQL、Redis、ZooKeeper实现,前者最简单且低效,后者最安全且低效,Redis是一个业内常用的折中方案
- 互斥、原子性、安全性 是Redis分布式锁的实现难点,Redisson是一个开源的高性能Redis分布式锁框架
- 在功能上的特点包括:阻塞or非阻塞、是否可重入、是否是公平锁等
4.2简单实现
简单实现能满足大部分场景,最简单的是在unlock的时候调用RedisTemplate.execute()来执行一段lua脚本
4.2.1实现思路与解决的问题
因为Redis分布式的本质原理是setnx+过期时间+del,所以需要注意以下几点:
- key值需要携带线程的id,以便于在del的时候先判断是不是自己上的锁再删除(防止因timeout导致的删掉其他线程上的锁)
4.2.2key值无身份标识
形如:
4.2.3解决误删问题
为了解决误删(不同线程)的问题,应在unlock的时候,先判断value中的threadid是否是当前threadid,如果是,则是删除自己上的锁
4.2.3业务阻塞:防止误删的判断
- 利用UUID区分分布式环境下不同的JVM,利用threadId区分不同的线程
- key值的UUID用于区分JVM
- value值是直接存一个threadid,用于判断是否可以删除锁
4.2.4解决误删问题
上面demo的最后一段,其实判断能不能删
和 删除
是两个步骤,这两个步骤之间虽然没有业务可以阻塞,但是可能存在因为JVM垃圾回收时STW时间过长,redis锁超时,造成2.1同样的问题
if(threadId.equals(id)){
stringRedisTemplate.delete(KEY_PREFIX+name);
}
因此,判断是否能删除(value中的threadid是否相同)、删除该锁 这两步操作必须是原子性
(一次性)的
4.2.5lua脚本保证原子性
为了避免中间可能出现的STW,引入lua语言的脚本(lua可以用脚本一次性执行多条redis语句)
4.2.6为什么不能用redis事务
- reids和mysql事务的区别
- redis的事务不支持中途回滚,他仅仅是一堆命令的集合(redis事务其实是
批处理
,比如我们想先查了再判断,其实我们是拿不到这个结果的)(redis不允许回滚、阻塞造成的性能降低);而mysql是支持回滚的 - 虽然我们可以通过redis乐观锁在释放锁的时候进行判断,但是很麻烦
4.2.7原生的执行lua脚本
脚本本质就是一个字符串,Redis提供了一个执行脚本(Sctipt)的语法EVAL " "
4.2.8RedisTemplate的execute()方法
这个API可以传入一个形参RedisScript
,其底层实现是lua脚本
4.3最终的简单实现(仍存在很多问题)
- 先创建一个脚本对象,用于释放锁的时候传参
- 然后调用,执行
- 其中配置文件unlock.lua的代码如下
基于Redis的分布式锁实现思路:
- 利用set nx ex获取锁,并设置过期时间,保存线程标示
- 释放锁时先判断线程标示是否与自己一致,一致则删除锁
特性:
- 利用set nxi满足互斥性
- 利用lua脚本保证释放锁时的一致性
- 利用set ex保证故障时锁依然能释放,避免死锁,提高安全性
- 利用Redis集群保证高可用和高并发特性
5.Redisson
上面“最终简单实现”存在如下几个问题:
- 不可重入:同一个线程,不同方法,不能获取同一个锁(因为使用的逻辑是setnx)
- 不可重试:在实际业务场景中,不能因为获取不到锁就直接false,应该有重试机制
- 超时释放问题:虽然上面用lua脚本解决了锁释放的一致性问题,但并没有解决“因为业务耗时太长导致的锁自动失效问题”
- 主从一致性问题:setnx是写操作,应发生在主节点,若此时主节点挂了,没有将刚刚setnx锁同步给从节点,那成为新主节点的这个从节点是没有这个锁的(当然这种情况发生的概率很低,主从复制延迟很低)
Redisson是一个在Redis的基础上实现的)ava驻内存数据网格(ln-Memory Data Grid)。它不仅提供了一系列的分布式的ava常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。
Redisson中文wiki文档
5.1Redisson配置
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonclient() {
//配置类
Config config = new Config();
//添redis地址,这里添加了单点的地址,也可以使用config.useClusterServers()添加集群地址
config.useSingleServer().setAddress("redis://1***6:6379").setPassword("****6");
//创建客户端
return Redisson.create(config);
}
}