Redis基础与Spring的集成

Redis基础

redis默认有16个数据库 默认使用第0个 可以使用select切换

redis单线程 细粒度 cpu不是redis的瓶颈 他的瓶颈是内存和网络带宽,所以用单线程来做

为啥快? redis将所有数据全放在内存中 所以说单线程效率就是最高的,多线程上下文会切换,对于内存来说,不切换效率就是最高的

1. Linux安装启动Redis

1.1 linux安装redis 4.0.9

  1. tar -xvf redis-4.0.9.tar.gz

  2. mv redis-4.0.9 redis

  3. cd redis

  4. sudo make && sudo make install

  5. sudo vim redis.conf

#bind 127.0.0.1 # 将这行代码注释,监听所有的ip地址,外网可以访问
protected-mode no # 把yes改成no,允许外网访问
daemonize yes # 把no改成yes,后台运行

1.2 启动Redis

redis.conf :配置文件
redis-cli :redis的客户端 如配置了密码 在启动后要 (auth 密码) 进行验证
redis-server :redis的服务器端

redis提供了服务端命令和客户端命令:

  • redis-server 服务端命令,可以包含以下参数:
    • start 启动
    • stop 停止
  • redis-server redis.conf 与配置文件启动

  • redis-cli 客户端控制台,包含参数:

    • -h xxx 指定服务端地址,缺省值是127.0.0.1
    • -p xxx 指定服务端端口,缺省值是6379

2. 数据结构与操作

2.1 数据结构

redis是典型的k-v形式的数据库:key(字符串),value格式的数据

value的5种类型:

  • string 字符串
  • hash 哈希类型
  • list 列表类型
  • set 集合类型
  • zset 有序集合类型

value的3种特殊数据类型:

  • Geospatial 地理位置
  • Hyperloglog 基数 (不重复的数) 统计
  • Bitmap 位图

2.2 操作5个基本类型

2.2.1 通用操作

keys * 查找所有键

type key 获取键对应值的类型

del key 删除指定key-value

flushdb 清空当前数据库

flushall 清空全部数据库

exist xx 判断某个键是否存在

expire xxx 10 xxx键10秒后过期

type xxx 查看key的类型

2.2.2 string

类比于 字符串(map<string,string>)

场景

  1. 计数器

  2. 统计多单位的数量

  3. 粉丝数

  4. 对象缓存存储

添加

  • set xxx v

获取

  • get xxx

删除

  • del xxx

append xxx "hello" 在键xxx后追加hello 如果key不存在相当于新建

strlen xxx 获取xxx长度

incr xxx 对于数值类型的自增

incrby xxx 10 对于数值类型的自增,手动设置步长

decr xxx 对于数值类型的自减

decrby xxx 10 对于数值类型的自减,手动设置步长

getrange xxx 0 3 获取字符串的0到3位 0到-1就是整个字符串

setrange xxx 0 3 替换字符串的0到3位 0到-1就是整个字符串

setex xxx 30 val 在设置的同时设置过期时间(set with expire) xxx30秒过期值为val

setnx xxx val 分布式锁: 不存在再设置(set if not exist) xxx不存在才设置xxx值为val

mset x1 v1 x2 v2 x3 v3 设置多个键值对 2个为一组

mget x1 x2 x3 获取多个值

m可以结合上面的命令建立多个,是原子性操作

getset xxx val 先get xxx的值 再set 新值 返回旧值 不存在则返回nil

2.2.3 list

类比于 双端队列(map<string,list>) 可以添加一个元素到列表的头部或者尾部(左边或者右边)

场景:

  1. 队列

添加

  • lpush xxx value 将值加到列表左边
  • rpush xxx value 将值加到列表右边

获取

  • lrange xxx start end 范围获取 //lrange mylist 0 -1 获取所有

删除

  • lpop xxx 从列表左边删除一个元素,并返回删除的元素
  • rpop xxx 从列表右边删除一个元素,并返回删除的元素

lindex xxx 1 获取左数第1个值 下标从0开始

llen xxx 获取xxx的长度

lrem xxx num val 从左到右移除num个val的值,精确匹配

ltrim xxx start stop 截取xxx指定长度 从start位置到stop位置

rpoplpush x1 x2 从x1的右边pop 再push进x2的左边

