Redis

268 阅读27分钟

nosql讲解

为什么要用nosql(nosql的演变)

  1. 单机mysql,遇到了瓶颈

    1. 数据量太大,一个机器放不大
    2. 数据的索引(B+树),一个机器内存放不下
    3. 访问量(读写混合),一个服务器承受不了
  2. Memcached(缓存) + mysql + 垂直拆分(读写分离,主从同步)

    1. 优化数据结构和索引 -> 文件缓存(IO效率低) -> Memcached(当时最热门的技术)
    2. 图解image.png
  3. 分库分表 + 水平拆分 + mysql集群 image.png

  4. 到现在关系型数据库不够用了,数据量太大,数据变化太快。因此引入nosql

  5. 为什么nosql能实现呢?

一个企业入门的架构

image.png

到底什么是nosql

  1. nosql = not only sql 泛指非关系型数据库

nosql的特点

  1. 方便扩展(数据之间没有关系,很好扩展)
  2. 大数据量高性能(redis每秒写8万次,读11万次,缓存是记录级的,是一种细粒度的缓存,性能很高)
  3. 数据类型是多样性的,不需要实现设计数据库,随取随用
  4. 传统RDBMS和nosql的区别
  • 传统RDBMS
结构化
结构化查询SQL
数据和关系都存储在单独的表中
严格的一致性acid
基础的事务
...
  • nosql
不仅仅是数据
没有固定的查询语言
键值对存储、列存储、文档存储、图形数据库(如社交关系)
CAP定理和BASE,(异地多活)
三高:高性能、高可用、高扩展
  1. 了解大数据时代的3V+3高
  • 3V:用于描述问题的
    • 海量 Volume
    • 多样 Variety
    • 实时 Velocity
  • 3高
    • 高性能
    • 高可拓(随时水平拆分,机器不够了,随时加机器,搭建集群)
    • 高并发

阿里巴巴架构演进

image.png

nosql数据模型

nosql四大分类

  • 键值对存储
  • 列存储
  • 文件存储
  • 图形数据库

CAP

BASE

Redis入门

  1. Redis Remote Dictionary Server 远程字典服务,c语言编写
  2. Redis能干嘛
    1. 内存存储,可以持久化,由于内存断电即失,因此持久化很重要,技术手段有RDB和AOF
    2. 效率高,可用于高速缓存
    3. 实现发布订阅系统,
    4. 实现地图信息分析
    5. 计时器、计数器(浏览量等等)
    6. ...
  3. Redis的特性
    1. 开源
    2. 持久化
    3. 事务
    4. 集群
    5. 跨语言
  4. Redis是单线程的,是基于内存操作的,cpu不是redis的性能瓶颈,redis的瓶颈是机器内存大小和网络带宽
  5. 不比同样是key-value的memcache差
  6. redis所有数据都是放在内存中的,没有CPU的上下文切换,因此性能很高

Redis安装(windows / linux)

  1. Redis推荐在linux上使用,windows在GitHub上停更很久了
  2. linux下,找到redis.conf,把daemonize no 改为 yes,以支持redis在后台启动
  3. 找到redis-server,执行redis-server /xx路径/redis.conf,进行后台启动 image.png
  4. redis-cli -p 6379,ping pong连接成功 image.png
  5. ps -ef | grep redis 查看redis进程 image.png
  6. 关闭服务,在redis客户端执行shutdown image.png
  7. benchmark性能测试:100个并发 100000个请求 redis-benchmark -h localhost -p 6379 -c 100 -n 100000

image.png

Redis官方描述

