Redis学习笔记

324 阅读17分钟

redis概述

Redis是开源的kv存储系统,nosql数据库

注:nosql泛指非关系性数据库,不遵守sql标准,不支持acid,远超sql性能

nosql适用于:数据高并发读写、海量数据读写,对数据高可扩展性

nosql不适用:事务,基于sql的结构化查询存储,处理复杂的关系,需要即席查询

value支持string,list,set,zset(sorted set --有序集合)和hash

操作是原子性的 ,并且支持不同方式的排序

redis可以配合关系型数据库做高速缓存

redis简单操作

启动:进入/usr/local/bin 运行redis-server /conf文件位置

结束:redis-cli shutdown, 也可以进入终端输入shutdown

多实例关闭:redis-cli -p portNumber shutdown

6379由来:6379在是手机按键上MERZ对应的号码,而MERZ取自意大利歌女Alessia Merz的名字。Redis 作者 Antirez 早年看电视节目,觉得 Merz 在节目中的一些话愚蠢可笑,Antirez 喜欢造“梗”用于平时和朋友们交流,于是造了一个词 "MERZ",形容愚蠢,与 "stupid" 含义相同。MERZ长期以来被Redis作者antirez及其朋友当作愚蠢的代名词。后来 Antirez 重新定义了 "MERZ" ,形容”具有很高的技术价值,包含技艺、耐心和劳动,但仍然保持简单本质“。

redis默认提供了16个数据库,从0开始,默认使用0号库

使用命令select <dbid>来切换数据库,如:select 8

redis使用单线程+多路IO复用

可以比方买票,许多人买票,通过代理买票,代理从柜台再买票。

graph TD
1 --> 黄牛
2 --> 黄牛
3 --> 黄牛
黄牛 --> 火车站

这里黄牛买票是单线程的,多个io复用体现在多个用户,谁的票买到了,再来拿,其他时候继续干自己的活,提高cpu运行效率。

key * 查看当前库中所有key

exists key 判断key是否存在,返回0/1

tpye key 看key是什么类型

del key 删除指定key数据

unlink key根据value选择非阻塞删除,仅将key从keyspace元数据中删除,真正的删除会在后续异步操作中。

expire key 10 给key设置10s的过期时间

ttl key 查看还有多少秒过期 -1永不过期, -2已过期

dbsize查看当前数据库的key数量

flushdb 清空当前库

flushall 通杀全部库

五大常用数据类型

String

简介

String是redis最基本的类型,一个key对应一个value,是二进制安全的,redis的string可以包含任何数据,比如jpg图片或者序列化的对象。

String类型是redis最基本的数据类型,一个redis字符串value最多512M

常用命令

set <key> <value> 添加键值对

get <key>通过key查询value

append <key><value>将给定的value追加到原值的末尾

strlen <key> 得到对应value的长度

setnx <key><value> key不存在的时候才设置key的值

incr <key> 将key中存储的数字+1,只能对数字值操作,为空则设置位1

decr <key> 反之

incrby/decrby <key> <length> 将key中的值+/-length。

注:incr是原子操作,不会被线程调度机制打断的操作,一旦开始,中间不会有任何context switch--切换到另一个线程,因为redis是单线程操作。

mset <key><value><key><value> 同时设置多个kv

mget <key1><key2> 同时获取多个vlue

msetnx <key><value><key><value>同时设置一个或多个kv,当且仅当所有key都不存在,由于原子性,一个失败都失败。

getrange <开始位置><结束位置> 获取值的范围,左闭右闭

setrange <开始位置><结束位置>覆写

setxt <key><过期时间><value>设置kv同时设置过期时间,单位秒

getset<k><v> 新换旧,设置新值同时获取旧值

数据结构

String的数据结构为简单动态字符串,是可修改的字符串,内部类似于java的arrayList,草用预分配冗余空间的方式来减少内存的频繁分配

扩容:字符串长度小于1m,扩容是加倍现有空间,如果超过1mb,扩容只会+1mb,字符串最大长度512mb

List

简介

单键多值,按照插入顺序排序,可以调价一个元素到列表的头/尾,底层是双向链表,对两端操作性能高,索引下标性能差。

常用命令

lpush/rpush <key><value><value> 从左/右插入一个或多个值

lpop/rpop <key>左边或右边吐出一个值,值在键在,值亡键亡

rpoplpop <key1><key2> 从key1列表右边吐出一个值,插到key2左边

lrange <key><start><stop> 按照索引下标获取元素,从左到右 0,-1表示取所有值

