Redis 笔记

Redis

参考视频地址

docker版安装

docker run -it --name redis01 -p 6379:6379 -v /home/redis/data/:/data -v /home/redis/conf/redis.conf:/etc/redis/redis.conf redis redis-server /etc/redis/redis.conf

#/etc/redis/redis.conf
#进入正在运行的redis
[root@Tintin ~]# docker exec -it 51 redis-server
root@51bf51ffb9ac:/usr/local/bin# redis-cli
  • 在本地添加配置文件

配置文件获取地址:https://redis.io/docs/management/config/

五大数据类型

String(字符串)

127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> get k1
"v1"
127.0.0.1:6379> keys *
1) "k1"
127.0.0.1:6379> exists k1
(integer) 1
127.0.0.1:6379> append k1 v2							
(integer) 4
127.0.0.1:6379> append newkey newValue      #追加key若不存在,则新增
(integer) 8
127.0.0.1:6379> keys *
1) "newkey"
2) "k1"
127.0.0.1:6379> get k1
"v1v2"
127.0.0.1:6379> strlen k1
(integer) 4
127.0.0.1:6379> append k1 "--Tintin"
(integer) 12
127.0.0.1:6379> strlen k1										#查看key对应value长度
(integer) 12
127.0.0.1:6379> get k1
"v1v2--Tintin"
##########################################################################
127.0.0.1:6379> set views 0
OK
127.0.0.1:6379> get views
"0"
127.0.0.1:6379> incr views
(integer) 1
127.0.0.1:6379> incr views
(integer) 2
127.0.0.1:6379> get views
"2"
127.0.0.1:6379> decr views
(integer) 1
127.0.0.1:6379> decr views
(integer) 0
127.0.0.1:6379> decr views
(integer) -1
127.0.0.1:6379> get views
"-1"
127.0.0.1:6379> INCRBY views 10             # 增加指定步长
(integer) 9
127.0.0.1:6379> incr views
(integer) 10
127.0.0.1:6379> DECRBY views 10
(integer) 0
##########################################################################
127.0.0.1:6379> set key1 tintin
OK
127.0.0.1:6379> get key1
"tintin"
127.0.0.1:6379> GETRANGE key1 0 3      # 截取
"tint"
127.0.0.1:6379> GETRANGE key1 0 -1
"tintin"
127.0.0.1:6379> SETRANGE key1 0 wu     # setrange 指定位置开始替换
(integer) 6
127.0.0.1:6379> get key1
"wuntin"
##########################################################################
[setex] (set with expire) # 设置过期时间
[setnx]	(set if not exist)

127.0.0.1:6379> setex key1 30 "hello"    # 设置这个key过期时间为30秒
OK
127.0.0.1:6379> get key1
"hello"
127.0.0.1:6379> ttl key1                 # 查看剩余过期时间
(integer) 17
127.0.0.1:6379> setnx mykey "redis"
(integer) 1
127.0.0.1:6379> keys *
1) "mykey"
127.0.0.1:6379> ttl key1                 #(TTL, time to live) 
(integer) -2
127.0.0.1:6379> setnx mykey "mongodb"
(integer) 0
127.0.0.1:6379> get mykey
"redis"
##########################################################################
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3   # 同时设置多个键值
OK
127.0.0.1:6379> keys *						
1) "k3"
2) "k2"
3) "k1"
127.0.0.1:6379> mget k1 k2 k3						# 同时获取多个值
1) "v1"
2) "v2"
3) "v3"
127.0.0.1:6379> msetnx k1 v1 k4 v4      # msetnx具有原子性
(integer) 0
127.0.0.1:6379> get k4
(nil)

#存入对象
127.0.0.1:6379> mset user:1:name tintin user:1:age 2
OK
127.0.0.1:6379> keys *
1) "user:1:age"
2) "user:1:name"
127.0.0.1:6379> mget user:1:name user:1:age
1) "tintin"
2) "2"
# 这个作用是什么呢?比如微信阅读量
127.0.0.1:6379> set article:10000:views 0  # 设置文章编号为10000的对象的初始浏览量,为0

##########################################################################
127.0.0.1:6379> getset k1 k1
(nil)
127.0.0.1:6379> get k1
"k1"
127.0.0.1:6379> getset k1 v222
"k1"
127.0.0.1:6379> get k1
"v222"

List(列表)

# 可实现堆栈
[LPUSH]        #队头插入
[RPUSH]        #队尾插入
[LRANGE]       #按序号获取具体的值

127.0.0.1:6379> lpush list one
(integer) 1
127.0.0.1:6379> lpush list two three four
(integer) 4
127.0.0.1:6379> LRANGE list 1 2
1) "three"
2) "two"
127.0.0.1:6379> lrange list 0 -1
1) "four"
2) "three"
3) "two"
4) "one"
127.0.0.1:6379> rpush list zero
(integer) 5
127.0.0.1:6379> lrange list 0 -1
1) "four"
2) "three"
3) "two"
4) "one"
5) "zero"

##############################################
[lpop]
[rpop]

127.0.0.1:6379> lrange list 0 -1
1) "four"
2) "three"
3) "two"
4) "one"
5) "zero"
127.0.0.1:6379> lpop list 1
1) "four"
127.0.0.1:6379> rpop list 1
1) "zero"
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
3) "one"

##############################################
[lindex]

127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> lindex list 1
"two"

##############################################
[Llen]

127.0.0.1:6379> llen list
(integer) 3
127.0.0.1:6379> lpush llen two    #two所在位置下标
(integer) 1


##############################################
[lrem] #应用场景:取消关注

127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> lrem list 1 one    #lrem listname count element
(integer) 1
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"

##############################################
[ltrim]

127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> ltrim list 0 1
OK
127.0.0.1:6379> lrange list 0 -1  # 截取指定下标
1) "three"
2) "two"


##############################################
[rpoplpush]  #队尾取出一个元素 放入 另一个队列

127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
3) "zero"
127.0.0.1:6379> rpoplpush list newlist
"zero"
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
127.0.0.1:6379> lrange newlist 0 -1
1) "zero"

##############################################
[lset] #类似于更新
127.0.0.1:6379> exists list
(integer) 0
127.0.0.1:6379> lset list 0 tintin  # 不存在无法更新
(error) ERR no such key
127.0.0.1:6379> lset list 2 tintin
(error) ERR index out of range
127.0.0.1:6379> lpush list tintin
(integer) 1
127.0.0.1:6379> lset list 0 wu
OK
127.0.0.1:6379> lrange list 0 -1
1) "wu"

##############################################
[linsert] #在某个元素前后插入

127.0.0.1:6379> lrange list 0 -1
1) "tintin"
2) "wu"
127.0.0.1:6379> linsert list after wu kids
(integer) 3
127.0.0.1:6379> lrange list 0 -1
1) "tintin"
2) "wu"
3) "kids"