Redis是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件MQ。它支持多种类型的数据结构,如字符串( strings),散列( hashes),列表(ists),集合(sets),有序集合( sorted sets)与范围查询bitmaps, hyperloglogs和地理空间( geospatial)索引半径查询。 Redis内置了复制( replication),LUA脚本(Luascripting),LRU驱动事件( LRU eviction),事务( transactions)和不同级别的磁盘持久化( persistence),井通过Redis哨兵(Sentinel)和自动分区( Cluster)提供高可用性( high availability

Redis-key

  1. select <dbid> 选择库,默认有16个数据库,默认选中0
  2. set <key> <value> 设置键值对,key相同会覆盖
  3. get <key> 获取键对应的值
  4. keys * 查看所有的键
  5. dbsize 查看当前数据库大小
  6. fulldb 清空当前数据库
  7. fullall 清空所有的数据库
  8. keys * 列出所有key
  9. move <key> <dbID> 把key移动到指定数据库
  10. EXSITS <key> 判断key是否存在
  11. expire <key> timesecond 设置key的过期时间
  12. ttl <key> 检查key的过期时间
  13. type <key> 查看key的类型
  14. del <key> 删除key

五大基本数据类型

String

  1. APPEND <key> <value> 拼接字符串/如果当前<key>不存在,就相当于set <key>
  2. STRLEN <key> 查看字符串的长度
  3. incr <key> 自增1
  4. decr <key> 自减1
  5. incrby <key> <num> 自增num
  6. decrby <key> <num> 自减num
  7. GETRANGE <key> <start> <stop> 获取部分字符串0 n ,如果是0 -1则表示获取全部字符串
  8. SETRANGE <key> <num> <value> 替换指定位置的字符串
  9. setex <key> <time> <value> 设置过期时间的key
  10. setnx <key> 不存在则设置key(在分布式锁中会常使用),1设置成功 0设置失败
  11. mset <key> <value> <key> <value> 批量设置kv,如果k相同会保留后者,是原子性操作
  12. mget <key> <key> 批量获取key,是原子性操作,要么一起成功,要么一起失败
  13. msetnx <key> <value> <key> <value> 如果不存在,则创建
  14. set user:1 {name:kieran,age:1} 设置对象
  15. mset user:1:name kieran user:1:age 1 以字段为单位,设置对象
  16. getset组合命令,如果不存在值,返回null,如果存在值,设置新的值,但返回原来的值
    127.0.0.1:6379> getset k5 v5
    (nil)
    127.0.0.1:6379> get k5
    "v5"
    127.0.0.1:6379> getset k5 v55
    "v5"
    127.0.0.1:6379> get k5
    "v55"
    
  17. String的使用场景
    1. 计数器
    2. 对象缓存存储
    3. 粉丝数、访问量

List

  1. 一边进一边出,可以当作队列
  2. 只能在一边进出,可以当作栈
  3. 两边都能进,可以当作阻塞队列
  4. LPUSH <list> <value1> <value2> 向列表的队头插入value
  5. RPUSH <list> <value1> <value2> 向列表的队尾插入value
  6. LRANGE <list> <start> <stop> 查看队列的值,没有RRANGE,可以从-1
  7. LPOP <list> 从队头弹出一个值
  8. RPOP <list> 从队尾弹出一个值
  9. lindex <list> <index> 查询下标的值 0,1,2开始是顺序查询,-1,-2,-3开始是倒序查询
  10. llen <list> 查询长度
  11. lrem <list> <count> <value> 移除列表中指定数量的值
  12. ltrim <list> <start> <stop> 保留指定下标范围内的值
  13. rpoplpush <source> <destination> 弹出原列表的队尾,插入新列表的队头
  14. exists <list1> <list2> 判断列表是否存在
  15. lset <list> <index> <value> 在列表的指定下标替换一个值,替换不存在的下标会报错
  16. linsert <list> before/after <point> <value> 向列表中指定的值point(注意:不是下标)的前面或后面插入一个值,但当有多个point相同时,只会选择第一个point作为插入的位置
  17. 消息队列(lpush rpop) 栈(lpush lpop)

Set

  • set会对纯数字进行升序排序
  1. sadd <set> <value1> <value2> 向集合中添加一个值
  2. sismember <set> <value> 判断值是否存在
  3. smembers <set> 查看集合的所有值
  4. scard <set> 查看集合中元素的个数
  5. srem <set> <value1> <value2> 删除集合中的值
  6. srandmember <set> <count> 选择随机的count个值(随机取值)
  7. spop <set> <count> 随机移除两个值
  8. smove <source> <destination> <value> 从原集合移动指定的值到新集合
  9. sdiff <set1> <set2> 以前一个集合为参照,找出两个集合不同的值
  10. sinter <set1> <set2> 找到两个集合的交集
  11. sunion <set1> <set2> 求出两个集合的去重并集
  12. 场景:微博的明星,以及明星的粉丝,并找出两个明星的共同粉丝
    • sadd starSet starA starB
    • sadd starA fanA fanB fanC
    • sadd starB fanB fanC fanD
    • sinter starA starB
    • 得到共同粉丝fanB fanC

Hash

  • Map集合,Key-Map,Key-<Key,Value>,hash更适合对象存储
  1. hset <key> <field> <value> 设置hash的某个属性的值
  2. hget <key> <field> 获取hash的指定属性的值
  3. hmset/hset <key> <field1> <value1> <field2> <value2> 批量设置hash的多个属性的值,相同属性会覆盖
  4. hmget <key> <field1> <field2> 批量获取hash的多个属性的值
  5. hgetall <key> 获取hash的所有属性的值,获取时是以 的形式展现
  6. hdel <key> <field1> <field2> 删除多个属性及值
  7. hlen <key> 查询hash的大小
  8. hexists <key> <field> 判断hash中指定的属性是否存在
  9. hkeys <key> 获取hash的所有<field>
  10. hvals <key> 获取hash的所有<value>
  11. hincrby <key> <field> <increment> 当increment>0,自增increment 当increment<0,自减increment
  12. hsetnx <key> <field> <value> 如果field不存在,则新增,否则失败

Zset

  • 有序集合,适用于带权重的场景,比如列出工资、普通消息/重要消息
  1. zadd <key> <score> <value> 向有序集合中添加值,score越小,顺序越靠前
  2. zrange <key> <start> <stop> 查看升序排列的集合的值 0 -1查看全部
  3. zrangebyscore <key> <min> <max> [withscores] [limit <start> <stop>]
    • 选择一个区间[min - max],根据score升序排序,witehscores显示score的值,结果数量为limit个,0 -1查询全部
    • zrangebyscore salary -inf +inf withscores limit 0 -1 查询score从负无穷到正无穷的全部含score数据

image.png

  • zrangebyscore salary 1000 9000 limit 0 3 查询score从1000到9000的数据,只查3条记录

image.png

  1. zrevrange <key> <start> <stop> 查看降序排列的集合的值 0 -1查看全部
  2. zrevrangebyscore <key> <max> <min> [withscores] [limit <start> <stop>]
    • 选择一个区间[max - min],根据score降序排序,witehscores显示score的值,结果数量为limit个,0 -1查询全部
    • zrangebyscore salary +inf -inf withscores limit 0 -1 查询score从负无穷到正无穷的全部含score数据
  3. zrem <key> <value1> <value2> 删除有序集合中的元素
  4. zcard <key> 获取有序集合中元素的个数
  5. zcount <key> <min> <max> 统计区间内元素的个数

三种特殊数据类型

geospatial

  • 地理位置
  • 一般都是通过下载城市数据,然后通过java导入
  • 底层原理其实是zset
  1. geoadd <key> <longitude> <latitude> <member> 经度纬度
  2. geoadd china:city 118.796877 32.060255 nanjing 121.473701 31.230416 shanghai 添加南京、上海坐标
  3. geopos <key> <member> <member> 获取member的经度纬度
  4. geodist <key> <member> <member> [unit] 获取两个地点的直线距离
    • unit选填,默认为m[米]
    • 单位:m[米] km[千米] mi[英里] ft[英尺]
  5. 需求:获取附近的人
      1. 获取所有人的定位,通过半径来查询
      1. georadius china:city 121 31 500 km 以经纬度121 31为中心,查找500km范围内的china:city
    • 完整的命令为 georadius key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
  6. 找指元素(非经纬度)置周围的位置
    • georadiusbymember china:city shanghai 500 km
    • 完整的命令为 georadiusbymember key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
  7. 查看位置的hash值(将二维的经纬度转化为一维的字符串,如果两个字符串越像,则越接近)
    • geohash china:city shanghai nanjing
    • geohash <key> <member> <member>
  8. geospatial的底层是zset,因此可以使用zset来操作
  9. zrange <key> <start> <stop> 查看所有的geospatial的元素

hyperloglog

  1. 什么是基数?一组数据中,去重后的数据个数
  2. hyperloglog在内存中的存储是固定的,可计算2^64个基数,最大12KB,数据量少时使用稀疏矩阵存储,数据量过大才会转为稠密矩阵
  3. hyperloglog不存储数据元素本身,只进行统计,因此无法像集合set一样查看有哪些数据元素
  4. hyperloglog本身其实是一种算法,并非redis独有,因此这个数据类型的底层也是个算法
  5. 存在0.81%的标准误差,但一般不影响统计结果
  6. hyperloglog数据结构由Philippe Flajolet教授发明,因此redis中的命令也是由pf开头
  7. 应用场景:统计注册IP数、统计网页UV、统计每天在线用户数、统计每天搜索不同词条的个数
  8. pfadd <key> <element> <element> 新增元素
  9. pfcount <key> 统计基数
  10. pfmerge <key> <source> <source> 合并并去重,key不存在,则会新建key

bitmap

  • bitmap中只有0 1两个状态,因此统计只有两个状态的场景,比如365天打卡,365天=365bit
  • 位运算+位存储
  1. setbit <key> <offset> <value> 添加指定下标的bitmap数据,可以不从0开始,但其他没存的数据返回0
  2. getbit <key> <offset> 查询指定下标的bitmap数据
  3. bitcount <key> 求和bitmap

bitField

stream (stream = MQ消息中间件 + 阻塞队列)

image.png

image.png

image.png

image.png

redis热key功能

  1. config get maxmemory-policy
  2. config set maxmemory-policy allkeys-lfu 设置内存淘汰策略
  3. redis-cli -p 6379 --hotkeys
# Scanning the entire keyspace to find hot keys as well as
# average sizes per key type.  You can use -i 0.1 to sleep 0.1 sec
# per 100 SCAN commands (not usually needed).


-------- summary -------

Sampled 9 keys in the keyspace!

image.png

redis的大key功能

单个key不要超过1mb,set zset不要超过10000个 redis-cli --bigkeys

Redis的过期策略

image.png

lua

redis运行lua脚本,实际上是把整个脚本当成一个事务,让这个脚本的内容保证不可中断,不可拆分的原子性,但是当脚本内容执行出错时,无法做到要么成功,要么失败,无法保证这种原子性。因此讨论lua的原子性其实是分两种原子性的角度来分析。

Redis事务

  • 事务:正常来说,事务是保证原子性的,要么全部成功,要么全部失败
  • redis事务:
    • 本质上是redis事务是一组redis命令,一个事务中所有的命令都会被序列化,按照一次性、顺序性、排他性执行。
    • redis对于单条命令能保证原子性,但是redis事物无法保证原子性,并不支持事务回滚
    • redis事物没有隔离级别,因为不会立即执行,也就不存在事务里的查询想看到事务里的更新
    • 事务默认关闭
      • 事务关闭,数据直接写入内存
      • 事务开启,数据不会立刻执行,而是进入队列,返回队列状态,等待exec命令才会执行commands中的命令
  • redis事务操作
  1. 事务命令
  • 开启事务(multi)
  • 命令入队
  • 执行事务(exec)
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> get k1
QUEUED
127.0.0.1:6379> set k1 v3
QUEUED
127.0.0.1:6379> get k1
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK
3) "v1"
4) OK
5) "v3"
6) OK
  1. 取消事务命令discard (也会解锁watch乐观锁)
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k5 v5
QUEUED
127.0.0.1:6379> set k6 v6
QUEUED
127.0.0.1:6379> set k7 v7
QUEUED
127.0.0.1:6379> get k7
QUEUED
127.0.0.1:6379> discard
OK
127.0.0.1:6379> exec
(error) ERR EXEC without MULTI
  1. 异常

    • 编译型异常:代码有错误、命令有错误,事务里的命令都不会被执行
    127.0.0.1:6379> multi
    OK
    127.0.0.1:6379> set k1 v1
    QUEUED
    127.0.0.1:6379> set k2 v2
    QUEUED
    127.0.0.1:6379> getset k3
    (error) ERR wrong number of arguments for 'getset' command
    127.0.0.1:6379> set k3 v4
    QUEUED
    127.0.0.1:6379> exec
    (error) EXECABORT Transaction discarded because of previous errors.
    127.0.0.1:6379> get k1
    (nil)
    
    • 运行时异常:运行时,事务中的命令存在语法性错误,则会将错误命令抛出异常并跳过当前命令,继续执行后续命令,因此redis事务不保证原子性
    127.0.0.1:6379> multi
    OK
    127.0.0.1:6379> set k1 v1 10n
    QUEUED
    127.0.0.1:6379> set k2 v2
    QUEUED
    127.0.0.1:6379> set k3 v3
    QUEUED
    127.0.0.1:6379> exec
    1) (error) ERR syntax error
    2) OK
    3) OK
    127.0.0.1:6379> mget k1 k2 k3
    1) (nil)
    2) "v2"
    3) "v3"
    
  2. 监控

    • 悲观锁
      • 很悲观,觉得什么时候都会出问题,因此无论什么操作都会加锁
    • 乐观锁
      • 很乐观,觉得什么时候都不会出现问题,所以不会上锁,更新数据的时候去判断在此期间是否有人更新过这个数据
      • 获取这条数据的version
      • 更新的时候比较version
      • 正常执行的情况
      127.0.0.1:6379> set money 100
      OK
      127.0.0.1:6379> set out 0
      OK
      127.0.0.1:6379> watch money # 监控,待事务成功后,监控会自动取消
      OK
      127.0.0.1:6379> multi #事务开启,在事务期间数据没有发生变动,则事务正常执行成功
      OK
      127.0.0.1:6379> decrby money 20
      QUEUED
      127.0.0.1:6379> incrby out 20
      QUEUED
      127.0.0.1:6379> exec
      1) (integer) 80
      2) (integer) 20
      
      • 异常情况:事务期间数据发生变动,则事务执行失败,以下代码按照并发顺序展示
      # 线程1
      127.0.0.1:6379> watch money
      OK
      127.0.0.1:6379> multi
      OK
      127.0.0.1:6379> decrby money 10
      QUEUED
      127.0.0.1:6379> incrby out 10
      QUEUED
      
      # 线程2
      127.0.0.1:6379> ping
      PONG
      127.0.0.1:6379> get money
      "70"
      127.0.0.1:6379> set money 1000
      OK
      
      # 线程1
      127.0.0.1:6379> watch money
      OK
      127.0.0.1:6379> multi
      OK
      127.0.0.1:6379> decrby money 10
      QUEUED
      127.0.0.1:6379> incrby out 10
      QUEUED
      --- 以下是接着第一段代码 ---
      127.0.0.1:6379> exec # 事务执行失败
      (nil)
      127.0.0.1:6379> unwatch # 事务执行失败,需要手动关闭watch
      OK
      127.0.0.1:6379> watch money # 再次开启
      OK
      127.0.0.1:6379> multi
      OK
      127.0.0.1:6379> decrby money 10
      QUEUED
      127.0.0.1:6379> incrby out 10
      QUEUED
      127.0.0.1:6379> exec
      1) (integer) 990 # 此处由于线程2的影响,被decrby的值早已修改
      2) (integer) 10
      