lset xxx index newVal 在xxx的index位置设置新值(xxx必须存在该位置)

linsert xxx before "v1" "v2" 在xxx的v1值前插入v2

linsert xxx after "v1" "v2" 在xxx的v1值后插入v2

2.2.4 set

(无序, 不允许元素重复) 类比于(map<string,set>)

场景

  1. 共同关注,共同爱好
  2. 推荐好友

添加

  • sadd xxx value sadd myset a b c d e

获取

  • smembers xxx 获取集合中所有元素

删除

  • srem xxx value 删除set集合中的指定元素

sismember xxx v 判断xxx中是否包含v 1包含 0不包含

scard xxx 获取xxx中元素个数

srandmember xxx num 从xxx中随机取出num个元素(num不填默认为1个)

spop xxx 从xxx中随机删除元素

smove x1 x2 v 将x1中的v 移动到x2中

sdiff x1 x2 求两个集合的差集

sinter x1 x2 求 x1 与x2 两个集合的交集

sunion x1 x2 求x1与x2的并集

2.2.5 hash

哈希类型 类比于 (map<string,map<string,string>>)

场景

  1. 更适合对象的存储

存储

  • hset xxx key value //hset myhash username lisi //hset myhash age 23

获取

  • 获取指定键的值 hget xxx key //hget myhash age
  • 获取所有键值对 hgetall xxx //hgetall myhash

删除

  • hdel key 键2 //hdel myhash age

hlen xxx 获取xxx的长度(map个数)

hgetall xxx 获取xxx所有map

hexist xxx key 判断xxx中key是否存在

hkeys xxx 获取xxx中所有键key

hvals xxx 获取xxx中所有值val

incrby xxx key step 自增xxx中键key的值 ,步长为step,step可为负

hsetnx xxx key val 如xxx中key不存在则 可以设置值val

2.2.6 zset

有序集合类型 不允许重复元素,且元素有顺序

场景

  1. 消息排序
  2. 带权重的 消息存储
  3. 排行榜

存储:

  • zadd xxx score value score为数据对应的分数 value为数据 会根据分数对数据进行排序

获取:

  • zrange xxx start end// start与end为范围的索引
  • zrange xxx start end withscores// 获取带分数的set

删除:

  • zrem xxx value

zrangebyscore xxx -inf +inf 排序xxx 从负无穷到正无穷,可以自己写范围

zrangebyscore xxx -inf +inf withscores 排序xxx 带score的排序,可以自己写范围

zrerangebyscore xxx +inf -inf 排序xxx 从正无穷到负无穷,可以自己写范围

zcard xxx 获取xxx的大小

zcount xxx 1 3 获取指定score区间的成员数量

2.3 操作3个特殊类型

2.3.1 Geospatial 地理位置

本质是zset 可以使用zset命令

场景

  1. 附近的人
  2. 计算距离

geoadd china:city 116.40 39.90 beijin 增加城市 中国的北京 经度116.40 纬度39.90

getpos china:city beijing 获取指定城市的经纬度

geodist beijing shanghai km 获取两个地点之间的距离 单位km/m/mi英里/ft 英尺

georadius china:city 110 30 500km 以给定的经纬度为中心,找出某一半径的位置

georadius china:city 110 30 500km withdist 以给定的经纬度为中心,找出某一半径的位置,并显示具体信息

georadius china:city 110 30 500km withdist count 2 以给定的经纬度为中心,找出某一半径的指定数量的位置,并显示具体信息

georadiusbymember china:city 110 30 500km withdist count 2 以给定的城市寻找城市

2.3.2 Hyperloglog

基数统计: 占用内存固定12k 0.81% 错误率(可接受)

场景

  1. 网页的访问量uv

pfadd xxx v1 v2 v3 v4 给xxx添加元素

pfcount xxx 统计总数

pfmerge xall x1 x2 将x1与x2 合并为xall

2.3.3 Bitmaps

简易版hash

场景

  1. 统计用户 活跃/不活跃 登录/未登录 … 对于只有2个状态的数据 可以使用Bitmaps,只有2个状态

setbit xxx 0 0 添加键0 值为0

getbit xxx 0 获取xxx的键0对应的值

bitcount xxx 获取xxx中map的数量

3. 事务