他实际上是一个链表,before Node after,left,right都可以插入值

如果key不存在,创建新的链表

如果key存在,新增内容

如果移除了所看值,空链表,也代表不存在!

在两边插入或者改动值,效率最高!如果链表非常长,中间元素,相对来说效率会低一点

消息排队!

消息队列 (LPush Rpop ) 从左边进,右边拿出来
栈(Lpush Lpop)从左边进 左边出

Set(集合)

关注列表

[smembers]
[SisMember]

127.0.0.1:6379> sadd myset tintin
(integer) 1
127.0.0.1:6379> sadd myset wu
(integer) 1
127.0.0.1:6379> smembers myset
1) "wu"
2) "tintin"
127.0.0.1:6379> Sismember myset wu
(integer) 1

##############################################
[scard] 集合中元素个数

127.0.0.1:6379> scard myset
(integer) 2

##############################################
[srem] 移除一个元素

127.0.0.1:6379> srem myset 1
(integer) 1

##############################################
[srandmember] 随机抽取指定个数元素

127.0.0.1:6379> srandmember myset
"tintin"
127.0.0.1:6379> srandmember myset 2
1) "wu"
2) "tintin"

##############################################
[spop] 随机删除一个元素

127.0.0.1:6379> sadd numset 1 2 3 4
(integer) 4
127.0.0.1:6379> spop numset 1
1) "3"
127.0.0.1:6379> smembers numset
1) "1"
2) "2"
3) "4"

##############################################
[smove] 将元素移到新集合

127.0.0.1:6379> smembers numset
1) "1"
2) "2"
3) "4"
127.0.0.1:6379> smove numset newnum 2
(integer) 1
127.0.0.1:6379> smembers newnum
1) "2"
127.0.0.1:6379> smembers numset
1) "1"
2) "4"

##############################################
[sdiff]   # 两者不同
[sinter]  # 两者交集
[sunion]  # 两者并集

127.0.0.1:6379> smembers numset
1) "1"
2) "4"
127.0.0.1:6379> smembers numset2
1) "1"
2) "3"
127.0.0.1:6379> sdiff numset numset2
1) "4"
127.0.0.1:6379> sinter numset numset2
1) "1"
127.0.0.1:6379> sunion numset numset2
1) "1"
2) "3"
3) "4"

Hash(哈希)

key-map<k, v>

127.0.0.1:6379> hset user1 name zhang3 age 21
(integer) 2
127.0.0.1:6379> hget user1 name
"zhang3"
127.0.0.1:6379> hkeys user1
1) "name"
2) "age"
127.0.0.1:6379> hvals user1
1) "zhang3"
2) "21"
127.0.0.1:6379> hmget user1 name age
1) "zhang3"
2) "21"
127.0.0.1:6379> hgetall user1
1) "name"
2) "zhang3"
3) "age"
4) "21"
127.0.0.1:6379> hlen user1
(integer) 2
127.0.0.1:6379> hdel user1 age
(integer) 1
127.0.0.1:6379> hgetall user1
1) "name"
2) "zhang3"
127.0.0.1:6379> hexists user1 name
(integer) 1
127.0.0.1:6379> hexists user1 age
(integer) 0
##################################
[hincrby] #增加
[hsetnx]  # 不存在才可以设置

127.0.0.1:6379> hset user1 age 20
(integer) 1
127.0.0.1:6379> hincrby user1 age 10
(integer) 30
127.0.0.1:6379> hget user1 age
"30"
127.0.0.1:6379> hdecrby user1 age 10   #
(error) ERR unknown command `hdecrby`, with args beginning with: `user1`, `age`, `10`, 
127.0.0.1:6379> hsetnx user1 addr china 
(integer) 1
127.0.0.1:6379> hsetnx user1 addr china 
(integer) 0

hash变更数据,user name age ,尤其用于用户信息保存,经常变动的信息!
hash更适合对象的存储,String更加适合字符串存储

Zset(有序集合)

#########################################

127.0.0.1:6379> zadd zset 1 one
(integer) 1
127.0.0.1:6379> zadd zset 2 two
(integer) 1
127.0.0.1:6379> zrange zset 0 -1
1) "one"
2) "two"

#########################################

127.0.0.1:6379> zrange salary 0 -1
1) "zhangsan"
2) "caitian"
3) "wuorurou"
4) "lis"
5) "wang5"
127.0.0.1:6379> zrangebyscore salary -inf +inf withscores  # 升序排序
 1) "zhangsan"
 2) "20000"
 3) "caitian"
 4) "28000"
 5) "wuorurou"
 6) "37000"
 7) "lis"
 8) "60000"
 9) "wang5"
10) "70000"

127.0.0.1:6379> zrevrange salary 0 -1              # 降序排序
1) "wang5"
2) "lis"
3) "wuorurou"
4) "caitian"
5) "zhao6"
6) "zhangsan"


127.0.0.1:6379> zrangebyscore salary 0 30000 withscores  # 按照区间查找
1) "zhangsan"
2) "20000"
3) "zhao6"
4) "23000"
5) "caitian"
6) "28000"

127.0.0.1:6379> zrem salary zhangsan                # 移除
(integer) 1

127.0.0.1:6379> zcard salary                       # 元素个数
(integer) 5

127.0.0.1:6379> zcount salary 20000 25000          #区间元素个数
(integer) 1

案例思路:set 排序存储班级成绩表,工资表排序!
普通消息,1,重要消息2,带权重进行判断!
bilibili热点排行榜应用实现(set集合排序 按分钟刷新)

三种特殊数据类型

geospatial 地理位置空间

# 添加数据
[geoadd] geoadd key 经度 纬度 地点名称

127.0.0.1:6379> geoadd china:city 113.280637 23.125178 guangzhou
(integer) 1
127.0.0.1:6379> geoadd china:city 113.122717 23.028762 foshan
(integer) 1
127.0.0.1:6379> geoadd china:city 110.919222 21.659751 maoming
(integer) 1


# 查询地点经纬度
[geopos] geopos key 地点

127.0.0.1:6379> geopos china:city maoming
1) 1) "110.91922134160995483"
   2) "21.65975121256696667"
   
127.0.0.1:6379> geopos china:city guangzhou maoming
1) 1) "113.28063815832138062"
   2) "23.12517743834835215"
2) 1) "110.91922134160995483"
   2) "21.65975121256696667"
   
# 查看两地之间的距离 

127.0.0.1:6379> geodist china:city guangzhou maoming     # 默认单位为m
"292464.4986"
127.0.0.1:6379> geodist china:city guangzhou maoming km  # 设置指定单位
"292.4645"