lindex <key><index> 按照索引下标获取元素 从左到右

llen <key> 获取列表长度

insert <key> before/after <value><newvalue> 在value的前/后面插入newvalue值

lrem <key> <n> <value> 从左边删除n个value

lset <key> <index> <value> 将列表key下标为index值替换为value

数据结构

List结构为快速链表quickList

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

多个压缩列表用链表形式组成一个quickList

将所有的元素挨着一起存储,分配的是一块连续的内存

数量较多的时候改为quicklist

由于普通链表需要附加指针太大,浪费空间,这里的列表只存int类型的数据,结构上还需要两个额外的指针prev和next

redis将链表和ziplist组成quicklist,也就是将多个ziplist使用双指针穿起来使用,挤满足了快速插入删除新能,又不会有太大的空间冗余(用链表穿起来多个数组)

Set

简介

和list类似的一个列表功能,set可以自动排重(不能重复,且无序)

是string类型的无序集合,底层是value为null的hash表,所以crud的复杂度是O(1)

常用命令

sadd <key> <value1> <value2> 将一个或多个value元素加入key中,已经存在则忽略

smembers <key> 取出key的值

sismember <key> <value> 判断key中是否有value

scard <key> 返回元素个数

srem <key> <value1> <value2> 删除集合中某个元素

spop <key> 随机从集合中吐出一个值

srandmember <key><n> 随机从集合中取出n个值,不会从集合中删除

smove <source><destination><value> 把集合中一个值从一个集合移动到另一个集合

sinter<key1><key2> 返回交集

sunion <key1><key2> 返回并集

sdiff <key1><key2> 返回差集key1中的不包含key2中的

数据结构

set数据结构是dict字典,hash表实现。

Hash

简介

value是一个string对应的field和value映射表,适合存储对象。

用户id位查找的key,存储的value用户对象包括了姓名,年龄,生日等,而用户id这个key对应了一个redis中的key。相当于value是个键值对。

常用命令

hset <key><field><value> key集合中的filed键赋值value

hget <key><field> 从key集合field取出value

hmset <key><field1><value1><field2><value2> 批量设置hash的值

hexists <key><field> 查看hash表key中,给定域field是否存在

hkeys <key>列出hash集合中所有field

hvals <key> 列出hash集合中所有value

hincrby<key><field><increment>为哈希表key中域field加上增量

hsetnx <key><field><value>将哈希表key中的域field的值设置为value,当且仅当field不存在。

数据结构

hash对应的数据结构适量中,ziplist,hashtable,field-value长度短,个数少,使用ziplist,否则是用hashtable

有序集合Zset

简介

zset和set相似,不能重复。有序集合的每个成员关联了一个评分score,这个评分备用来按照最低分到最高分排序集合成员,成员唯一,但是评分可以重复,可以通过评分或者次序快速获得一个范围的元素。访问有序集合中间元素也很快,因此可以使有序集合作为一个没有重复成员的智能列表。

常用命令

zadd <key><score1><value1><score2><value2>将一个或多个member元素及其score加入到有序集合中。

zrange<key><start><stop> [WITHSCORES] 返回有序集key中下标在start到stop之间的元素,带有WITHSCORES可以让分数和值一起返回

zrangebyscore key minmax [WITHSCORES] [limit offset count] 返回有序集key中,所有score值介于min和max之间的成员(闭区间)。

zrangebyscore key maxmin [WITHSCORES] [limit offset count] 从大到小排列

zincrby <key><increment><value> 元素score加增量

zrem <key><value>删除集合下指定值的元素

zcount<key><min><max>统计分数区间元素个数

zrank<key><value>返回该值在集合中的排名,0开始

数据结构

等价java Map<String, Double> 给每个value权重,又类似TreeSet,元素按照score排序。

底层使用了hash和跳跃表,hash作用是关联score和value,保障value元素性,通过value可以找到score。跳跃表目的给元素value通过score排序。

Redis配置文件

unit单位:只支持bytes,不支持bit,大小写不敏感

bind 127.0.0.1 只能本机访问

protected-mode no 关闭保护模式 支持远程访问

tcp-backlog 511 队列总和是未完成三次握手+完成三次握手队列

timeout 0 多久不操作超时 0 永不超时

tcp-keeplive 检测心跳 看看连接是否活着

daemonize yes 后台启动redis

.....

Redis发布订阅

简介

发布订阅是消息通信模式,发送者pub发送消息,订阅这sub接收消息,redis客户端可以订阅任意数量频道

使用

客户端1订阅:SUBSCRIBE 频道名称