特点

  • redis单条命令保证原子性 但事务不保证原子性

  • redis事务不存在隔离级别的概念,所有的命令在事务中并没有直接被执行 只有发起执行命令的时候才会被执行

  • redis事务的本质:一组命令的集合!一个事务中所有命令都会被序列化,在事务执行过程中,会按照顺序执行

特性:

  1. 一次性
  2. 顺序性
  3. 排他性

3.1 开启事务

过程:

  1. 开启事务 multi
  2. 命令入队
  3. 执行事务 exec

放弃事务 discard 事务队列的所有任务都被放弃

事务的异常

  1. 编译型异常(代码有问题 命令语法有错) 事务中所有的命令都不会被执行

  2. 运行时异常(1/0) 单条错误的命令被放弃并抛出异常 但是其他命令是可以被正常执行的

在这里插入图片描述

3.2 事务监控

悲观锁: 什么时候都会出问题 无论做什么都加锁

乐观锁: 认为什么时候都不会出问题,所以不会上锁 更新的时候去判断一下 是否有人修改过这个数据,version

  1. 获取version
  2. 更新时比较version

redis监视测试 watch xxx

在这里插入图片描述

当修改值前,另一个线程把值改变了 会修改失败 乐观锁操作

解决:

  1. unwatch 放弃监视
  2. watch xxx 重新监视
  3. multi开启事务
  4. exec 执行事务
  5. 如果未变化 执行成功 变化 执行失败,重复1-5

4. 持久化

4.1 RDB

默认方式,不需要进行配置,默认就使用这种机制

在一定的间隔时间中,检测key的变化情况,然后持久化数据

配置方法

  1. 编辑redis.conf文件

    after 900 sec (15 min) if at least 1 key changed

     		save 900 1
    

    after 300 sec (5 min) if at least 10 keys changed

      		save 300 10
    

    after 60 sec if at least 10000 keys changed

     		save 60 10000
    
  2. 重新启动redis服务器

触发机制

  1. save的规则满足的情况下,会自动触发rdb规则
  2. 执行 flushall 命令,也会触发rdb规则
  3. 退出redis,也会产生 rdb 文件
  4. 备份就自动生成一个 dump.rdb

恢复机制

  1. 只需要将rdb文件放在redis启动目录就可以,redis启动的时候会自动检查dump.rdb 恢复
  2. 查看需要存在的位置 config get dir 在那个目录存在dump.rdb 启动就会自动恢复其中的数据

优点:

  1. 适合大规模的数据恢复!

  2. 对数据的完整性要不高!

缺点:

  1. 需要一定的时间间隔进程操作!如果redis意外宕机了,这个最后一次修改数据就没有的了
  2. fork进程的时候,会占用一定的内容空间

4.2 AOF

日志记录的方式,可以记录每一条写命令的操作。每一次写命令操作后,持久化数据(默认不开启)

开启 :

编辑redis.conf文件

			appendonly no(关闭aof) --> appendonly yes (开启aof)
			appendfilename "appendonly.aof" # 持久化的文件的名字
			# appendfsync always : 每一次操作都进行持久化
			appendfsync everysec : 每隔一秒进行一次持久化
			# appendfsync no	 : 不进行持久化

优点:

  1. 每一次修改都同步,文件的完整会更加好!
  2. 每秒同步一次,可能会丢失一秒的数据
  3. 从不同步,效率最高的!

缺点:

  1. 相对于数据文件来说,aof远远大于 rdb,修复的速度也比 rdb慢!
  2. aof 运行效率也要比 rdb 慢,所以我们redis默认的配置就是rdb持久化

5. Jedis连接Redis

	/**
	 * 连接测试
	 */
	@Test
    public void test1(){
        //1.获取连接
        Jedis jedis=new Jedis("localhost",6379);
        //2.操作
        jedis.set("username","zhangsan");
        //3.关闭连接
        jedis.close();
    }
	/**
	 *   Jedis操作redis中的数据结构string
	 */
	@Test
    public void test2(){
        Jedis jedis=new Jedis("localhost",6379);//如果使用空参,默认"localhost",6379
        jedis.set("username","lisi");
        String username = jedis.get("username");
        System.out.println(username);

        //使用过setex()方法存储可以指定过期时间的key value
        jedis.setex("activecode",20,"hehe");//将activecode:hehe存入redis并且20秒后自动删除

        jedis.close();
    }