# 指定范围内的城市 (附近的人)
[georadius] georadius key 经度 纬度 距离 单位

127.0.0.1:6379> georadius china:city 110 21 1000 km          # 方圆1000km的城市
1) "maoming"
2) "foshan"
3) "guangzhou"

127.0.0.1:6379> georadius china:city 110 21 130 km withdist   # 方圆城市+距离
1) 1) "maoming"
   2) "120.2290"
127.0.0.1:6379> georadius china:city 110 21 130 km withcoord  # 方圆城市+经纬度信息
1) 1) "maoming"
   2) 1) "110.91922134160995483"
      2) "21.65975121256696667"
127.0.0.1:6379> georadius china:city 110 21 1000 km withdist count 1  # 方圆的城市+距离+显示一个
1) 1) "maoming"
   2) "120.2290"
   
   
# geoRadiusByMember 根据元素喝距离查询
127.0.0.1:6379> georadiusbymember china:city guangzhou 150 km
1) "foshan"
2) "guangzhou"


# geohash 将二维的经纬度转换为一维的字符串,如果两个字符串越接近,那么则距离越近!
127.0.0.1:6379> geohash china:city guangzhou foshan   
1) "ws0e9cb3yj0"
2) "ws07n0m2q20"
127.0.0.1:6379> geohash china:city guangzhou maoming
1) "ws0e9cb3yj0"
2) "w7yghp0cjn0"

# geo 底层是zset
127.0.0.1:6379> zrange china:city 0 -1
1) "maoming"
2) "foshan"
3) "guangzhou"
127.0.0.1:6379> zrem china:city foshan
(integer) 1
127.0.0.1:6379> zrange china:city 0 -1
1) "maoming"
2) "guangzhou"

Hyperloglog

HyperLogLog是一种估计集合基数的数据结构。作为一个概率数据结构,HyperLogLog以完美的准确性来高效的空间利用。

Redis HyperLogLog实现使用高达12 KB,标准误差为0.81%

# pfadd
127.0.0.1:6379> pfadd mykey a b c a b c d e r q 
(integer) 1
127.0.0.1:6379> pfcount mykey                 # 统计mykey元素的基数数量
(integer) 7
127.0.0.1:6379> pfadd mykey2 a j k l w
(integer) 1
127.0.0.1:6379> pfcount mykey2
(integer) 5
127.0.0.1:6379> pfmerge mergekey mykey mykey2  # 合并两组 mykey mykey2=>mykey3并集
OK
127.0.0.1:6379> pfcount mergekey
(integer) 11

Bitmap

位存储

实现打卡功能、用户活跃度、登录状态、

127.0.0.1:6379> setbit tintin 0 0     		# setbit key 星期 是否打卡
(integer) 0
127.0.0.1:6379> setbit tintin 1 1
(integer) 0
127.0.0.1:6379> setbit tintin 2 1
(integer) 0
127.0.0.1:6379> setbit tintin 3 1
(integer) 0
127.0.0.1:6379> setbit tintin 4 1
(integer) 0
127.0.0.1:6379> setbit tintin 5 1
(integer) 0
127.0.0.1:6379> setbit tintin 6 0      
(integer) 0
127.0.0.1:6379> bitcount tintin 0 6       # 统计打卡天数
(integer) 5
127.0.0.1:6379> bitcount tintin 0 -1      
(integer) 5

事物

  • multi (开启事务)
  • 命令入队
  • exec(执行)discard(放弃事物)

redis中单事物不具有原子性,单条命令具有原子性

事务执行

127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2 
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) OK

放弃事务

127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> discard
OK

事务编译时出现错误,事务不会执行

127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> setk2 v2
(error) ERR unknown command `setk2`, with args beginning with: `v2`, 
127.0.0.1:6379(TX)> exec
(error) EXECABORT Transaction discarded because of previous errors.

运行时出现错误,正确命令会正常执行

127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> incr k1             # 错误
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) (error) ERR value is not an integer or out of range
3) OK
  • 这里也就是说的,redis单条保证原子性,但是事务不保证原子性

redis监视

  • 悲观锁:

​ 很悲观,认为什么时候都会出问题,无论做什么都会加锁!

  • 乐观锁:

​ 很乐观,认为什么时候都不会出问题,所以不会上锁!
​ 更新的时候去判断一下,在此期间是否有人修改过这个数据,mysql中的version字段!
​ 获取version
​ 更新的时候比较version
​ 我们基本大部分使用乐观锁为主,很少使用悲观锁,因为效率太低了。

  • 正常执行
127.0.0.1:6379> set balance 10 
OK
127.0.0.1:6379> watch balance          #开启监视
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> decrby balance 10
QUEUED
127.0.0.1:6379(TX)> incrby output 10
QUEUED
127.0.0.1:6379(TX)> exec
1) (integer) 0
2) (integer) 10
  • 修改失败
127.0.0.1:6379> set balance 10
OK
127.0.0.1:6379> watch balance
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> decrby balace 10
QUEUED
127.0.0.1:6379(TX)> incrby output 10          #此时balance已经发生修改
QUEUED
127.0.0.1:6379(TX)> exec	
(nil)																					#事务执行失败

--------失败后watch未解除,version已经更新------------
node1:6379> unwatch   # 先解锁
OK

Jedis

整合SpringBoot

Springboot 2.x后底层为 lettuce

Springboot 1.x底层为 jedis

通过自动装配文件可发现

@EnableConfigurationProperties(RedisProperties.class)  //每个自动装配都会绑定一个properties

// RedisTemplate 如果需要定制重写该文件即可
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
  throws UnknownHostException {
  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;
}
  • 整合过程

写入properties

# 应用名称
spring.application.name=redis


spring.redis.host=8.134.166.xxx

引入redisTemplate

@SpringBootTest
class RedisApplicationTests {

    @Autowired
    RedisTemplate redisTemplate;

    @Test
    void contextLoads() {
        ValueOperations valueOperations = redisTemplate.opsForValue();
        valueOperations.set("key", "value");
        System.out.println(valueOperations.get("key"));
    }

}

自定义redisTemplate

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
        template.setConnectionFactory(factory);
        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);
        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;
    }
}

自定义RedisUtil工具类

package com.tintin.redis.utils;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

