nosql讲解
为什么要用nosql(nosql的演变)
-
单机mysql,遇到了瓶颈
- 数据量太大,一个机器放不大
- 数据的索引(B+树),一个机器内存放不下
- 访问量(读写混合),一个服务器承受不了
-
Memcached(缓存) + mysql + 垂直拆分(读写分离,主从同步)
- 优化数据结构和索引 -> 文件缓存(IO效率低) -> Memcached(当时最热门的技术)
- 图解
-
分库分表 + 水平拆分 + mysql集群
-
到现在关系型数据库不够用了,数据量太大,数据变化太快。因此引入nosql
-
为什么nosql能实现呢?
一个企业入门的架构
到底什么是nosql
- nosql = not only sql 泛指非关系型数据库
nosql的特点
- 方便扩展(数据之间没有关系,很好扩展)
- 大数据量高性能(redis每秒写8万次,读11万次,缓存是记录级的,是一种细粒度的缓存,性能很高)
- 数据类型是多样性的,不需要实现设计数据库,随取随用
- 传统RDBMS和nosql的区别
- 传统RDBMS
结构化
结构化查询SQL
数据和关系都存储在单独的表中
严格的一致性acid
基础的事务
...
- nosql
不仅仅是数据
没有固定的查询语言
键值对存储、列存储、文档存储、图形数据库(如社交关系)
CAP定理和BASE,(异地多活)
三高:高性能、高可用、高扩展
- 了解大数据时代的3V+3高
- 3V:用于描述问题的
- 海量 Volume
- 多样 Variety
- 实时 Velocity
- 3高
- 高性能
- 高可拓(随时水平拆分,机器不够了,随时加机器,搭建集群)
- 高并发
阿里巴巴架构演进
nosql数据模型
nosql四大分类
- 键值对存储
- 列存储
- 文件存储
- 图形数据库
CAP
BASE
Redis入门
- Redis Remote Dictionary Server 远程字典服务,c语言编写
- Redis能干嘛
- 内存存储,可以持久化,由于内存断电即失,因此持久化很重要,技术手段有RDB和AOF
- 效率高,可用于高速缓存
- 实现发布订阅系统,
- 实现地图信息分析
- 计时器、计数器(浏览量等等)
- ...
- Redis的特性
- 开源
- 持久化
- 事务
- 集群
- 跨语言
- Redis是单线程的,是基于内存操作的,cpu不是redis的性能瓶颈,redis的瓶颈是机器内存大小和网络带宽
- 不比同样是key-value的memcache差
- redis所有数据都是放在内存中的,没有CPU的上下文切换,因此性能很高
Redis安装(windows / linux)
- Redis推荐在linux上使用,windows在GitHub上停更很久了
- linux下,找到redis.conf,把daemonize no 改为 yes,以支持redis在后台启动
- 找到redis-server,执行redis-server /xx路径/redis.conf,进行后台启动
- redis-cli -p 6379,ping pong连接成功
- ps -ef | grep redis 查看redis进程
- 关闭服务,在redis客户端执行shutdown
- benchmark性能测试:100个并发 100000个请求
redis-benchmark -h localhost -p 6379 -c 100 -n 100000
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
- redis中文网可查询命令
- redis不区分大小写命令
select <dbid>选择库,默认有16个数据库,默认选中0set <key> <value>设置键值对,key相同会覆盖get <key>获取键对应的值keys *查看所有的键dbsize查看当前数据库大小fulldb清空当前数据库fullall清空所有的数据库keys *列出所有keymove <key> <dbID>把key移动到指定数据库EXSITS <key>判断key是否存在expire <key> timesecond设置key的过期时间ttl <key>检查key的过期时间type <key>查看key的类型del <key>删除key
五大基本数据类型
String
APPEND <key> <value>拼接字符串/如果当前<key>不存在,就相当于set<key>STRLEN <key>查看字符串的长度incr <key>自增1decr <key>自减1incrby <key> <num>自增numdecrby <key> <num>自减numGETRANGE <key> <start> <stop>获取部分字符串0 n ,如果是0 -1则表示获取全部字符串SETRANGE <key> <num> <value>替换指定位置的字符串setex <key> <time> <value>设置过期时间的keysetnx <key>不存在则设置key(在分布式锁中会常使用),1设置成功 0设置失败mset <key> <value> <key> <value>批量设置kv,如果k相同会保留后者,是原子性操作mget <key> <key>批量获取key,是原子性操作,要么一起成功,要么一起失败msetnx <key> <value> <key> <value>如果不存在,则创建set user:1 {name:kieran,age:1}设置对象mset user:1:name kieran user:1:age 1以字段为单位,设置对象- 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" - String的使用场景
- 计数器
- 对象缓存存储
- 粉丝数、访问量
List
- 一边进一边出,可以当作队列
- 只能在一边进出,可以当作栈
- 两边都能进,可以当作阻塞队列
LPUSH <list> <value1> <value2>向列表的队头插入valueRPUSH <list> <value1> <value2>向列表的队尾插入valueLRANGE <list> <start> <stop>查看队列的值,没有RRANGE,可以从-1LPOP <list>从队头弹出一个值RPOP <list>从队尾弹出一个值lindex <list> <index>查询下标的值 0,1,2开始是顺序查询,-1,-2,-3开始是倒序查询llen <list>查询长度lrem <list> <count> <value>移除列表中指定数量的值ltrim <list> <start> <stop>保留指定下标范围内的值rpoplpush <source> <destination>弹出原列表的队尾,插入新列表的队头exists <list1> <list2>判断列表是否存在lset <list> <index> <value>在列表的指定下标替换一个值,替换不存在的下标会报错linsert <list> before/after <point> <value>向列表中指定的值point(注意:不是下标)的前面或后面插入一个值,但当有多个point相同时,只会选择第一个point作为插入的位置- 消息队列(lpush rpop) 栈(lpush lpop)
Set
- set会对纯数字进行升序排序
sadd <set> <value1> <value2>向集合中添加一个值sismember <set> <value>判断值是否存在smembers <set>查看集合的所有值scard <set>查看集合中元素的个数srem <set> <value1> <value2>删除集合中的值srandmember <set> <count>选择随机的count个值(随机取值)spop <set> <count>随机移除两个值smove <source> <destination> <value>从原集合移动指定的值到新集合sdiff <set1> <set2>以前一个集合为参照,找出两个集合不同的值sinter <set1> <set2>找到两个集合的交集sunion <set1> <set2>求出两个集合的去重并集- 场景:微博的明星,以及明星的粉丝,并找出两个明星的共同粉丝
- 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更适合对象存储
hset <key> <field> <value>设置hash的某个属性的值hget <key> <field>获取hash的指定属性的值hmset/hset <key> <field1> <value1> <field2> <value2>批量设置hash的多个属性的值,相同属性会覆盖hmget <key> <field1> <field2>批量获取hash的多个属性的值hgetall <key>获取hash的所有属性的值,获取时是以键值键值的形式展现hdel <key> <field1> <field2>删除多个属性及值hlen <key>查询hash的大小hexists <key> <field>判断hash中指定的属性是否存在hkeys <key>获取hash的所有<field>hvals <key>获取hash的所有<value>hincrby <key> <field> <increment>当increment>0,自增increment 当increment<0,自减incrementhsetnx <key> <field> <value>如果field不存在,则新增,否则失败
Zset
- 有序集合,适用于带权重的场景,比如列出工资、普通消息/重要消息
zadd <key> <score> <value>向有序集合中添加值,score越小,顺序越靠前zrange <key> <start> <stop>查看升序排列的集合的值 0 -1查看全部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数据
zrangebyscore salary 1000 9000 limit 0 3查询score从1000到9000的数据,只查3条记录
zrevrange <key> <start> <stop>查看降序排列的集合的值 0 -1查看全部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数据
zrem <key> <value1> <value2>删除有序集合中的元素zcard <key>获取有序集合中元素的个数zcount <key> <min> <max>统计区间内元素的个数
三种特殊数据类型
geospatial
- 地理位置
- 一般都是通过下载城市数据,然后通过java导入
- 底层原理其实是zset
geoadd <key> <longitude> <latitude> <member>经度纬度geoadd china:city 118.796877 32.060255 nanjing 121.473701 31.230416 shanghai添加南京、上海坐标geopos <key> <member> <member>获取member的经度纬度geodist <key> <member> <member> [unit]获取两个地点的直线距离- unit选填,默认为
m[米] - 单位:
m[米]km[千米]mi[英里]ft[英尺]
- unit选填,默认为
- 需求:获取附近的人
-
- 获取所有人的定位,通过半径来查询
-
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]
-
- 找指元素(非经纬度)置周围的位置
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]
- 查看位置的hash值(将二维的经纬度转化为一维的字符串,如果两个字符串越像,则越接近)
geohash china:city shanghai nanjinggeohash <key> <member> <member>
- geospatial的底层是zset,因此可以使用zset来操作
zrange <key> <start> <stop>查看所有的geospatial的元素
hyperloglog
- 什么是基数?一组数据中,去重后的数据个数
- hyperloglog在内存中的存储是固定的,可计算2^64个基数,最大12KB,数据量少时使用稀疏矩阵存储,数据量过大才会转为稠密矩阵
- hyperloglog不存储数据元素本身,只进行统计,因此无法像集合set一样查看有哪些数据元素
- hyperloglog本身其实是一种算法,并非redis独有,因此这个数据类型的底层也是个算法
- 存在0.81%的标准误差,但一般不影响统计结果
- hyperloglog数据结构由Philippe Flajolet教授发明,因此redis中的命令也是由pf开头
- 应用场景:统计注册IP数、统计网页UV、统计每天在线用户数、统计每天搜索不同词条的个数
pfadd <key> <element> <element>新增元素pfcount <key>统计基数pfmerge <key> <source> <source>合并并去重,key不存在,则会新建key
bitmap
- bitmap中只有0 1两个状态,因此统计只有两个状态的场景,比如365天打卡,365天=365bit
- 位运算+位存储
setbit <key> <offset> <value>添加指定下标的bitmap数据,可以不从0开始,但其他没存的数据返回0getbit <key> <offset>查询指定下标的bitmap数据bitcount <key>求和bitmap
bitField
stream (stream = MQ消息中间件 + 阻塞队列)
redis热key功能
- config get maxmemory-policy
- config set maxmemory-policy allkeys-lfu 设置内存淘汰策略
- 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!
redis的大key功能
单个key不要超过1mb,set zset不要超过10000个 redis-cli --bigkeys
Redis的过期策略
lua
redis运行lua脚本,实际上是把整个脚本当成一个事务,让这个脚本的内容保证不可中断,不可拆分的原子性,但是当脚本内容执行出错时,无法做到要么成功,要么失败,无法保证这种原子性。因此讨论lua的原子性其实是分两种原子性的角度来分析。
Redis事务
- 事务:正常来说,事务是保证原子性的,要么全部成功,要么全部失败
- redis事务:
- 本质上是redis事务是一组redis命令,一个事务中所有的命令都会被序列化,按照一次性、顺序性、排他性执行。
- redis对于单条命令能保证原子性,但是redis事物无法保证原子性,并不支持事务回滚
- redis事物没有隔离级别,因为不会立即执行,也就不存在事务里的查询想看到事务里的更新
- 事务默认关闭
- 事务关闭,数据直接写入内存
- 事务开启,数据不会立刻执行,而是进入队列,返回队列状态,等待exec命令才会执行commands中的命令
- redis事务操作
- 事务命令
- 开启事务(
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
- 取消事务命令
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
-
异常
- 编译型异常:代码有错误、命令有错误,事务里的命令都不会被执行
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" -
监控
- 悲观锁
- 很悲观,觉得什么时候都会出问题,因此无论什么操作都会加锁
- 乐观锁
- 很乐观,觉得什么时候都不会出现问题,所以不会上锁,更新数据的时候去判断在此期间是否有人更新过这个数据
- 获取这条数据的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
获取依赖
- 通过 mvnrepository.com/ 或 package-search.jetbrains.com/ 搜索Jedis
- 获取依赖,本文是Gradle
implementation 'redis.clients:jedis:4.4.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(); } } - 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();
}
}
- 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();
}
}
}
- 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();
}
}
}
- 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
- jedis:采用直连,多个线程的话,是不安全的,如果想安全,需要使用jedis pool连接池,如果线程数量过多,那么redis-server数量就会很大,更像BIO模式
- lettuce:底层使用netty,实例可以在多个线程中共享,不存在线程不安全的问题,可以减少线程数量,更像NIO模式
- 导入redis依赖
implementation 'org.springframework.boot:spring-boot-starter-data-redis:3.1.0'implementation 'org.springframework.data:spring-data-redis:3.0.6'
- 源码分析
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;
}
}
- 设置配置
spring.redis.host=127.0.0.1
spring.redis.port=6379
- 自定义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;
- RedisUtils(基于RedisTemplate的Redis工具包,可到网上按需寻找)
Redis配置文件解析
Redis持久化
RDB (Redis Database)
- save规则满足的情况下,会产生RDB文件
- flushdb/flushall时,会产生RDB文件
- 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
优缺点
- 数据量大时,性能表现很好
- 对数据的完整性要求度不高!
- 如果在等待备份redis宕机了,会丢失数据,导致最后一次的数据丢失
- fork进程的时候,会花费一定的内存空间
- 一般来说 在生产环节要对 dump.rdb文件做一个备份
在主从复制中
在主从复制中,rdb一般作为备用文件,放在从机上
AOF (Append Only File)
AOF修复
- 修复命令:redis-check-aof --fix
<filename> - 查看差异:diff -u
rewrite
- appendonly.aof文件容量超过阈值,就会fork一个新的子线程,重写一份新的appendonly.aof文件
优缺点
- 数据完整性更高,甚至完整
- 默认同步策略:每秒同步一次,可能会丢失一秒钟的数据
- 默认开启:不开启
- AOF文件会非常的大,只拼接,不修改
- AOF效率很低
RDB / AOF 拓展
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>
其他命令
- unsubscribe channels
- punsubscribe [pattern...]
- pubsub channels [pattern] 查询订阅
- pubsub numsub [channel ...] 查看频道订阅数
- pubsub numpat 查看模式订阅数
Redis集群模式1: 主从复制
配置要求
- 最低要求:1主2从
- 一般来说:单机redis内存达到20个G,就应该立刻搭建一个新的redis集群
第一步:配置redis.conf文件
- 生成主从对应个数的redis.conf
- 修改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服务
- redis-server redisconfig/redis-m-6379.conf
- redis-cli -p 6379
- redis-server redisconfig/redis-s-6380.conf
- redis-cli -p 6380
- redis-server redisconfig/redis-s-6381.conf
- 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
第四步:永久保存从机配置需要在配置文件中配置
- replicaof
<masterip><masterport>主机IP 主机端口 - masterauth
<master-password>主机登陆密码
第五步:可以在本地看到根据配置生成的rdb文件和log文件
主从复制的细节
- 主机负责写,从机只能读。主机写完,从机会自动同步数据。从机无法进行写操作。
- 如果主机宕机了,从机依旧连接着主机可以进行读,但是不能写了。当主机再次连接上,从机依旧可以读取到主机新写的数据
- 只要主机和从机相连,一定会执行一次“全量复制”,即主从数据一致
- 如果主机断了,可以选择使用命令 slaveof no one 来使自己成为主机。其他从机需要重新slaveof
redis集群的另一种模型:级联复制
- 主机6379
- 从机6380 slaveof 6379
- 从机6381 slaveof 6380
- 此时6380,理论上即是6379的从机又是6381的主机,但使用info replicaton查看,6380仍然是从机,因此仍然无法进行写操作
- 以这种模式运行时:6379写入数据,6380、6381都能读到数据
Redis哨兵模式
- 哨兵会定时发送心跳包,判断监控的redis服务是否宕机,如果宕机则
主观下线,随后其他哨兵尝试连接,如果都连接不上,则进行选举,由一个哨兵选举出一个新的主机,并自动配置从机,而宕机的服务则称为客观下线
简答的配置单个哨兵
- 新建 sentinel.conf配置文件
- 编写命令 sentinel monitor
<master-name><redisIP><redisPort><quorum> - 例如 sentinel monitor mySentinel 127.0.0.1 6379 1
- 执行命令 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
- 当主机下线时,会自动选举从机变更为主机,并修正其他从机的订阅主机。当旧主机再次上线时不再是主机,而是新主机的从机
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
- 优缺点
- 完整的哨兵配置文件(哨兵集群则需要多个哨兵配置文件,像主从复制的配置一样)
缓存穿透及解决方案(特点:查不到)
- 缓存穿透:redis缓存中没有,数据库也没有,那么请求会一直打到数据库上,如果洪水攻击,就让数据库宕机
- 解决方案:布隆过滤器、缓存空对象(空对象影响性能,而且可能与数据库的数据不一致,即数据库有了,缓存还是空)
缓存击穿及解决方案(特点:量太大、key过期)
- 缓存击穿:在一个热key从过期到恢复的时间段中,大量的查询请求打进来,导致全部打入数据库造成的宕机
- 解决方案:设置key永不过期、分布式锁、互斥锁
缓存雪崩及解决方案(特点:缓存集体失效、redis宕机,所有缓存全部没了)
- 缓存雪崩:比如双11,热点数据大概率是放在缓存中的,当集体失效,全部打到数据库造成的宕机
- 解决方案:redis集群、限流降级、数据预热、设置不同的过期时间
Redis+Lua GPT4的回答
在地理空间信息处理场景下,Redis+Lua 能够提供高效和灵活的解决方案。Redis 是一款高性能的内存数据库,支持多种数据结构,其中地理空间数据类型(GEOSPATIAL)可用于存储地理坐标,如经纬度。结合 Lua 脚本,用户可以编写一些复杂的应用逻辑来处理这些地理空间数据,例如计算两点间的距离,查找附近的目标点等。
以下是一些典型的 Redis+Lua 地理空间应用场景:
- 地理围栏:将某一地域划定为一个区域,监控区域内的实时位置信息,例如跟踪物流货车的运输轨迹,判断是否进入某个特定区域,从而实现实时报警和监控。
- 位置搜索:通过输入关键词或者分类信息,实现附近相关地点的查询推荐。
- 路径规划:利用地理空间数据计算出两点间的最短路径或者最快路径,帮助规划行车或步行路线。
- 热力图:根据地理坐标信息,绘制出一定范围内的热力图,可用于了解人流、车流等分布情况。
总之,利用 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多路复用进行单线程阻塞式的读写。
如何解决缓存和数据库的数据一致性
- 可以考虑延迟双删策略,先删缓存,再更新数据库,再过一点时间删除缓存,避免并发造成的脏数据 2. 一般考虑稳定性和对代码的入侵性,可以采用异步删除缓存、更新缓存,使用MQ或者监控数据库的binlog
如何实现延迟消息
- zset score=过期时间(下单时间+过期时长) member=订单号,定时redis扫描 当前时间>score,取出来关单,但是在高并发的场景下,容易出现不保证幂等性的操作,例如多个消费者拿到同一个订单
- 可以使用redisson,使用其中的RDelayedQueue解决,这是基于zset实现的延迟列表,是基于内存的延迟队列