/**
 *       Jedis操作redis中的数据结构hash
 */
@Test
public void test3(){
    Jedis jedis=new Jedis("localhost",6379);//如果使用空参,默认"localhost",6379
    jedis.hset("myhash","66","6");
    String myHash66 = jedis.hget("myhash", "66");
    System.out.println(myHash66);
    Map<String, String> myhashAll = jedis.hgetAll("myhash");
    System.out.println(myhashAll);
    jedis.hdel("myhash","66");
    myHash66 = jedis.hget("myhash", "66");
    System.out.println(myHash66);
    jedis.close();
}
/**
 * Jedis操作redis中的数据结构list
 */
@Test
public void test4(){
    Jedis jedis=new Jedis("localhost",6379);//如果使用空参,默认"localhost",6379
    jedis.lpush("list","1","2","3");
    jedis.rpush("list","1","2","3");
    List<String> list = jedis.lrange("list", 0, -1);
    for (String s : list) {
        System.out.println(s);
    }
    jedis.close();
}
/**
 * Jedis操作redis中的数据结构set
 */
@Test
public void test5(){
    Jedis jedis=new Jedis("localhost",6379);//如果使用空参,默认"localhost",6379
    jedis.sadd("set","2","4","6");
    Set<String> set = jedis.smembers("set");
    for (String s : set) {
        System.out.println(s);
    }
    jedis.close();
}
/**
 *  Jedis操作redis中的数据结构sortedset
 */
@Test
public void test6(){
    Jedis jedis=new Jedis("localhost",6379);//如果使用空参,默认"localhost",6379
    jedis.zadd("sset",10,"4");
    jedis.zadd("sset",50,"7");
    jedis.zadd("sset",30,"99");
    Set<Tuple> sset = jedis.zrangeWithScores("sset", 0, -1);
    for (Tuple tuple : sset) {
        System.out.println(tuple);
    }
    jedis.close();
}
/**
 * Jedis连接池
 */
@Test
public void test7(){
    //创建配置对象
    JedisPoolConfig jedisPoolConfig=new JedisPoolConfig();
    jedisPoolConfig.setMaxTotal(50);
    jedisPoolConfig.setMaxIdle(10);
   //创建jedis连接池对象
    JedisPool jedisPool=new JedisPool(jedisPoolConfig,"localhost",6379);
    //获取连接
    Jedis jedis=jedisPool.getResource();
    jedis.set("uu","ee");
    String uu = jedis.get("uu");
    System.out.println(uu);
    jedis.close();//归还到连接池中
}
/**
 * 测试JedisPool工具类
 */
@Test
public void test8(){
    Jedis jedis = JedisPoolUtils.getJedis();
    jedis.set("name","mike");
    String name = jedis.get("name");
    System.out.println(name);
    jedis.close();
}
/**
 * redis事务
 */
@Test
public void test9(){
    //创建配置对象
    JedisPoolConfig jedisPoolConfig=new JedisPoolConfig();
    jedisPoolConfig.setMaxTotal(50);
    jedisPoolConfig.setMaxIdle(10);
    //创建jedis连接池对象
    JedisPool jedisPool=new JedisPool(jedisPoolConfig,"47.106.248.41",6379,6000,"lixiang1997");
    Jedis jedis = jedisPool.getResource();
    Transaction multi = jedis.multi();
    try {
        multi.set("money","222");
        //int i=1/0;
        multi.incr("money");
        multi.exec();
    }catch (Exception e){
        multi.discard();
    }finally {
        jedis.close();
    }
}

6. spring-redis

6.1 配置

spring:
  redis:
    host: 47.106.238.55
    password: *****
    database: 0
    timeout: 6000

6.2 两个模板

	@Bean
	@ConditionalOnMissingBean(name = "redisTemplate")
	public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
			throws UnknownHostException {
        //默认的template配置简单,redis对象需要序列化,泛型为Obkect,需要强转,一般使用下面的
		RedisTemplate<Object, Object> template = new RedisTemplate<>();
		template.setConnectionFactory(redisConnectionFactory);
		return template;
	}

	@Bean
	@ConditionalOnMissingBean
	public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory)
			throws UnknownHostException {
		StringRedisTemplate template = new StringRedisTemplate();
		template.setConnectionFactory(redisConnectionFactory);
		return template;
	}