@Component
public final class RedisUtil {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // =============================common============================
    /**
     * 指定缓存失效时间
     * @param key  键
     * @param time 时间(秒)
     */
    public boolean expire(String key, long time) {
        try {
            if (time > 0) {
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 根据key 获取过期时间
     * @param key 键 不能为null
     * @return 时间(秒) 返回0代表为永久有效
     */
    public long getExpire(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }


    /**
     * 判断key是否存在
     * @param key 键
     * @return true 存在 false不存在
     */
    public boolean hasKey(String key) {
        try {
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 删除缓存
     * @param key 可以传一个值 或多个
     */
    @SuppressWarnings("unchecked")
    public void del(String... key) {
        if (key != null && key.length > 0) {
            if (key.length == 1) {
                redisTemplate.delete(key[0]);
            } else {
                redisTemplate.delete(CollectionUtils.arrayToList(key));
            }
        }
    }


    // ============================String=============================

    /**
     * 普通缓存获取
     * @param key 键
     * @return 值
     */
    public Object get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }

    /**
     * 普通缓存放入
     * @param key   键
     * @param value 值
     * @return true成功 false失败
     */

    public boolean set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 普通缓存放入并设置时间
     * @param key   键
     * @param value 值
     * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */

    public boolean set(String key, Object value, long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 递增
     * @param key   键
     * @param delta 要增加几(大于0)
     */
    public long incr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递增因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, delta);
    }


    /**
     * 递减
     * @param key   键
     * @param delta 要减少几(小于0)
     */
    public long decr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递减因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, -delta);
    }


    // ================================Map=================================

    /**
     * HashGet
     * @param key  键 不能为null
     * @param item 项 不能为null
     */
    public Object hget(String key, String item) {
        return redisTemplate.opsForHash().get(key, item);
    }

    /**
     * 获取hashKey对应的所有键值
     * @param key 键
     * @return 对应的多个键值
     */
    public Map<Object, Object> hmget(String key) {
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * HashSet
     * @param key 键
     * @param map 对应多个键值
     */
    public boolean hmset(String key, Map<String, Object> map) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * HashSet 并设置时间
     * @param key  键
     * @param map  对应多个键值
     * @param time 时间(秒)
     * @return true成功 false失败
     */
    public boolean hmset(String key, Map<String, Object> map, long time) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 向一张hash表中放入数据,如果不存在将创建
     *
     * @param key   键
     * @param item  项
     * @param value 值
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 向一张hash表中放入数据,如果不存在将创建
     *
     * @param key   键
     * @param item  项
     * @param value 值
     * @param time  时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value, long time) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 删除hash表中的值
     *
     * @param key  键 不能为null
     * @param item 项 可以使多个 不能为null
     */
    public void hdel(String key, Object... item) {
        redisTemplate.opsForHash().delete(key, item);
    }


    /**
     * 判断hash表中是否有该项的值
     *
     * @param key  键 不能为null
     * @param item 项 不能为null
     * @return true 存在 false不存在
     */
    public boolean hHasKey(String key, String item) {
        return redisTemplate.opsForHash().hasKey(key, item);
    }


    /**
     * hash递增 如果不存在,就会创建一个 并把新增后的值返回
     *
     * @param key  键
     * @param item 项
     * @param by   要增加几(大于0)
     */
    public double hincr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, by);
    }


    /**
     * hash递减
     *
     * @param key  键
     * @param item 项
     * @param by   要减少记(小于0)
     */
    public double hdecr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, -by);
    }


    // ============================set=============================

    /**
     * 根据key获取Set中的所有值
     * @param key 键
     */
    public Set<Object> sGet(String key) {
        try {
            return redisTemplate.opsForSet().members(key);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }


    /**
     * 根据value从一个set中查询,是否存在
     *
     * @param key   键
     * @param value 值
     * @return true 存在 false不存在
     */
    public boolean sHasKey(String key, Object value) {
        try {
            return redisTemplate.opsForSet().isMember(key, value);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 将数据放入set缓存
     *
     * @param key    键
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSet(String key, Object... values) {
        try {
            return redisTemplate.opsForSet().add(key, values);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


    /**
     * 将set数据放入缓存
     *
     * @param key    键
     * @param time   时间(秒)
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSetAndTime(String key, long time, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().add(key, values);
            if (time > 0)
                expire(key, time);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


    /**
     * 获取set缓存的长度
     *
     * @param key 键
     */
    public long sGetSetSize(String key) {
        try {
            return redisTemplate.opsForSet().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


    /**
     * 移除值为value的
     *
     * @param key    键
     * @param values 值 可以是多个
     * @return 移除的个数
     */

    public long setRemove(String key, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().remove(key, values);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    // ===============================list=================================

    /**
     * 获取list缓存的内容
     *
     * @param key   键
     * @param start 开始
     * @param end   结束 0 到 -1代表所有值
     */
    public List<Object> lGet(String key, long start, long end) {
        try {
            return redisTemplate.opsForList().range(key, start, end);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }


    /**
     * 获取list缓存的长度
     *
     * @param key 键
     */
    public long lGetListSize(String key) {
        try {
            return redisTemplate.opsForList().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


    /**
     * 通过索引 获取list中的值
     *
     * @param key   键
     * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
     */
    public Object lGetIndex(String key, long index) {
        try {
            return redisTemplate.opsForList().index(key, index);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }


    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     */
    public boolean lSet(String key, Object value) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 将list放入缓存
     * @param key   键
     * @param value 值
     * @param time  时间(秒)
     */
    public boolean lSet(String key, Object value, long time) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            if (time > 0)
                expire(key, time);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

    }


    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @return
     */
    public boolean lSet(String key, List<Object> value) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

    }


    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒)
     * @return
     */
    public boolean lSet(String key, List<Object> value, long time) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            if (time > 0)
                expire(key, time);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 根据索引修改list中的某条数据
     *
     * @param key   键
     * @param index 索引
     * @param value 值
     * @return
     */

    public boolean lUpdateIndex(String key, long index, Object value) {
        try {
            redisTemplate.opsForList().set(key, index, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 移除N个值为value
     *
     * @param key   键
     * @param count 移除多少个
     * @param value 值
     * @return 移除的个数
     */

    public long lRemove(String key, long count, Object value) {
        try {
            Long remove = redisTemplate.opsForList().remove(key, count, value);
            return remove;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }

    }

}

Redis.conf详解

单位不区分大小写

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fMkdeF8m-1669512994879)(%E4%B8%AA%E4%BA%BA%E6%96%87%E6%A1%A3.assets/image-20221126133028269.png)]

类似于Spring,可以引入多个配置文件

在这里插入图片描述

网络

#bind 127.0.0.1            # 绑定ip
#protected-mode no 				 # 保护模式,默认开启
#port 6379								 # 端口号

General

daemonize no   #以守护进程方式启动,默认no

# 日志
# Specify the server verbosity level.
# This can be one of:
# debug (a lot of information, useful for development/testing)
# verbose (many rarely useful info, but not a mess like the debug level)
# notice (moderately verbose, what you want in production probably)  # 生产环境用
# warning (only very important / critical messages are logged)

loglevel notice  # 日志级别

logfile ""  # 日志路径和文件名
databases 16
always-show-logo no

SNAPSHOTTING 快照

# 持久化
# Unless specified otherwise, by default Redis will save the DB:
#   * After 3600 seconds (an hour) if at least 1 key changed
#   * After 300 seconds (5 minutes) if at least 100 keys changed
#   * After 60 seconds if at least 10000 keys changed
#
# You can set these explicitly by uncommenting the three following lines.
#
# save 3600 1            
# save 300 100
# save 60 10000
stop-writes-on-bgsave-error yes # 持久化失败后是否继续工作,默认为yes
rdbcompression yes  # 保存reb文件时,进行错误校验,若有错误则修复
dir ./   #rdb文件保存目录,默认为当前目录

REPLICATION 主从复制

SECURITY

---配置文件设置密码
# requirepass foobared      # 设置密码,默认没有密码 requirepass 123

---控制台设置密码
node1:6379> config set requirepass "123456" # 设置密码123456
OK
node1:6379> config get requirepass  # 发现所有命令没权限了
(error) NOAUTH Authentication required.
node1:6379> ping
(error) NOAUTH Authentication required.
node1:6379> auth 123456  # 使用密码进行登陆
OK
node1:6379> config get requirepass  # 获取redis密码
1) "requirepass"
2) "123456"

CLIENTS

# maxclients 10000 # 最大链接数

MEMORY MANAGEMENT

# maxmemory <bytes>            # 最大内存
# maxmemory-policy noeviction  # 达到最大内存后的策略
1、volatile-lru:只对设置了过期时间的key进行LRU(默认值) 
2、allkeys-lru : 删除lru算法的key   
3、volatile-random:随机删除即将过期key   
4、allkeys-random:随机删除   
5、volatile-ttl : 删除即将过期的   	
6、noeviction : 永不过期,返回错误

APPEND ONLY MODE

appendonly no  #默认是不开启aof模式的,默认是使用rdb方式持久化的,在大部分所有的情况下,rdb完全够用!
appendfilename "appendonly.aof"  # 持久化的文件的名字

# appendfsync always  # 每次修改都会 sync 消耗性能
appendfsync everysec  # 每秒执行一次 sync,可能会丢失这一秒的数据
# appendfsync no      # 不执行 sync 操作系统自己同步数据,速度最快,但一般也不用。

Redis持久化

RDB(Redis DataBase)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wZnlagDs-1669512994880)(%E4%B8%AA%E4%BA%BA%E6%96%87%E6%A1%A3.assets/802eccb6c17bb9fd51c013581a29a819.png)]

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

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

我们默认的就是RDB,一般情况下不需要修改这个配置。

有时候在生产环境我们会将dump.rdb进行备份

rdb保存的文件是dump.rdb 都是在我们的配置文件中快照中配置的
在这里插入图片描述
在这里插入图片描述

触发j机制

1、save的规则满足的情况下,会自动触发rdb规则

2、执行flushall命令,也会触发我们的rdb规则!

3、退出redis,也会产生rdb文件!

备份就自动生成一个dump.rdb文件

如何恢复rdb文件

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

127.0.0.1:6379> config get dir
1) "dir"
2) "/etc"

优缺点

  • 优点:

1.体积更小:相同的数据量rdb数据比aof的小,因为rdb是紧凑型文件

2.恢复更快:因为rdb是数据的快照,基本上就是数据的复制,不用重新读取再写入内存

3.性能更高:父进程在保存rdb时候只需要fork一个子进程,无需父进程的进行其他io操作,也保证了服务器的性能

  • 缺点:

1.需要一定的时间间隔进程操作!(当然你也可以自己去修改配置)。如果redis意外宕机了,这个最后一次修改数据就没有的了

2.fork进程的时候,会占用一定的内存空间

AOF(Append Only File)

将我们的所有命令都记录下来,history,恢复的时候就把这个文件全部在执行一遍!

在这里插入图片描述

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

Aof保存的是appendonly.aof文件

  • 开启Aof模式
appendonly no # 默认是不开启aof模式的,默认是使用rdb方式持久化的,在大部分所有的情况下,rdb完全够用!

#一般的话,我们只要每秒保存即可
# appendfsync always
appendfsync everysec
# appendfsync no
# If you have latency problems turn this to "yes". Otherwise leave it as
# "no" that is the safest pick from the point of view of durability.
# 如果有延迟问题,请将此选项转换为“yes”。否则就让它保持原样 “no”从耐久性的角度来看,这是最安全的选择。
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100 # 重写的基准是100 增幅100%就重新覆盖一次
auto-aof-rewrite-min-size 64mb  # 最小值是64MB 

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IYRbRFtC-1669512994881)(%E4%B8%AA%E4%BA%BA%E6%96%87%E6%A1%A3.assets/image-20221126154816463.png)]

默认是不开启的,我们要把他改成yes来开启。我们只需要将appendoly 改为yes就开启了aof!

重启服务,shutdown 再登录就可以生效了。

然后我们set k1 v1 ,并且get k1 v1以下,输入cat appendonly.aof看看

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qGDMY1z3-1669512994881)(%E4%B8%AA%E4%BA%BA%E6%96%87%E6%A1%A3.assets/a70be9bc7f62460915cfd515da427e6b.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YUTNtZPy-1669512994882)(%E4%B8%AA%E4%BA%BA%E6%96%87%E6%A1%A3.assets/63be029240f73ae54a62fb1876dcac14.png)]

