NOSQL
什么是NOSQL
NoSQL(NoSQL = not only sql),意为“不仅仅是sql”,是一项全新的数据库理念,泛指非关系型数据库
为什么需要NOSQL
用户的个人信息,社交网络,地理位置。用户自己产生的数据,用户日志等等爆发式增长!
这时候我们就需要使用nosql数据库,nosql可以很好的处理以上的情况。
NOSQL的特点
-
查询快
-
单条数据不大
-
在所有key-value类型的数据库中,因为key是唯一的,所以可以把它当做索引来看待,查询是相当快的
-
redis不适合大数据的场景,但是几百万条数据用redis还是很轻松
-
-
存储的结构没有结构化,数据之间无关系
带来的结果就是易扩展、高性能、灵活的数据模型
redis介绍
Redis(Remote Dictionary Server ),即远程字典服务
redis是用c语言
开发的一个开源的高性能键值对(key-value)非关系型数据库
,基于内存
也可持久化的数据库,相对于关系型数据库(数据主要存在硬盘中),性能高,因此我们一般用redis来做缓存使用,还可以用作数据库和消息中间件。
官方提供测试数据,50个并发执行10万个请求,读的速度是11万次/s,写的速度是81000次/s
且redis通过提供多种键值数据类型
来适应不同场景下的存储需求
redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步
。
redis能干嘛?
特性
key的定义
-
key不要太长,最好不要超过1024个字节,这不仅会消耗内存还会降低查找效率
-
key不要太短,如果太短会降低key的可读性
-
在项目中,key最好有一个统一的命名规范
redis基本知识
# redis有16个数据库,默认使用的是第0个
# 可以使用 select 切换数据库。
127.0.0.1:6379> select 2
OK
# 查看数据库大小
127.0.0.1:6379[2]> DBSIZE
(integer) 0
# 清空当前数据库
flushdb;
# 清空所有数据库的内容
flushall;
redis是单线程的!
redis是很快的,官方表示,redis是基于内存操作,cpu不是redis性能瓶颈,redis的瓶颈是根据机器的内存和网络带宽
。
redis为什么单线程还这么快?
- 误区1:高性能的服务器一定是多线程的。
- 误区2:多线程(cpu上下文会切换)一定比单线程效率高。
核心:redis是将所有的数据全部放在内存中的,所以说使用单线程去操作效率就是最高的,对于内存系统来说,如果没有上下文切换,效率就是最高的。
多次读写都是在一个cpu上的,在内存情况下,这个就是最佳的方案!
redis-benchmark 性能测试工具
在bin目录下,和redis-cli同一目录
redis基本数据类型
数据类型 | 定义 | 使用场景 |
---|---|---|
字符串(String) | Redis的基本数据类型,一个Key对应一个Value | 缓存、计数器、分布式锁等 |
哈希(Hash) | Hash是一个string类型的key和value的映射表,特别适合用于存储对象 | 用户信息、Hash表等 |
列表(List) | List是简单的字符串列表,按照插入顺序排序。可以添加一个元素到列表的头部(左边)或者尾部(右边)。相当于双向链表 | 链表、队列、微博关注人时间轴列表等 |
集合(Set) | Set是string类型的无序集合并不能重复,通过哈希表实现,添加,删除,查找的复杂度都是O(1) | 去重、赞、踩、共同好友等 |
有序集合(Zset) | zset 和 set 一样也是string类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个double类型的分数。通过分数来为集合中的成员进行从小到大的排序 | 访问量排行榜、点击量排行榜等 |
redis五种基本类型命令
string
# string
// 设置key、value
set company lx
// 获取value
# 如果key不存在,返回nil
get company
# 追加,如果当前key不存在,就相当于setkey
# lxlx
append company lx
# 获取字符串的长度
strlen company
# 截取字符串,0索引开头,0索引结尾
# 第二个值为-1代表查看完整的字符串
getrange name 0 0
# 替换指定位置开头的字符串,lxcompany
SETRANGE name 2 company
# 设置过期时间,age是key,20是过期时间,12是val
setex age 20 12
# 如果age不存在则创建,存在的情况则创建失败
setnx age 6
# 批量创建/获取
mset name zs age 12
mget name age
# 批量创建,如果key不存在则创建,否则失败。
# 是一个原子性的操作
msetnx
// 删除值,返回1代表成功,0代表失败
del company
# 对象
# json形式
set user:1{name:zs,age:5}
# 这里的key是一个巧妙的设计, user:{id}:{field}
127.0.0.1:6379> mset user:1:name zs user:1:age 5
OK
127.0.0.1:6379> mget user:1:name
1) "zs"
127.0.0.1:6379> mget user:1:age
1) "5"
# 先获取,再设置,如果不存在则返回nil
127.0.0.1:6379> getset db redis
(nil)
127.0.0.1:6379> getset db mongodb
"redis"
127.0.0.1:6379>
list
# 列表类型,list
# 双向列表,可以从头操作,也可以从尾巴操作
// 从左推三个元素(可以重复)
lpush mylist a b c b
// 从左遍历,从索引0开始,取一个值,b
lrange mylist 0 0
// 从左遍历,从索引0开始,取四个值,b c b a
// 因为是列表,存的时候,从左开始,a第一个,依次,b最后一个,
# 所以第二个b就成了左边第一个,所以取出来应该是倒序的
# -1 代表取出所有值
lrange mylist 0 -1
// 从右推
rpush mylist 1 2 3
// 从左或右弹出去一个元素,相当于删除
lpop mylist
rpop mylist
# 通过下标获得list中的某一个值
lindex mylist 0
# 返回列表的长度
llen mylist
# 移除指定个数的值
# 第一个1代表移除几个,因为元素可能是重复的
# 第二个1代表移除哪个元素,是个固定的值,例如移除 1
lrem mylist 1 1
# 截取,mylist被更改
ltrim mylist 1 2
# 从mylist弹出一个元素push到mylist2中
rpoplpush mylist mylist2
# 判断有没有该key
EXISTS mylist
# 设置1到mylist中的0索引位置,
# 这应该是个更新操作,如果该索引没有值也不能设置
# 前提是,mylist得存在,像lpush就不需要
lset mylist 0 1
# 将world插入到other的前面/后面
linsert mylist before/after world other
set
# set,无序不可重复集合
// 新增,a b c,有去重功能
sadd myset a b c b
// 遍历,不保证每次取的顺序都一样
smembers myset
// 删除
srem myset a
# 查看myset中是否包含hello
# 有返回1无返回0
sismember myset hello
# 随机取出一个元素,第二个参数可以指定取出的个数
srandmember myset
# 随机删除一个key
spop myset
# 将一个指定的值,移动到另外一个set集合
smove myset myset2 name
# 下面三个输出的都是元素
# 差集,两个集合中不同的元素
sdiff myset myset2
# 交集
sinter myset myset2
# 并集,将两个集合的元素合并
sunion myset myset2
hash
# 散列hash
// 新增 键,map
hset user01 name lisi
// 获取键对应的map中name的值,lisi
hget user01 name
# 删除key中的字段,该字段的值也就没了
hdel user01 name
# 为一个key设置多个值
hmset myhash field1 hello field2 world
# 获取一个key中的多个字段
hmget myhash field1 field2
# 获取键对应的map
hgetall user01
(1 name
(2 lisi
# key中有几个键值对
hlen myhash
# 判断hash中的字段是否存在
hexists myhash field
# 只获得所有的key/value
hkeys myhash
hvals myhash
# 自增
hincreby myhash field 1
# 如果不存在则可以设置,否则不能设置
hsetnx myhash field hello
zset
# zset,有序集合
# 在set的基础上, 增加了一个值
# 添加,修改
# 1代表分数,或者说排序,这个排序也是按score来的
zadd myzset 1 mydata 2 mydata2 3 mydata3
# 升序查询
zrange myzset 0 -1
# 降序查询,并输出值
ZREVRANGE mysortset 0 -1 withscores
# 根据score查询,-inf和+inf代表负无穷和正无穷
ZRANGEBYSCORE myzset -inf +inf withscores
# 同样是按score来的
ZRANGEBYSCORE myzset -inf 2
# 删除mydata
zrem myzset mydata
# 查看zset元素个数
ZCARD myzset
# 获取 值在3-5中间的值一共有多少个
ZCOUNT myzset 3 5
key
// 查询所有键
keys *
// 模糊查询键,?代表一个,*代表0或多
keys ?name
keys *name
// 查询key是什么类型,
type myset
//查询names键是否存在,返回0,1
exists names
# 删除key 1是代表肯定的意思
move key 1;
其他
// 1
incr num
// 2
incr num
// 1
decr num
// 4 自增设置步数为3
incrby num 3
// 1 自减设置步数
decrby num 3
# 该key5s后自动删除,单位是s
expire key 5
# 查询该key的过期时间
ttl key
# 返回key集合中元素的数量
scard key
进阶命令
# ex 过期时间
# nx not exist 加nx,当key不存在才设置,
# 分布式锁就是set带了nx命令来做的,它是一个原子性的命令
# SET key value [EX seconds] [PX milliseconds] [NX|XX]
三种特殊数据类型
Bitmaps,Hyperloglogs 和地理空间(Geospatial)索引半径查询
Geospatial 地理位置
朋友的定位,附近的人,打车距离计算?
这个计算的是直线距离
redis的Geo在redis3.2版本就推出了!这个功能可以推算地理位置的信息,两地之间的距离,方圆几里的人。
# 有效的经度 -180~180
# 有效的维度 -85-85
# 超出范围后,则会返回错误
# 添加地理位置
geoadd bj:region 116.36003 39.9305 xicheng
# 获取经纬度
127.0.0.1:6379> GEOPOS bj:region xicheng fengtai
1) 1) "116.36002868413925171"
2) "39.93050039138125129"
2) 1) "116.28625184297561646"
2) "39.85849910212906622"
# 西城到丰台的距离 单位有 m km
127.0.0.1:6379> geodist bj:region xicheng fengtai km
"10.1869"
# 显示该经纬度下,方圆10km的区
127.0.0.1:6379> GEORADIUS bj:region 116.41005 39.93157 10 km
1) "xicheng"
# 距离丰台直线5km距离的区
127.0.0.1:6379> GEORADIUSBYMEMBER bj:region fengtai 5 km
1) "fengtai"
# 以给定的经度、纬度为中心,找出某一半径内的元素
# withdist 显示直线距离
# withcoord 显示经纬度
# count 1 只取一个元素
127.0.0.1:6379> GEORADIUS bj:region 116.41005 39.93157 10 km
1) "xicheng"
# geo底层的实现原理就是zset,所以zset命令可以操作
ZRANGE bj:region 0 -1
ZREM bj:region fengtai
hyperloglog 基数(不重复的元素)
先说明一下,HyperLogLog是一种算法,并不是由redis创造了它。
Redis 在 2.8.9 版本添加了 HyperLogLog 结构(简介HLL),用于做基数统计,其使用算法HyperLogLog使得在数量级特别大的情况下占用空间很小。说白了就是在大数据量级的情况下能够在很小的空间中进行元素去重统计。如果使用我们平常的数据结构比如set,HashMap,等,虽然也可以实现去重统计的工作,但是当数据量上升到一定级别之后,其占用的空间也是非常的大。
需要注意的是HyperLogLog算法的去重计数方案并不精确,当然不是特别不精确,标准误差只有0.81%。
所以如果允许容错就可以使用,否则就不行。
当然HyperLogLog虽说占据空间小,但也不是不占空间,它需要占据一定12k存储空间,所以如果我们的统计量可能比较小,使用HyperLogLog可能就是大材小用了,但是如果百万级、千万级,那节省的空间就大的大了去了。
基数:我理解就是一个数据集中不重复的元素个数。
127.0.0.1:6379> PFADD mypf2 1 2 3 4 5 6 6 2 2
(integer) 1
127.0.0.1:6379> PFCOUNT mypf2
(integer) 6
# 将mypf 和 mypf2 合并到mypf3中
PFMERGE mypf3 mypf mypf2
bitmaps 位存储(0 1)
统计疫情感染人数:0 1 0 1 0 0
统计用户信息,活跃/不活跃,登录/未登录,打卡/未打卡
两个状态的,都可以使用bitmaps
bitmaps 位图,数据结构!都是操作二进制来进行记录,就只有0/1两个状态。
365天 = 365bit 1字节=8bit 46个字节左右!
# 举例:记录周一到周日的打卡
127.0.0.1:6379> SETBIT sign 0 0
(integer) 0
127.0.0.1:6379> SETBIT sign 1 0
(integer) 0
127.0.0.1:6379> SETBIT sign 2 1
GETBIT sign 2
# 统计这周的打卡记录总数,查看是否有全勤
BITCOUNT sign
redis事务
redis单条命令是保证原子性的,redis的事务是不保证原子性的。
redis事务也没有隔离级别的概念!
Redis是有事务的,redis事务的本质是一组命令的集合
,这组命令要么都执行,要不都不执行,而且一个事务中的所有命令都会被序列化,在事务执行的过程中,会按照顺序执行。
redis事务的实现,需要用到MULTI(事务的开始)和EXEC(事务的结束)命令 ;
有事务,但是不能回滚,redis为了快不支持事务回滚。
127.0.0.1:6379> set k1 k1
QUEUED
127.0.0.1:6379> set k2 k2
QUEUED
127.0.0.1:6379> get k1
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK
3) "k1"
# 放弃事务
# 在java中的应用可以 try catch中,
discard
# 开启事务后出现异常
# 1. 运行时异常,输入的命令没有问题,运行的时候报错了(1/0),
# 其他命令是可以正常执行的,错误命令抛出异常
# 所以保证不了原子性
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 k1
QUEUED
127.0.0.1:6379> INCR k1
QUEUED
127.0.0.1:6379> exec
1) OK
2) (error) ERR value is not an integer or out of range
127.0.0.1:6379> get k1
"k1"
# 2. 编译型异常
# 命令就敲错了,执行事务的时候就一条命令都不会执行了
redis实现乐观锁
# 单线程
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> INCRBY out 20
QUEUED
127.0.0.1:6379> DECRBY money 20
QUEUED
127.0.0.1:6379> exec
1) (integer) 20
2) (integer) 80
# 在多线程的情况下,使用watch可以当作redis的乐观锁操作
# 第一个线程修改数据后先不提交事务,第二个线程修改后提交事务,
# 此时第一个线程提交事务就会失败
# 取消监视
unwatch
jedis
是官方推荐的java连接开发工具!使用java操作redis!
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>redis-study</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.60</version>
</dependency>
</dependencies>
</project>
package awu;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
public class JedisDemo {
public static void main(String[] args) {
// jedis命令和redis原生命令一模一样
// 1. 创建数据库对象
// Jedis jedis = new Jedis("192.168.66.3", 6379);
//
// jedis.set("xiyou","孙悟空");
//
// String xiyou = jedis.get("xiyou");
//
// System.out.println(xiyou);
//
// jedis.close();
// 使用数据库连接池 完成 jedis对象的获取
// 1. 创建连接池配置对象
JedisPoolConfig config = new JedisPoolConfig();
// 2. 配置参数
// 最大连接数
config.setMaxTotal(50);
// 最大空闲个数
config.setMaxIdle(30);
// 3. 创建连接池对象
JedisPool jedisPool = new JedisPool(config,"localhost",6379);
// 4. 连接池 获取连接对象
Jedis jedis = jedisPool.getResource();
jedis.get("xiyou");
// 5. 将对象归还给连接池
jedis.close();
}
}
redisUtils
一般企业级开发,都会封装一下redis
package com.aihangxunxi.scheduler.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.jedis.JedisClientConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import redis.clients.jedis.JedisPoolConfig;
import java.time.Duration;
public class RedisUtil {
// @Value("${spring.redis.host:192.168.100.223}")
private String redisHost = "192.168.100.240";
// @Value("${spring.redis.port:6379}")
private int redisPort = 6379;
private static final Logger logger = LoggerFactory.getLogger(RedisUtil.class);
public JedisPoolConfig getRedisConfig() {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxIdle(8);
config.setMinIdle(0);
return config;
}
public JedisConnectionFactory jedisConnectionFactory() {
JedisPoolConfig config = getRedisConfig();
JedisConnectionFactory factory = new JedisConnectionFactory(config);
factory.setUsePool(true);
factory.setHostName(redisHost);
factory.setPort(redisPort);
return factory;
}
public StringRedisTemplate getStringRedisTemplate(int db) {
RedisConnectionFactory factory = connectionFactory(db);
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(factory);
template.afterPropertiesSet();
return template;
}
public RedisConnectionFactory connectionFactory(int db) {
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxIdle(5);
poolConfig.setMaxTotal(500);
poolConfig.setMaxWaitMillis(1000 * 100);
poolConfig.setTestOnBorrow(true);
poolConfig.setTestOnReturn(false);
poolConfig.setTestWhileIdle(true);
JedisClientConfiguration clientConfig = JedisClientConfiguration.builder().usePooling().poolConfig(poolConfig)
.and().readTimeout(Duration.ofMillis(6000)).build();
// 单点redis
RedisStandaloneConfiguration redisConfig = new RedisStandaloneConfiguration();
// logger.info("===========================" + redisHost +
// "=======================");
redisConfig.setHostName(redisHost);
// logger.info("===========================" + redisPort +
// "=======================");
redisConfig.setPort(redisPort);
redisConfig.setDatabase(db);
return new JedisConnectionFactory(redisConfig, clientConfig);
}
}
部分知识引用自https://blog.csdn.net/xianlvfan2224/article/details/102722298
https://blog.csdn.net/qq_35190492/article/details/102841400
https://juejin.cn/post/7031916434972213285
https://www.bilibili.com/video/BV1S54y1R7SB?p=7&spm_id_from=pageDriver&vd_source=64c73c596c59837e620fed47fa27ada7
https://www.jianshu.com/p/00b25ec577a1
https://blog.csdn.net/qq_44377709/article/details/117388620
老友老酒老地方,这是一种对自己过往肯定的一种做法,因为没有老友老酒老地方,其实很难肯定自己已经走过的路。
圆桌新春派第2期