通过Jedis访问Redis

获取依赖

  1. 通过 mvnrepository.com/package-search.jetbrains.com/ 搜索Jedis
  2. 获取依赖,本文是Gradle implementation 'redis.clients:jedis:4.4.3'
  3. Jedis - ping pong
    import redis.clients.jedis.Jedis;
    
    public class TestPing {
        public static void main(String[] args) {
            Jedis jedis = new Jedis("127.0.0.1", 6379);
    
            // jedis含有所有原有命令
            String pong = jedis.ping();
            System.err.println(pong);
    
            jedis.select(0);
            jedis.flushDB();
            jedis.close();
        }
    }
    
  4. Jedis - hash
import java.util.HashMap;
import java.util.Map;

public class TestHash {
  public static void main(String[] args) {
      Jedis jedis = new Jedis("127.0.0.1", 6379);
      Map<String, String> map = new HashMap<>();
      map.put("a", "a");
      map.put("b", "b");
      map.put("c", "c");
      map.put("d", "d");
      map.put("e", "e");
      map.put("f", "f");

      jedis.hset("engineer:china", map);
      jedis.close();
  }
}
  1. Jedis - 事务
import com.alibaba.fastjson.JSONObject;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;

/**
 * 事务
 */
public class TestWork {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        JSONObject result = new JSONObject();
        result.put("name", "kieran");
        result.put("age", "10");
        String data = result.toJSONString();