redis-check-aof:aof文件修复工具,我们来测试下

如果这个aof文件有错误,这时候redis是启动不起来的,我们需要修复这个aof文件

redis 给我们提供了一个工具 redis-check-aof --fix

步骤:

1.shutdown exit,退出redis
2.删除datas/dump.rdb文件,因为里面有保存数据
3.vim appendonly.aof,肆意修改,并保存
4.尝试启动redis,登录cli的时候报错:
Could not connect to Redis at node1:6379: Connection refused
5.执行修复工具:redis-check-aof ../datas/appendonly.aof
6.需要你确认,输入y,修复成功。Successfully truncated AOF
7.再次执行,如果aof文件正常,就会发现数据都恢复了

这里注意,你破坏aof文件,在aof文件底部添加数据是可以修复的,但是如果set k1 v1这种的,你结构破坏了,aof也无法修复,修复了直接是空的数据库。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RxNnYfZS-1669512994882)(%E4%B8%AA%E4%BA%BA%E6%96%87%E6%A1%A3.assets/600e9ab59e1f02771b7e20bb8b805107.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SqCaQQw6-1669512994882)(%E4%B8%AA%E4%BA%BA%E6%96%87%E6%A1%A3.assets/4ad657d4dd86d032019043cbd7596caf.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aGeXD2IR-1669512994882)(%E4%B8%AA%E4%BA%BA%E6%96%87%E6%A1%A3.assets/3a281643d8ca0bc53e47c45c675b89b0.png)]