客户端2发布:publish 频道名称 内容

Redis新数据类型

Bitmaps

简介

如存abc使用二进制存储其对应ASCII码,97,98,99(01100001,01100010,01100011)。合理使用操作位可以有效提高内存使用率和开发效率。

把bitmaps想象成存0和1的数组,数组的下标在bitmaps叫偏移量。

操作:

setbit <key><偏移量><0/1> 存放

getbit <key><偏移量> 取值

bitcount <key><start><end> 开始到结束1的个数

bitop and(or/not/xor) <key><key>逻辑操作

HyperLogLog

简介

便于计算基数(不重复元素个数),如访问网站,相同ip只算一个。重复元素不再添加

操作:

pfadd <key><element> [element] 添加指定元素

pfcount <key>[key] 基数数量

pfmerge <key><key>

Geospatial

简介

地理信息,元素二维坐标,经纬度设置,查询,范围查询,距离查询,经纬度hash等。

操作:

geoadd <key> <longitude><latitude><member>[long lati member] 添加

geopos <key><member>[member] 获取指定地区坐标值

geodist<key><member1><member2>[m|km|ft|mi] 获取两个位置之间的直线距离

georadius <key><long><lati> radius[m|km|ft|mi] 以给定经纬度为中心,找某一半径内的元素

Jedis操作Redis6

  1. 添加dependency
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.2.0</version>
        </dependency>
  1. java代码
        //创建jedis对象
        Jedis jedis = new Jedis("localhost", 6379);
        //测试
        System.out.println(jedis.ping());

返回PONG

注:redis.conf要注释bind 127.0.0.1,否则只能本机访问ip无法访问到,protected-mode no改为yes,否则不能远程访问,只能本机访问。

具体操作调用jedis各个方法,方法名称基本和命令行一致。

简单实现手机验证码功能

    //随机生成六位验证码
    public static String getCode() {
        Random random = new Random();
        String code = "";
        for (int i = 0; i < 6; i++) {
            int rand = random.nextInt(10);
            code += rand;
        }
        System.out.println(code);
        return code;
    }
    //把随机生成的验证码存入redis
    //一个手机号每天可以获取三个验证码,每个验证码有效期2分钟
    public static String saveToRedis(String phone, String code) {
        Jedis jedis = new Jedis("localhost", 6379);
        // 手机发送次数的key
        String countKey = "VerifyCode" + phone + ":count";
        // 验证码的key
        String codeKey = "VerifyCode" + phone + ":code";

        // 每个手机每天只能发送三次验证码
        String count = jedis.get(countKey);
        if(count == null) {
            //没发送过,设置为1
            jedis.setex(countKey, 24 * 3600, "1");
            // 把验证码存入redis
            jedis.setex(codeKey, 30, code);
            jedis.close();
        } else if(Integer.parseInt(count) <= 2){
            //验证码有效期间不能再获取验证码
            //查询不到redis中的kv
            if(jedis.get(codeKey) == null) {
                jedis.setex(codeKey, 120, code);
                jedis.incr(countKey);
            } else {
                //查询到redis中验证码还在,不更新验证码
                System.out.println("验证码获取速度过快");
            }
            jedis.close();
        } else {
            //发送三次了不能在发送了
            System.out.println("超过三次");
            jedis.close();
        }
        return "seccess";
    }

    //检测输入的验证码是否正确
    public static void getRedisCode(String phone, String code) {
        Jedis jedis = new Jedis("localhost", 6379);
        String codeKey = "VerifyCode" + phone + ":code";
        if(jedis.get(codeKey).equals(code)) {
            System.out.println("验证码正确");
        } else {
            System.out.println("验证码错误");
        }
    }

SpringBoot操作redis

  1. 依赖
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <version>2.5.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
            <version>2.9.0</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>2.10.1</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.10.1</version>
        </dependency>
  1. application.properties
#Redis服务器地址
spring.redis.host=127.0.0.1
#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

  1. config Class
@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;
    }
}
  1. ControllerDemo
@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;
    }
}

Redis事务

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

Multi、Exec、discard

从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec后,Redis会将之前的命令队列中的命令依次执行。

组队的过程中可以通过discard来放弃组队。

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

如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。

事务冲突的问题

悲观锁

每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

乐观锁

每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。

执行multi前,执行watch key1 key2... 如果在事务执行之前这些key被修改,事务将被打断。

unwatch可以解除对所有key的监视。

Redis事务三特性

单独的隔离操作

事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

没有隔离级别的概念

队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行

不保证原子性