        Transaction multi = null;
        try {
            multi = jedis.multi();
            multi.set("user1", data);
            multi.set("user2", data);
            multi.exec();
        } catch (Exception e) {
            e.printStackTrace();
            if (null != multi) {
                multi.discard();
            }
        } finally {
            System.err.println(jedis.get("user1"));
            System.err.println(jedis.get("user2"));
            jedis.close();

        }
    }
}
  1. Jedis - 乐观锁
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;

/**
 * 乐观锁
 */
public class TestWatch {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        jedis.set("money", "100");
        jedis.set("out", "0");

        Transaction multi = null;
        try {
            String setWatchResult = jedis.watch("money");
            System.err.println(setWatchResult);
            multi = jedis.multi();
            multi.decrBy("money", 20);
            multi.incrBy("out",20);
            multi.exec();
        } catch (Exception e) {
            e.printStackTrace();
            if (null != multi) {
                multi.close();
            }
            jedis.unwatch();
        } finally {
            System.err.println(jedis.get("money"));
            System.err.println(jedis.get("out"));
            jedis.close();
        }
    }
}
  1. Jedis - 乐观锁生效,事务执行失败
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;

import java.util.List;

/**
 * 编译时异常,watch会提示失败
 * 场景:学生人数减少n名,毕业人数增加n名
 *
 * 测试步骤
 * 1. 检查redis中是否有 student、graduate 两个key,有则删除
 * 2. main方法的代码全部保留,并执行一次,此时的结果应该是线程1的打印结果应该是10 2,实际上在redis中已经执行失败了
 * 3. 只保留new Thread(thread1()).start();代码,其他main方法的代码注释掉,并执行1次,此时的结果应该是线程1的打印结果应该是8 4,在redis中执行成功了
 * 4. 说明 毕业人数为4名是因为,在线程2执行时,打断了线程1的事务,并且成功把毕业人数设置为2,因此后续线程1执行是,毕业人数的基准是从2人开始的
 */