重要规则说明

aof默认就是文件的无限追加,文件会越来越大!
在这里插入图片描述

如果这个aof文件大于64M,太大了。fork一个新的进程来将文件进行重写

优缺点

appendonly no  #默认是不开启aof模式的,默认是使用rdb方式持久化的,在大部分所有的情况下,rdb完全够用!
appendfilename "appendonly.aof"  # 持久化的文件的名字

# appendfsync always  # 每次修改都会 sync 消耗性能
appendfsync everysec  # 每秒执行一次 sync,可能会丢失这一秒的数据
# appendfsync no      # 不执行 sync 操作系统自己同步数据,速度最快,但一般也不用。
  • 优点:

​ 1.每一次修复都同步,文件的完整会更好。

​ 2.每秒同步一次,可能会丢失一秒的数据。

​ 3.从不同步,效率最高的。

  • 缺点:

​ 1.相对于数据文件来说,aof远远大于rdb,修复的速度也比rdb慢!

​ 2.Aof运行效率也要比rdb慢,所以我们redis默认的配置就是rdb持久化!

扩展

1、RDB持久化方式能够在指定的时间间隔内对你的数据进行快照存储

2、AOF持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以Redis协议追加保存每次写的操作到文件未尾,Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大。

3、只做缓存,如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化

4、同时开启两种持久化方式

在这种情况下,当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。
RDB的数据不实时,同时使用两者时服务器重启也只会找AOF文件,那要不要只使用AOF呢?建议不要,因为RDB更适合用于备份数据库(AOF在不断变化不好备份),快速重启,而且不会有AOF可能潜在的Bug,留着作为一个万一的手段。
5.性能建议

因为RDB文件只用作后备用途,建议只在Slave上持久化RDB文件,而且只要15分钟备份一次就够了,只保留 save9001这条规则。

如果EnableAOF,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只load自己的AOF文件就可以了,代价一是带来了持续的IO,二是AOF rewrite的最后将rewrite过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的。只要硬盘许可,应该尽量减少AOF rewrite的频率,AOF重写的基础大小默认值64M太小了,可以设到5G以上,默认超过原大小100%大小重写可以改到适当的数值。

如果不Enable AOF,仅靠Master-Slave Repllcation 实现高可用性也可以,能省掉一大笔IO,也减少了rewrite时带来的系统波动。代价是如果Master/Slave同时倒掉(断电),会丢失十几分钟的数据,启动脚本也要比较两个Master/Slave中的RDB文件,载入较新的那个,微博就是这种架构。

Redis发布订阅

Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。微信、微博、关注系统!
Redis客户端可以订阅任意数量的频道。
订阅/发布消息图:
在这里插入图片描述

下图展示了频道 channel1,以及订阅这个频道的三个客户端——client2、client5和client1之间的关系:

在这里插入图片描述

当有新消息通过PUBLISH命令发送给频道channel1时,这个消息就会被发送给订阅它的三个客户端:

在这里插入图片描述

命令

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G33x6tLO-1669512994883)(%E4%B8%AA%E4%BA%BA%E6%96%87%E6%A1%A3.assets/a7c2286b770303b6ed73d4369b8d70ae-20221126161548443.png)]

测试

---订阅端
127.0.0.1:6379> subscribe tintin
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "tintin"
3) (integer) 1
1) "message"
2) "tintin"
3) "hello"

---发布端
127.0.0.1:6379> publish tintin hello
(integer) 1

原理
Redis是使用C实现的,通过分析Redis源码里的pubsub.c文件,了解发布和订阅机制的底层实现,籍此加深对Redis的理解。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jE6KBIK4-1669512994883)(%E4%B8%AA%E4%BA%BA%E6%96%87%E6%A1%A3.assets/src=http%253A%252F%252Fupload-images.jianshu.io%252Fupload_images%252F10817794-5dbbaceda1313591.png&refer=http%253A%252F%252Fupload-images.jianshu.png)]

微信:

Redis通过PUBLISH、SUBSCRIBE和PSUBSCRIBE等命令实现发布和订阅功能。

通过SUBSCRIBE命令订阅某频道后,redis-server里维护了一个字典,字典的键就是一个个channel(频道),而字典的值则是一个链表,链表中保存了所有订阅这个channel的客户端。SUBSCRIBE命令的关键,就是将客户端添加到给定channel的订阅链表中。

通过PUB凵SH命令向订阅者发送消息,redis-server会使用给定的频道作为键,在它所维护的channel字典中查找记录了订阅这个频道的所有客户端的链表,遍历这个链表,将消息发布给所有订阅者。

Pub/Sub从字面上理解就是发布(Publish)与订阅(Subscribe),在Redis中你可以设定对某一个key值进行消息发布及消息订阅,当一个key值上进行了消息发布后,所有订阅它的客户端都会收到相应的消息。这一功能最明显的用法是用作实时消息系统,比如普通的即时聊天,群聊等功能。

使用场景:

1.实时消息系统

2.实时聊天(频道当做聊天室,将消息回显给所有人即可)

3.订阅、关注的系统都可以。

稍微复杂的场景我们就会使用 消息中间件 MQ

Redis主从复制

概念

主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master/leader),后者称为从节点(slave/follower);数据的复制是单向的,只能由主节点到从节点。Master以写为主,Slave以读为主。

默认情况下,每台Redis服务器都是主节点(因为你还没有配置主从);

且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。

主从复制的作用主要包括

1、数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。

2、故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。

3、负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。

4、高可用(集群)基石:除了上述作用用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。

一般来说,要将Redis运用于工程项目中,只使用一台Redis是万万不能的(可能宕机,1主2从),原因如下:

1、从结构上,单个Redis服务器会发生单点故障,并且一台服务器需要处理所有的请求负载,压力较大

2、从容量上,单个Redis服务器内存容量有限,就算一台Redis服务器内存容量为256G,也不能将所有内存用作Redis存储内存

一般来说,单台Redis最大使用内存不应该超过20G。

电商网站上的商品,一般都是一次上传,无数次浏览的,说专业点也就是"多读少写"。

对于这种场景,我们可以使如下这种架构:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BNIUlUWg-1669512994884)(%E4%B8%AA%E4%BA%BA%E6%96%87%E6%A1%A3.assets/a5ead5bce37addf66ddbe2107969fbe0.png)]

主从复制,读写分离!80%的情况下都是在进行读操作!减缓服务器压力。架构中经常使用,最低1主2从。

只要在公司中,主从复制就是必须要用的,因为真实项目中不可能单机使用Redis!

环境搭建

环境配置(单机多集群)

只配置从库,不用配置主库。

info replication # 查看当前库的信息