事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚

持久化

RDB

指定时间间隔内将内存中的数据集快照写入磁盘,也就是Snapshot快照,他恢复时是将快照文件直接读到内存中。

有一定规则,多少秒内多少个key发生变化会写入磁盘。在redis.conf文件改变

设置持久化方式 手动/自动:redis.conf bgsave

redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程结束,用这个临时文件取代上次持久化好的文件,整个过程中,主进程是不用进行任何IO操作,保证高性能。相当于复制主进程,并且称为主进程的子进程,子进程进行持久化,主进程继续做自己的事儿。如果进行大规模数据恢复,且对于数据恢复的完整性不是很敏感,RDB币AOF更加高效。其最后一次持久化后的数据可能丢失,因为是设置多少个值发生变化后持久化,如果变化一部分,但是还没到持久化设置的值,服务崩掉,此时会丢失这部分数据。

RBD恢复

config get dir查询到rdb文件目录,关闭redis,将rdb拷贝到工作目录下,再次启动redis会直接加载数据。

优势

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

劣势

  • Fork时候内存中数据被克隆了一份,大约两倍的膨胀性要考虑
  • redis在fork时使用了写时拷贝基数,但是数据庞大消耗性能
  • 备份周期在一定间隔时间做一次备份,如果redis挂掉,丢失部分数据。

AOF(Append Only File)

日志形式记录每个写操作,将redis执行过的所有指令记录下(不包括读操作),只许追加文件,但是不能修改文件。AOF默认不开启。conf中appendonly修改。

AOF和RDB同时开启默认取AOF数据。

备份恢复和RDB相同,文件是appendonly.aof

缓存穿透

redis查询不到数据,出现很多非正常url访问(db里没有)

解决方案:

  • 对空值缓存,如果查询返回的数据为空,仍然把这个空结果缓存,过期时间可能会很短。
  • 设置可访问的白名单,bitmaps,id作为偏移量,和bitmap里面id比较,如果id不在其中,进行拦截。(每次都访问bitmaps,效率一般)
  • 布隆过滤器,底层也是bitmaps
  • 实时监控,发现redis命中率急剧降低,排查访问对象和访问数据,设置黑名单。

缓存击穿

数据库访问压力瞬时增加,redis中的key没有出现大量过期,redis正常运行。redis某个key过期了,而大量访问查询这个key

解决方案

  • 预先设置热门数据到redis中
  • 实时调整key的过期时长
  • 使用锁,但是会降低效率

缓存雪崩

数据库压力变大,极少时间内,出现了大量key过期

解决方案

  • 构建多级缓存架构:nginx缓存+redis缓存+其他缓存
  • 使用锁或队列:避免大量请求落在底层存储上,但是会降低效率,不适合高并发
  • 设置过期标志更新缓存,设置提前量,如果过期会触发通知另外的线程在后台更新key的缓存
  • 将缓存失效时间分散开,设置成随机值。

分布式锁

java api不能提供分布式锁问题,而系统分布在不同的服务器上,单机的并发锁失效,需要一种跨jvm的互斥机制来控制共享资源的访问,这就是分布式锁。

主流方案:

  • 基于数据库实现分布式锁
  • 基于缓存 如redis
  • 基于zookeeper

每一种分布式锁都有各自优缺点

  • 性能:redis最高
  • 可靠性: zookeeper最高

解决方案

redis命令: SETNX key value expire key time

如果担心原子性问题 set key value nx ex time 注意: SETEX key second value = SET key value EX second

释放锁:del,或者设置过期时间等他过期也能自动释放锁

可能出现的问题

abc三个线程同时操作想要上锁,a先上锁了,过期时间为10s,在具体操作之前A卡住了,并且超过10s,锁过期释放了,但是A的操作并没有结束,而此时B抢到了锁,进行B的具体操作,此时A反应过来了,手动释放锁,本来想释放A的锁,但是实际释放了B的锁

A获得锁-A卡顿-A锁过期-B获得锁-A进行具体操作-A释放锁----此时A释放的是B的锁。

  • 解决:UUID防误删,设置锁的val时候设置为唯一的uuid,解锁的时候判断uuid是否一致再进行释放,防止错误释放。

另一个问题:a和b两个操作,a先上锁,再进行具体操作,之后释放锁(del)的时候,已经判断号uuid一样了,但是此时锁正好到了过期时间,自动释放了,而此时B获取到了这个锁,进行具体操作,而A还没结束,可能会释放掉B的锁(原因缺乏原子性)

解决:使用lua脚本(支持原子操作)