6.3 自定义RedisTemplate

    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
        template.setConnectionFactory(factory);
        // Json序列化配置
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,ObjectMapper.DefaultTyping.NON_FINAL );
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // String 的序列化
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value序列化方式采用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // hash的value序列化方式采用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }

7. 发布 订阅

subscribe xxx 订阅xxx频道

publish xxx "message" 向xxx频道推送消息

8. Redis主从复制

主从复制,读写分离! 80% 的情况下都是在进行读操作!减缓服务器的压力! 一主二从!

主机可以写,从机不能写只能读!主机中的所有信息和数据,都会自动被从机保存!

info replication 查看当前库的信息

复制3个配置文件,然后修改对应的信息

  1. 端口
  2. pid 名字
  3. log文件名字
  4. dump.rdb 名字

SLAVEOF IP 端口 设置该从机的主节点 (redis默认开机为主节点 ,需要配置了主节点后 该节点就为从节点)

复制原理

  1. Slave 启动成功连接到 master 后会发送一个sync同步命令

  2. Master 接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行

  3. 完毕之后,master将传送整个数据文件到slave,并完成一次完全同步。

    全量复制:而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中。

    增量复制:Master 继续将新的所有收集到的修改命令依次传给slave,完成同步

    但是只要是重新连接master,一次完全同步(全量复制)将被自动执行!

更改从节点为主节点: SLAVEOF no one手动将其他节点连接到此节点

8.1 哨兵模式

主服务宕机后要手动操作服务器连接 而哨兵模式解决了这个问题

哨兵有两个作用

  1. 通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器。

  2. 当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服
    务器,修改配置文件,让它们切换主机。

  3. 主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认
    为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一
    定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover[故障转移]操作。
    切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为
    客观下线

  4. 主机此时回来了,只能归并到新的主机下,当做从机

优点:

  1. 哨兵集群,基于主从复制模式,所有的主从配置优点,它全有
  2. 主从可以切换,故障可以转移,系统的可用性就会更好
  3. 哨兵模式就是主从模式的升级,手动到自动,更加健壮!

缺点:

  1. Redis 不好在线扩容,集群容量一旦到达上限,在线扩容就十分麻烦!
  2. 实现哨兵模式的配置其实是很麻烦的,里面有很多选择!

9. 缓存穿透与雪崩

9.1 缓存穿透

用户想要查询一个数据,发现redis内存数据库没有,也就是缓存没有命中,于是向持久层数据库查询。发现也没有,于是本次查询失败。当用户很多的时候,缓存都没有命中(秒杀!),于是都去请求了持久层数据库。这会给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透

解决方案

  1. 布隆过滤器 对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则
    丢弃,从而避免了对底层存储系统的查询压力
  2. 缓存空对象 当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源

存在问题:

  1. 如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多
    的空值的键;
  2. 即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于
    需要保持一致性的业务会有影响。

9.2 缓存击穿

缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
当某个key在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访
问数据库来查询最新数据,并且回写缓存,会导使数据库瞬间压力过大

解决方案

  1. 设置热点数据永不过期 从缓存层面来看,没有设置过期时间,所以不会出现热点 key 过期后产生的问题。
  2. 加互斥锁 使用分布式锁,保证对于每个key同时只有一个线程去查询后端服务,其他线程没有获得分布
    式锁的权限,因此只需要等待即可。这种方式将高并发的压力转移到了分布式锁,因此对分布式锁的考
    验很大

9.3 缓存雪崩

在某一个时间段,缓存集中过期失效。Redis 宕机!
产生雪崩的原因之一,比如马上就要到双十二零点,很快就会迎来一波抢购,这波商品时间比较集中的放入了缓存,假设缓存一个小时。那么到了凌晨一点钟的时候,这批商品的缓存就都过期了。而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。

解决方案

  1. redis高可用 既然redis有可能挂掉,那我多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。(异地多活!)
  2. 限流降级 在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
  3. 数据预热 数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
发布了22 篇原创文章 · 获赞 0 · 访问量 570

猜你喜欢

转载自blog.csdn.net/lixiang19971019/article/details/105734700