# 必须填写bind地址
在主服务器的redis.conf配置文件上绑定的 bind ip地址  表示可以那些机器可以访问:
设置为 0.0.0.0 表示都可以访问;如果是127.0.0.1 表示localhost访问;如果不设置从就没有访问主的权限 
  • 从机(命令行设置的从机,重启后失效)
# 填写主机地址和端口号
127.0.0.1:6379> slaveof 8.134.166.133 6379
OK

# 查看配置情况
127.0.0.1:6379> info replication
# Replication
role:slave
master_host:8.134.166.133
master_port:6379
master_link_status:up
master_last_io_seconds_ago:9
master_sync_in_progress:0
slave_read_repl_offset:528
slave_repl_offset:528
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:5a99a13cc0e03fc0fd6c83b20502679ceb947212
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:528
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:528

# 主机中设置值
127.0.0.1:6379> set k1 v1
OK

# 从机中获取
127.0.0.1:6379> get k1
"v1"
  • 通过配置文件设置可以永久设置从机

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TluNuBd3-1669512994884)(%E4%B8%AA%E4%BA%BA%E6%96%87%E6%A1%A3.assets/image-20221126200540240.png)]

细节

主机可以写;从机不能写只能读。

主机中的所有信息和数据,都会自动被从机保存

(error) READONLY You can’t write against a read only slave.

翻译:你不能写,只能读,你仅仅只是一个从机

在没有哨兵的模式下,主机down,从机也只能读不能写,主机重新上线后依然为主机

**全量复制:**主机set k1 v1,从机断开,主机set k2 v2,从机重新连接主机,全量复制,从机还可以get k2

**增量复制:**主机连接主机后,主机set k3 v3,从机可以get k3,这就是增量复制,但是只要是重新连接master,一次完全同步(全量复制)将被自动执行。我们的数据一定可以在从机中看到。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dqrmENJy-1669512994884)(%E4%B8%AA%E4%BA%BA%E6%96%87%E6%A1%A3.assets/9b0340c78f7018a0ce205463c38fdf67.png)]

层层链路模式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G4rjL55q-1669512994884)(%E4%B8%AA%E4%BA%BA%E6%96%87%E6%A1%A3.assets/93f7e179ea463659e83029c8a4049196.png)]

在哨兵模式没有出来之前,我们都是手动设置的。

如果主机断开了连接,我们可以使用 SLAVEOF no one ,来让自己变成主机!其他节点就可以手动连接到这个最新的节点(手动)!

如果这个时候老大6379恢复了,也只是光杆司令了。除非手动让6380再次跟从6379.[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ndOGiBQP-1669512994885)(%E4%B8%AA%E4%BA%BA%E6%96%87%E6%A1%A3.assets/6e626c0492b2b59e10ab0185ea35a82d.png)]

哨兵模式

主从切换技术的方方法是“当主服务器宕机后需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。Redis从2.8开始正式提提供了sentinel(哨兵)架构解决这个问题。

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

哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。
在这里插入图片描述

哨兵模式文件:redis-sentinel

在这里插入图片描述

这里的哨兵有两个作用:

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

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

然而一个哨兵进程对Redis服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mqZ7YKHp-1669512994885)(%E4%B8%AA%E4%BA%BA%E6%96%87%E6%A1%A3.assets/31c2188b87c6ada06f4c737ee3a1ff25.png)]

假设主服务器宕机哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为主服务器

不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间

就会进行一次投票,投票的结果由一个哨兵发起,进行failover[故障转移]操作。切换成功后,就会通过发布订阅

模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。

测试

我们目前的状态是1主2从

1.配置哨兵配置文件

哨兵模式有非常多,我们只写一个最简单的。

vim sentinel.conf

sentinel  monitor   myredis        node1   6379       1
# 哨兵      监视    被监控名称:自定义   主机名  端口名port  1代表一个哨兵认为主机挂了就下线
[root@Tintin redis]# ./bin/redis-sentinel /usr/local/sentinel.conf 