public class TestExceptionWatch {
    public static void main(String[] args) {
//        Jedis jedis = new Jedis();
//        jedis.set("student", "10");
//        jedis.set("graduate", "0");
//        jedis.close();

        // 此处需要两个线程;
        new Thread(thread1()).start();
//        new Thread(thread2()).start();
    }

    private static Runnable thread1() {
        return () -> {
            Jedis jedis = new Jedis("127.0.0.1", 6379);
            Transaction multi = null;

            try {
                String result = jedis.watch("graduate");
                System.err.println(result);

                multi = jedis.multi();
                multi.decrBy("student", 2);
                multi.incrBy("graduate", 2);

                // 模拟此时有一个线程2,在修改graduate的值
                Thread.sleep(8000);

                // TODO:: 如何得知事务执行失败了?如何打印失败信息?
                List<Object> exec = multi.exec();

                if (null == exec) {
                    System.err.println("事务失败");
                } else {
                    for (Object object : exec) {
                        System.err.println(object.toString());
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
                if (null != multi) {
                    multi.discard();
                }
                jedis.unwatch();
            } finally {
                System.err.println(jedis.get("student"));
                System.err.println(jedis.get("graduate"));
                jedis.close();
            }
        };
    }

    private static Runnable thread2() {
        return () -> {
//            Jedis jedis = new Jedis("127.0.0.1", 6379);
//            try {
//                Thread.sleep(2000);
//                jedis.set("graduate", "2");
//            } catch (Exception e) {
//                e.printStackTrace();
//            } finally {
//                jedis.close();
//            }

            // jedis.close(); 的代码简化写法,放在try中,代码执行完时会自动关闭
            try (Jedis jedis = new Jedis("127.0.0.1", 6379)) {
                Thread.sleep(2000);
                jedis.set("graduate", "2");
            } catch (Exception e) {
                e.printStackTrace();
            }
        };
    }
}

Springboot集成Redis

为什么不使用Jedis了,而被换成了lettuce

  1. jedis:采用直连,多个线程的话,是不安全的,如果想安全,需要使用jedis pool连接池,如果线程数量过多,那么redis-server数量就会很大,更像BIO模式
  2. lettuce:底层使用netty,实例可以在多个线程中共享,不存在线程不安全的问题,可以减少线程数量,更像NIO模式
  3. 导入redis依赖
    • implementation 'org.springframework.boot:spring-boot-starter-data-redis:3.1.0'
    • implementation 'org.springframework.data:spring-data-redis:3.0.6'
  4. 源码分析
package org.springframework.boot.autoconfigure.data.redis;

import java.net.UnknownHostException;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;

/**
 * {@link EnableAutoConfiguration Auto-configuration} for Spring Data's Redis support.
 *
 * @author Dave Syer
 * @author Andy Wilkinson
 * @author Christian Dupuis
 * @author Christoph Strobl
 * @author Phillip Webb
 * @author Eddú Meléndez
 * @author Stephane Nicoll
 * @author Marco Aust
 * @author Mark Paluch
 */
@Configuration
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {

   @Bean
   @ConditionalOnMissingBean(name = "redisTemplate") // 该注解表示如果[redisTemplate]bean不存在,则使用此默认方法,因此可以自定义redisTemplate
   public RedisTemplate<Object, Object> redisTemplate(
         RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
      // 默认的redisTemplate没有过多的设置,redis对象一定需要序列化
      // 默认的泛型是<Object, Object>,在后续使用需要强转为<String, Object>,因此需要使用自定义RedisTemplate
      RedisTemplate<Object, Object> template = new RedisTemplate<>();
      template.setConnectionFactory(redisConnectionFactory);
      return template;
   }

   @Bean
   @ConditionalOnMissingBean // 由于string是redis中最常使用的类型,所以单独提出来了一个bean
   public StringRedisTemplate stringRedisTemplate(
         RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
      StringRedisTemplate template = new StringRedisTemplate();
      template.setConnectionFactory(redisConnectionFactory);
      return template;
   }

}
  1. 设置配置
spring.redis.host=127.0.0.1
spring.redis.port=6379
  1. 自定义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;

/**
 * Redis配置
 */
@Configuration
public class RedisConfig {

    /**
     * 自定义RedisTemplate 固定模版
     * @param factory RedisConnectionFactory
     * @return RedisTemplate
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        // 为了自己开发方便,直接使用<String, Object>
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(factory);

        // Json序列化配置
        Jackson2JsonRedisSerializer<Object> objectJackson2JsonRedisSerializer
                = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        objectJackson2JsonRedisSerializer.setObjectMapper(om);

        // String序列化
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        // key采用String序列化
        redisTemplate.setKeySerializer(stringRedisSerializer);
        // hashKey采用String序列化
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        // value采用Json序列化
        redisTemplate.setValueSerializer(objectJackson2JsonRedisSerializer);
        // hash的value采用Json序列化
        redisTemplate.setHashValueSerializer(objectJackson2JsonRedisSerializer);
        redisTemplate.afterPropertiesSet();

        return redisTemplate;
    }
}
@Autowired
@Qualifier("redisTemplate") // 指定Bean的名称
# @Resource(name = "redisTemplate") // 二选一
private RedisTemplate redisTemplate;
  1. RedisUtils(基于RedisTemplate的Redis工具包,可到网上按需寻找)

Redis配置文件解析

www.cnblogs.com/FondWang/p/…

Redis持久化

RDB (Redis Database)

image.png

  1. save规则满足的情况下,会产生RDB文件
  2. flushdb/flushall时,会产生RDB文件
  3. redis shutdown时,会产生RDB文件

如何恢复

只要把dump.rdb文件放在redis启动目录下就行,获取启动目录的命令: 在redis客户端输入 config get dir

127.0.0.1:6379> config get dir
1) "dir"
2) "/usr/local/var/db/redis"

例如:/usr/local/var/db/redis/dump.rdb

优缺点

  1. 数据量大时,性能表现很好
  2. 对数据的完整性要求度不高!
  3. 如果在等待备份redis宕机了,会丢失数据,导致最后一次的数据丢失
  4. fork进程的时候,会花费一定的内存空间
  5. 一般来说 在生产环节要对 dump.rdb文件做一个备份

在主从复制中

在主从复制中,rdb一般作为备用文件,放在从机上

AOF (Append Only File)

image.png

AOF修复

  1. 修复命令:redis-check-aof --fix <filename>
  2. 查看差异:diff -u

rewrite

image.png

  1. appendonly.aof文件容量超过阈值,就会fork一个新的子线程,重写一份新的appendonly.aof文件

优缺点

  1. 数据完整性更高,甚至完整
  2. 默认同步策略:每秒同步一次,可能会丢失一秒钟的数据
  3. 默认开启:不开启
  4. AOF文件会非常的大,只拼接,不修改
  5. AOF效率很低

RDB / AOF 拓展

image.png

Redis实现订阅发布(消息队列也能实现)

发布(PUB) 订阅(SUB)

发布

127.0.0.1:6379> ping
PONG
127.0.0.1:6379> PUBLISH kieran-say hello friends
(error) ERR wrong number of arguments for 'publish' command
127.0.0.1:6379> PUBLISH kieran-say 'hello my friends'
(integer) 1
127.0.0.1:6379> PUBLISH kieran-say 'how are you '
(integer) 1
127.0.0.1:6379>

订阅

127.0.0.1:6379> SUBSCRIBE kieran-say
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "kieran-say"
3) (integer) 1

1) "message"
2) "kieran-say"
3) "hello my friends"

1) "message"
2) "kieran-say"
3) "how are you "

模式订阅 (订阅it开头的所有频道)

127.0.0.1:6379> PSUBSCRIBE it*
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "it*"
3) (integer) 1
1) "pmessage"
2) "it*"
3) "itx"
4) "hi"

模式发布

127.0.0.1:6379> PUBLISH itx hi
(integer) 1
127.0.0.1:6379>

其他命令

  1. unsubscribe channels
  2. punsubscribe [pattern...]
  3. pubsub channels [pattern] 查询订阅
  4. pubsub numsub [channel ...] 查看频道订阅数
  5. pubsub numpat 查看模式订阅数

Redis集群模式1: 主从复制

配置要求

  1. 最低要求:1主2从
  2. 一般来说:单机redis内存达到20个G,就应该立刻搭建一个新的redis集群

第一步:配置redis.conf文件

  1. 生成主从对应个数的redis.conf image.png
  2. 修改redis.conf中的参数
    • port xxxx 改为redis服务的对应端口
    • daemonize yes 设置redis服务为守护进程,在后台运行
    • pidfile /var/run/redis_$port.pid 修改为对应的端口号
    • logfile "$port.log" 不能为空,否则每个配置文件都为空会重复,一般以端口号命名log
    • dbfilename dump-$port.rdb 如果以RDB为持久化,则需要修改rdb文件名字/AOF持久化则修改AOF的文件

第二步:启动三台redis服务

  1. redis-server redisconfig/redis-m-6379.conf
  2. redis-cli -p 6379
  3. redis-server redisconfig/redis-s-6380.conf
  4. redis-cli -p 6380
  5. redis-server redisconfig/redis-s-6381.conf
  6. redis-cli -p 6381

第三步:命令配置从机(临时的,断电即失)

在客户端中执行 slaveof host port,如slaveof 127.0.0.1 6379 通过命令 info replaction查看机器信息

##主机##
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:2
slave0:ip=127.0.0.1,port=6380,state=online,offset=210,lag=1
slave1:ip=127.0.0.1,port=6381,state=online,offset=210,lag=1
master_replid:9517f26977b172370e1e1144e492129e718d0323
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:210
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:210
##从机##

127.0.0.1:6380> slaveof 127.0.0.1 6379
OK
127.0.0.1:6380> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
master_link_status:up
master_last_io_seconds_ago:5
master_sync_in_progress:0
slave_repl_offset:14
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:9517f26977b172370e1e1144e492129e718d0323
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:14
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:14

第四步:永久保存从机配置需要在配置文件中配置

  1. replicaof <masterip> <masterport> 主机IP 主机端口
  2. masterauth <master-password> 主机登陆密码

第五步:可以在本地看到根据配置生成的rdb文件和log文件

image.png

主从复制的细节

  1. 主机负责写,从机只能读。主机写完,从机会自动同步数据。从机无法进行写操作。
  2. 如果主机宕机了,从机依旧连接着主机可以进行读,但是不能写了。当主机再次连接上,从机依旧可以读取到主机新写的数据
  3. 只要主机和从机相连,一定会执行一次“全量复制”,即主从数据一致
  4. 如果主机断了,可以选择使用命令 slaveof no one 来使自己成为主机。其他从机需要重新slaveof

redis集群的另一种模型:级联复制

  1. 主机6379
  2. 从机6380 slaveof 6379
  3. 从机6381 slaveof 6380
  4. 此时6380,理论上即是6379的从机又是6381的主机,但使用info replicaton查看,6380仍然是从机,因此仍然无法进行写操作
  5. 以这种模式运行时:6379写入数据,6380、6381都能读到数据

Redis哨兵模式

  1. 哨兵会定时发送心跳包,判断监控的redis服务是否宕机,如果宕机则主观下线,随后其他哨兵尝试连接,如果都连接不上,则进行选举,由一个哨兵选举出一个新的主机,并自动配置从机,而宕机的服务则称为客观下线

简答的配置单个哨兵

  1. 新建 sentinel.conf配置文件
  2. 编写命令 sentinel monitor <master-name> <redisIP> <redisPort> <quorum>
  3. 例如 sentinel monitor mySentinel 127.0.0.1 6379 1
  4. 执行命令 redis-sentinel redisconfig/sentinel.conf
1151:X 08 Aug 2023 15:13:10.464 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
1151:X 08 Aug 2023 15:13:10.464 # Redis version=5.0.7, bits=64, commit=00000000, modified=0, pid=1151, just started
1151:X 08 Aug 2023 15:13:10.464 # Configuration loaded
1151:X 08 Aug 2023 15:13:10.465 * Increased maximum number of open files to 10032 (it was originally set to 256).
                _._
           _.-``__ ''-._
      _.-``    `.  `_.  ''-._           Redis 5.0.7 (00000000/0) 64 bit
  .-`` .-```.  ```\/    _.,_ ''-._
 (    '      ,       .-`  | `,    )     Running in sentinel mode
 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 26379
 |    `-._   `._    /     _.-'    |     PID: 1151
  `-._    `-._  `-./  _.-'    _.-'
 |`-._`-._    `-.__.-'    _.-'_.-'|
 |    `-._`-._        _.-'_.-'    |           http://redis.io
  `-._    `-._`-.__.-'_.-'    _.-'
 |`-._`-._    `-.__.-'    _.-'_.-'|
 |    `-._`-._        _.-'_.-'    |
  `-._    `-._`-.__.-'_.-'    _.-'
      `-._    `-.__.-'    _.-'
          `-._        _.-'
              `-.__.-'

1151:X 08 Aug 2023 15:13:10.466 # Sentinel ID is c11dec5097bc1df1a194b323dd0ba4235482a6e2
1151:X 08 Aug 2023 15:13:10.466 # +monitor master redis-sentinel 127.0.0.1 6379 quorum 1
1151:X 08 Aug 2023 15:13:10.467 * +slave slave 127.0.0.1:6380 127.0.0.1 6380 @ redis-sentinel 127.0.0.1 6379
1151:X 08 Aug 2023 15:13:10.467 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ redis-sentinel 127.0.0.1 6379
  1. 当主机下线时,会自动选举从机变更为主机,并修正其他从机的订阅主机。当旧主机再次上线时不再是主机,而是新主机的从机
1151:X 08 Aug 2023 15:14:31.272 # +sdown master redis-sentinel 127.0.0.1 6379
1151:X 08 Aug 2023 15:14:31.272 # +odown master redis-sentinel 127.0.0.1 6379 #quorum 1/1
1151:X 08 Aug 2023 15:14:31.272 # +new-epoch 1
1151:X 08 Aug 2023 15:14:31.272 # +try-failover master redis-sentinel 127.0.0.1 6379
1151:X 08 Aug 2023 15:14:31.273 # +vote-for-leader c11dec5097bc1df1a194b323dd0ba4235482a6e2 1
1151:X 08 Aug 2023 15:14:31.273 # +elected-leader master redis-sentinel 127.0.0.1 6379
1151:X 08 Aug 2023 15:14:31.273 # +failover-state-select-slave master redis-sentinel 127.0.0.1 6379
1151:X 08 Aug 2023 15:14:31.327 # +selected-slave slave 127.0.0.1:6381 127.0.0.1 6381 @ redis-sentinel 127.0.0.1 6379
1151:X 08 Aug 2023 15:14:31.327 * +failover-state-send-slaveof-noone slave 127.0.0.1:6381 127.0.0.1 6381 @ redis-sentinel 127.0.0.1 6379
1151:X 08 Aug 2023 15:14:31.411 * +failover-state-wait-promotion slave 127.0.0.1:6381 127.0.0.1 6381 @ redis-sentinel 127.0.0.1 6379
1151:X 08 Aug 2023 15:14:31.668 # +promoted-slave slave 127.0.0.1:6381 127.0.0.1 6381 @ redis-sentinel 127.0.0.1 6379
1151:X 08 Aug 2023 15:14:31.668 # +failover-state-reconf-slaves master redis-sentinel 127.0.0.1 6379
1151:X 08 Aug 2023 15:14:31.761 * +slave-reconf-sent slave 127.0.0.1:6380 127.0.0.1 6380 @ redis-sentinel 127.0.0.1 6379
1151:X 08 Aug 2023 15:14:32.718 * +slave-reconf-inprog slave 127.0.0.1:6380 127.0.0.1 6380 @ redis-sentinel 127.0.0.1 6379
1151:X 08 Aug 2023 15:14:32.718 * +slave-reconf-done slave 127.0.0.1:6380 127.0.0.1 6380 @ redis-sentinel 127.0.0.1 6379
1151:X 08 Aug 2023 15:14:32.796 # +failover-end master redis-sentinel 127.0.0.1 6379
1151:X 08 Aug 2023 15:14:32.796 # +switch-master redis-sentinel 127.0.0.1 6379 127.0.0.1 6381
1151:X 08 Aug 2023 15:14:32.796 * +slave slave 127.0.0.1:6380 127.0.0.1 6380 @ redis-sentinel 127.0.0.1 6381
1151:X 08 Aug 2023 15:14:32.796 * +slave slave 127.0.0.1:6379 127.0.0.1 6379 @ redis-sentinel 127.0.0.1 6381
1151:X 08 Aug 2023 15:15:02.825 # +sdown slave 127.0.0.1:6379 127.0.0.1 6379 @ redis-sentinel 127.0.0.1 6381
1151:X 08 Aug 2023 15:15:32.135 # -sdown slave 127.0.0.1:6379 127.0.0.1 6379 @ redis-sentinel 127.0.0.1 6381
1151:X 08 Aug 2023 15:15:42.115 * +convert-to-slave slave 127.0.0.1:6379 127.0.0.1 6379 @ redis-sentinel 127.0.0.1 6381
  1. 优缺点

image.png

  1. 完整的哨兵配置文件(哨兵集群则需要多个哨兵配置文件,像主从复制的配置一样)

image.png

image.png

image.png

image.png

image.png

缓存穿透及解决方案(特点:查不到)

  1. 缓存穿透:redis缓存中没有,数据库也没有,那么请求会一直打到数据库上,如果洪水攻击,就让数据库宕机
  2. 解决方案:布隆过滤器、缓存空对象(空对象影响性能,而且可能与数据库的数据不一致,即数据库有了,缓存还是空)

缓存击穿及解决方案(特点:量太大、key过期)

  1. 缓存击穿:在一个热key从过期到恢复的时间段中,大量的查询请求打进来,导致全部打入数据库造成的宕机
  2. 解决方案:设置key永不过期、分布式锁、互斥锁

缓存雪崩及解决方案(特点:缓存集体失效、redis宕机,所有缓存全部没了)

  1. 缓存雪崩:比如双11,热点数据大概率是放在缓存中的,当集体失效,全部打到数据库造成的宕机
  2. 解决方案:redis集群、限流降级、数据预热、设置不同的过期时间

Redis+Lua GPT4的回答

在地理空间信息处理场景下,Redis+Lua 能够提供高效和灵活的解决方案。Redis 是一款高性能的内存数据库,支持多种数据结构,其中地理空间数据类型(GEOSPATIAL)可用于存储地理坐标,如经纬度。结合 Lua 脚本,用户可以编写一些复杂的应用逻辑来处理这些地理空间数据,例如计算两点间的距离,查找附近的目标点等。

以下是一些典型的 Redis+Lua 地理空间应用场景:

  1. 地理围栏:将某一地域划定为一个区域,监控区域内的实时位置信息,例如跟踪物流货车的运输轨迹,判断是否进入某个特定区域,从而实现实时报警和监控。
  2. 位置搜索:通过输入关键词或者分类信息,实现附近相关地点的查询推荐。
  3. 路径规划:利用地理空间数据计算出两点间的最短路径或者最快路径,帮助规划行车或步行路线。
  4. 热力图:根据地理坐标信息,绘制出一定范围内的热力图,可用于了解人流、车流等分布情况。

总之,利用 Redis+Lua,地理空间信息处理变得更加高效,同时也降低了应用的复杂性,有助于处理大量实时地理数据,并支持各种空间查询和运算。

redis的一些学习

redis是cp还是ap

有人说单机redis是cp,集群redis是ap,但我认为redis是ap。因为单机redis是单线程,一致性确实有保障,但一旦这唯一的节点宕机了,还谈什么cp。在分布式中,redis的一致性是通过异步同步的方式,所以无法做到强一致性(扩展:一致性分为强一致性、弱一致性、最终一致性)。redis的设计目的是高性能、高可扩展性、高可用性。分布式集群redis会实现最终一致性。

redis的集群模式

主从模式

哨兵模式

分布式Cluster模式

redis为什么只有16384个hash slot

什么是redis分区

redis使用什么协议进行通信

redis6.0以后使用多线程

原本的单线程是指 在一个线程内完成网络I/O和键值对读写I/O。后用I/O多路复用,实现对多个套接字批量获取,然后批量与内核态进行交互。此时优化了读写I/O。6.0后使用多线程优化网络I/O,并发的获取网络请求,再通过I/O多路复用进行单线程阻塞式的读写。

如何解决缓存和数据库的数据一致性

  1. 可以考虑延迟双删策略,先删缓存,再更新数据库,再过一点时间删除缓存,避免并发造成的脏数据 2. 一般考虑稳定性和对代码的入侵性,可以采用异步删除缓存、更新缓存,使用MQ或者监控数据库的binlog

image.png

如何实现延迟消息

  1. zset score=过期时间(下单时间+过期时长) member=订单号,定时redis扫描 当前时间>score,取出来关单,但是在高并发的场景下,容易出现不保证幂等性的操作,例如多个消费者拿到同一个订单
  2. 可以使用redisson,使用其中的RDelayedQueue解决,这是基于zset实现的延迟列表,是基于内存的延迟队列

redis还能用来干什么

image.png

redis在操作时有哪些建议

image.png