31202:X 26 Nov 2022 21:23:29.254 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
31202:X 26 Nov 2022 21:23:29.254 # Redis version=6.2.6, bits=64, commit=00000000, modified=0, pid=31202, just started
31202:X 26 Nov 2022 21:23:29.254 # Configuration loaded
31202:X 26 Nov 2022 21:23:29.254 * monotonic clock: POSIX clock_gettime
                _._                                                  
           _.-``__ ''-._                                             
      _.-``    `.  `_.  ''-._           Redis 6.2.6 (00000000/0) 64 bit
  .-`` .-```.  ```\/    _.,_ ''-._                                  
 (    '      ,       .-`  | `,    )     Running in sentinel mode
 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 26379
 |    `-._   `._    /     _.-'    |     PID: 31202
  `-._    `-._  `-./  _.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |           https://redis.io       
  `-._    `-._`-.__.-'_.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |                                  
  `-._    `-._`-.__.-'_.-'    _.-'                                   
      `-._    `-.__.-'    _.-'                                       
          `-._        _.-'                                           
              `-.__.-'                                               

31202:X 26 Nov 2022 21:23:29.255 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
31202:X 26 Nov 2022 21:23:29.258 # Sentinel ID is 4f5271450ec2b94c1fd5860d19e69eab0d36e596
31202:X 26 Nov 2022 21:23:29.258 # +monitor master myredis 8.134.166.133 6379 quorum 1
31202:X 26 Nov 2022 21:23:29.259 * +slave slave 8.134.166.133:6379 8.134.166.133 6379 @ myredis 8.134.166.133 6379
31202:X 26 Nov 2022 21:23:39.383 * +convert-to-slave slave 8.134.166.133:6379 8.134.166.133 6379 @ myredis 8.134.166.133 6379

主机崩

1.在6379主机,set k8 v8,然后shutdown exit退出
2.在6380 和 6381从机 分别:info replication ,发现目前依旧是:slave模式,不要急 ,观察下哨兵日志
3.哨兵日志:

27661:X 26 Nov 18:42:03.730 # +sdown master myredis 192.168.88.161 6379
27661:X 26 Nov 18:42:03.731 # +odown master myredis 192.168.88.161 6379 #quorum 1/1
27661:X 26 Nov 18:42:03.731 # +new-epoch 1
27661:X 26 Nov 18:42:03.731 # +try-failover master myredis 192.168.88.161 6379
27661:X 26 Nov 18:42:03.732 # +vote-for-leader b289eec7e609a7cad25fcb5e9891931fc10afc15 1
27661:X 26 Nov 18:42:03.732 # +elected-leader master myredis 192.168.88.161 6379
27661:X 26 Nov 18:42:03.732 # +failover-state-select-slave master myredis 192.168.88.161 6379
27661:X 26 Nov 18:42:03.823 # +selected-slave slave 192.168.88.161:6380 192.168.88.161 6380 @ myredis 192.168.88.161 6379
27661:X 26 Nov 18:42:03.823 * +failover-state-send-slaveof-noone slave 192.168.88.161:6380 192.168.88.161 6380 @ myredis 192.168.88.161 6379
27661:X 26 Nov 18:42:03.924 * +failover-state-wait-promotion slave 192.168.88.161:6380 192.168.88.161 6380 @ myredis 192.168.88.161 6379
27661:X 26 Nov 18:42:04.427 # +promoted-slave slave 192.168.88.161:6380 192.168.88.161 6380 @ myredis 192.168.88.161 6379
27661:X 26 Nov 18:42:04.427 # +failover-state-reconf-slaves master myredis 192.168.88.161 6379
27661:X 26 Nov 18:42:04.482 * +slave-reconf-sent slave 192.168.88.161:6381 192.168.88.161 6381 @ myredis 192.168.88.161 6379
27661:X 26 Nov 18:42:05.443 * +slave-reconf-inprog slave 192.168.88.161:6381 192.168.88.161 6381 @ myredis 192.168.88.161 6379
27661:X 26 Nov 18:42:05.443 * +slave-reconf-done slave 192.168.88.161:6381 192.168.88.161 6381 @ myredis 192.168.88.161 6379
27661:X 26 Nov 18:42:05.496 # +failover-end master myredis 192.168.88.161 6379  # 故障转移6379
27661:X 26 Nov 18:42:05.496 # +switch-master myredis 192.168.88.161 6379 192.168.88.161 6380
27661:X 26 Nov 18:42:05.496 * +slave slave 192.168.88.161:6381 192.168.88.161 6381 @ myredis 192.168.88.161 6380
27661:X 26 Nov 18:42:05.496 * +slave slave 192.168.88.161:6379 192.168.88.161 6379 @ myredis 192.168.88.161 6380
27661:X 26 Nov 18:42:35.513 # +sdown slave 192.168.88.161:6379 192.168.88.161 6379 @ myredis 192.168.88.161 6380  # 发现6379已经sdown了,选举6380作为master

4.再次观察6380和6381,就会发现6380变成了master

如果Master节点断开了,这个时候就会从从机中随机选择一个服务器(这里面有一个投票算法)!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aJbGrfiI-1669512994885)(%E4%B8%AA%E4%BA%BA%E6%96%87%E6%A1%A3.assets/2364eba7ec736d592666af777a9fb14d.png)]

现在选出了新的主机,你就算把6379连回来,也没用了,因为已经选出了6380作为新的老大,6379是光杆司令了。

这里有个有趣的点:

  • 当你6379主机刚连接回来的时候,info replication,这时候6379是master主机
  • 观察哨兵日志,发现开始转换
27661:X 26 Nov 18:48:51.189 * +convert-to-slave slave 192.168.88.161:6379 192.168.88.161 6379 @ myredis 192.168.88.161 6380
  • 这时候才看6379,info replication,发现6379已经是slave了
  • 再看6380,info replication,发现6380的connected_slaves:从1变成了2,也就是说6379恢复连接后,被转换成了新的Master6380的从机了

哨兵模式

如果主机此时回来了,只能归并到新的主机下,当做从机,这就是哨兵模式的规则!

优点:

1、哨兵集群,基于主从复制模式,所有的主从配置优点,他全有。

2、主从可以切换,故障可以转换,系统的可用性就会更好

3、哨兵模式就是主从模式的升级,手动到自动,更加健壮!

缺点:

1、Redis不好在线扩容的,集群容量一旦到达上限,在线扩容就会非常麻烦!

2、实现哨兵模式的配置其实是很麻烦的,里面有很多选择!(我们这是这是开了一个最简单的哨兵模式)

哨兵模式的全部配置
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

缓存穿透

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eshsnh27-1669512994886)(%E4%B8%AA%E4%BA%BA%E6%96%87%E6%A1%A3.assets/616f4180a22d1378fea9cd4b873c5884.png)]

解决方案

  • 布隆过滤器

布隆过滤器是一种数据结构,对所有可能査询的参数以hash形式存储,在控制层先进行校验,不符合则丟弃,从而避免了对底层存储系统的查询压力;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dN0HPeDP-1669512994887)(%E4%B8%AA%E4%BA%BA%E6%96%87%E6%A1%A3.assets/179b5cb8d47851d48f432950356c3ee2.png)]

  • 缓存空对象

当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UGPjByHw-1669512994887)(%E4%B8%AA%E4%BA%BA%E6%96%87%E6%A1%A3.assets/17c9c5deee674088be5b53ae91480f38.png)]

但是这种方法存在2个问题:

1、如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键

2、即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。

缓存击穿(量太大,缓存过期!)

微博热搜,服务器宕机(60秒过期,60.1秒回复,这0.1秒的间隔 ==> 瞬间全部砸在mysql服务器上)

概述

这里需要注意和缓存击穿的区别,缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。

当某个key在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访问数据库来查询最新数据,并且回写缓存,会导使数据库瞬间压力过大。

解决方案

设置热点数据永不过期

从缓存层面来看,没有设置过期时间,所以不会出现热点key过期后产生的问题。

加互斥锁

分布式锁:使用分布式锁,保证对于每个key同时只有一个线程去査询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可。这种方式将高并发的压力转移到了分布式锁,因此对分布式锁的考验很大。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vYaldd0Y-1669512994887)(%E4%B8%AA%E4%BA%BA%E6%96%87%E6%A1%A3.assets/9abc8ae90c2f84fc94ad3d45db161fbf.png)]

缓存雪崩

概念

缓存雪崩,是指在某一个时间段,缓存集中过期失效。或者Redis宕机(停电了)!

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rBuBySsp-1669512994887)(%E4%B8%AA%E4%BA%BA%E6%96%87%E6%A1%A3.assets/74665614a812a8a8eb34e7d573a1b9d1.png)]

其实集中过期,倒不是非常致命,比较致命的缓存雪崩,是缓存服务器某个节点宕机或断网。因为自然形成的缓存雪崩,一定是在某个时间段集中创建缓存,这个时候,数据库也是可以顶住压力的。无非就是对数据库产生周期性的压力而已。而缓存服务节点的宕机,对数据库服务器造成的压力是不可预知的,很有可能瞬间就把数据库压垮。

双十一:停掉一些服务,来保证主要的服务可用!

解决方案

redis高可用

这个思想的含义是,既然 redis有可能挂掉,那我多增设几台 redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。(异地多活!)

限流降级

这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程査询数据和写缓存,其他线程等待。

数据预热

数据加热的含义就是在正式部署之前,我先把可能的数据先预先访闩-遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值