1. 概述
首先提一下下NoSQL的概念,应读做“嗯欧SQL”,而不是“肉SQL”,等价于Not Only SQL。非关系型数据库(NoSQL)的产生于WEB2.0,传统的web1.0是不可以进行互动的,到了web2.0是可以进行互动的,比如新浪微博。。。传统关系型数据库在应对web2.0(尤其是超大规模和高并发的SNS类型)暴露出很大的问题,比如
- 高并发读写问题;
- 海量数据的高效率存储和访问;
- 高可扩展性和高可用性;
常见的NOSQL有redis、mongoDB等,NoSQL所具备的优势有:
- 易扩展性;
- 灵活的数据模型;
- 大数据量,高性能;
- 高可用;
NoSQL数据库的分类主要有四类:
- 键值存储(优点:快速查询;劣势:数据缺少结构化;如redis);
- 列存储(优点:查找快,可扩展性强;劣势:功能局限。如HBase);
- 文档数据库(优点:数据结构的要求不是特别严格;劣势:查询的速度不高,缺少统一的语法;如mongoDB、CounchDB);
- 图形数据库(优点:利用图做数据结构;劣势:需要对图做计算,不容易作分路式集群方案;如InfoGrid);
2. redis常用操作
位置/user/local
,启动进入redis的目录中操作即可。redis所支持的数据类型有:redis是高性能键值对数据库,支持的数据类型有字符串(String
)、字符串列表(list
)、有序集合(sorted set
)、哈希(hash
)、字符串集合(set
)。redis的应用场景主要有:缓存、任务队列、应用排行榜、网站统计访问、数据过期处理、分布式集群架构中的session分离。一些命令行操作如下:
2.1 通用操作
- 获取所有的 key –
keys *
; - 获取所有以 my 开头的 key –
keys my?
(匹配单个字符)/keys my*
(匹配多个字符); - 删除多个 key –
del key1 key2 key3
(删除key值为key1、key2…的对象); - 判断某个 key 是否存在(存在返回1,不存在返回0) –
exists key1
(判断 key 为 key1 的对象是否存在); - 获取指定 key –
get key1
(获取 key 为 key1 的对象); - 重命名指定的 key –
rename oldKey newKey
(将 key 为 oldKey 重命名为 newKey); - 设置过期时间(单位为秒)--
expire key1 5
(将 key 为 key1 的对象设置过期时间为5秒); - 查看所剩的生命时长(相对设置的生命周期而言,若没有设置过期时间返回-1)--
ttl key1
(查看 key 为 key1 对象的剩余生命时间); - 查看指定 key 的类型 –
type key1
(查看 key 为 key1 的对象的类型);
2.2 string类型的操作
- 赋值 –
set key1 value1
; - 取值 –
get key1
; - 获取 key1 的旧值,同时将 key1 置成新值(输出原始值,然后重新赋值) –
getset key1
; - 删除(删除后在获取输出 nil,表示删除不存在了) –
del key1
; - 自增1(将 key 对应的 value 自增1并将结果返回,如果 key 不存在则自动创建该 key 默认为0,自增1变为1,如果 key 对应的 value 不能转成整型将报错) –
incr key1
; - 自减1(同上)--
decr key1
; - 指定步长进行自加并返回(同上)--
incrby key1 increment1
(即让 key1 对象转成整型并加上 increment1 之后返回); - 指定步长进行自减并返回(同上)--
decrby key1 decrement1
;
2.3 hash类型
hash
类型:string 类型的 key 和 value 的容器,适合存放对对象。通常会将对象序列化为 JSONString 再存储进来。
- 单个赋值 –
hset hash1 property propertyValue
(存储一个hash1对象,这个对象中有个 property-propertyValue 属性); - 单个取值 –
hget hash1 property
(获取 hash1 中 property 属性的值); - 多个赋值 –
hmset hash1 property1 value1 property2 value2......
(存储 hash1 对象,该对象中有多个key-value属性 ); - 多个取值 –
hmget hash1 property1 property2......
(获取 hash1 中多个 key 值); - 获取该
hash
的所有属性 –hgetall hash1
(获取 hash1 中的所有键值对,包含key值,而hget
和hmget
只有value); - 删除某一属性 –
hdel hash1 property1
; - 直接删除某个
hash
–del hash1
; - 对
hash
中某一属性自增/自减 –hincrby/hdecrby hash1 property1 increment/decrement
; - 判断该hash中是存在某个属性(存在返回1,不存在返回0) –
hexists hash1 property1
; - 返回该
hash
中属性的个数 –hlen hash1
; - 返回该
hash
中所有属性名 –hkeys hash1
; - 返回该
hash
中所有属性值 –hvals hash1
;
2.4 list类型
- 从左/右侧插入元素(开始list默认为空) –
lpush/rpush list1 value1 value2....
; - 从左/右弹出第一个元素 –
lpop/rpop list1
; - 查看list中元素(负数代表倒数第几个) –
lrange list1 startIndex endIndex
,比如0到-1表示查看整个list的元素; - 从头到尾删除m个指定元素n –
lrem list1 m n
(当m为0时,表示删除所有的n); - 修改下标为index的值为newValue –
lset list1 index newVlaue
; - 在指定元素前/后插入元素 –
linsert list1 before/after 指定元素 待插入元素
(若指定元素在 list 中有多个,插入行为只会发生在第一个指定元素的前后); - 将list1的尾部元素返回并将其移动到list2的头部 –
rpoplpush list1 list2
;
注:其实我实操下来,这里的list
数据类型更像是一种双向队列,每次插入元素之前的元素索引都要向后挪一位,以lpush
操作为例,依次插入1、2、3,实际是 3->2->1 这样的形式,此时如果lpop
弹出的是3而不是1。默认也是3是头,1是尾。
2.5 set类型(无序,不重复)
- 添加元素 –
sadd set1 value1 value2....
; - 删除元素 –
srem set1 value1 value2 .....
; - 求差异(找出第一个
set
和第二个的不同,只显示第一个set中有但第二个set中没有元素) –sdiff set1 set2
;
3.2 将相差的元素存到另一个set3中 –sdiffstore set3 set1 set2
; - 求交集 –
sinter set1 set2
;
4.2 将交集的元素存到另一个set3中 –sinterstore set3 set1 set2
; - 求并集 –
sunion set1 set2
;
5.2 将并集的元素存到另一个set3中 –sunionstore set3 set1 set2
; - 查看
set
中的元素 –smembers set1"
; - 获取
set
中元素的数量 –scard set1"
; - 随意获取
set
中的一个元素 –srandmember set1"
;
2.6 sortedset类型
有序的不重复,元素唯一但分数可重复,默认从小到大排序;
- 添加元素 –
zadd sset1 score1 key1 score2 key2.....
,如果增加已存在的元素,那么该元素的分数将会是最新的分数,注意是先分数再元素,不能搞反了,比如zadd sset2 1.0 key1 2.0 key2
; - 获取
sset
中某个元素的分数 –zscore sset1 key1
,查看 sset 中 key1 元素的分数; - 获取
sset
中元素的个数(分数和元素是一个整体)--zcard sset1
; - 删除元素 –
zrem sset1 key1
; - 查找指定索引区间内的元素 –
zrange sset1 startIndex endIndex
可以追加withscores
将对应的分数也返回; - 从大到小(按分数)排序 –
zrevrange sset1 0 -1
,带上withscores
可返回分数; - 删除指定排名范围的元素 –
zremrangebyrank sset1 开始排名(第一名是0) 末位排名
; - 删除指定分数范围内的元素 –
zremrangebyscore sset1 lowScore highScore
; - 返回指定分数范围内的元素 –
zrangebyscore sset1 lowScore highScore
,当然也可以带上withscores
,也可以带上limit startIndex endIndex
(限制返回其中的前几名); - 给指定元素增加分数 –
zincrby sset1 increment key1
; - 返回指定分数范围内元素的个数 –
zcount sset1 lowScore highScore
;
3. redis的特性
3.1 多数据库
一个redis中可以包含多个数据库,就像mysql中可以创建多个数据库,用户进行链接时可以指定链接具体的某一个数据据库。一个redis实例最多提供16个数据库,下标分别为0到15。客户端默认连接的是0号数据库,当然也可以用select关键字来选择连接哪一个数据库:如选择1号数据库 – select 1
即可。将当前数据中的某个key移动到另一个数据库可以用move key 目标数据索引
,如将当前数据库中的key1移动到0号数据中:move key1 0
。
3.2 Redis事务
redis中也提供了事务的特性(multi、exec、discard),在redis中所有事务命令将会被放进队列中顺序串行化执行,并且在执行事务操作期间,redis将不会再为任何客户端提供服务,流程如下:
- multi后面所有的语句都会被当作一个事务按序执行,相当于开启事务;
- exec相当于提交事务;
- discard相当于回滚事务;
3.3 redis的持久化
redis的高性能源于他的所有数据都存放在内存中,为了数据不丢失,就需要将这些数据从内存中转存到本地硬盘上,这就是持久化,redis的持久化有两种方式:RDB方式和AOF方式。
3.3.1 RDB持久化
RDB是默认支持的,不需要配置,在指定的时间间隔内将内存中数据集快照写入到磁盘。RDB持久化的优势:
- 采用RDB持久化方式后,整个redis数据库将只包含一个文件,对于文件备份非常方便;
- 对于灾难恢复而言,RDB是不错的选择,可以将单独的文件压缩后转移到其他介质上;
- 性能最大化,在开始持久化时,redis需要做的仅仅是分叉出一些子进程去完成这些持久化的操作,极大的避免了服务器进程进行I/O操作。如果数据集很大,相对AOF,RDB的启动效率更高;
劣势:如果想保证数据的高可用性(最大限度避免数据丢失),那RDB不是一个很好的选择,因为RDB在定时持久化的时候不可避免的会出现宕机的情况,宕机前的数据就会丢失。RDB是利用子进程来协助完成持久化,所以当数据集非常大的时候,可能会导致整个服务器停止几百毫秒甚至1秒。
配置:在redis.conf文件中
这里的save 900 1表示每900秒至少有一个key发生变化时,redis就会持久化一次,即dump一次快照到磁盘上,下面的同理;dump的文件名
dump文件的路径,
即当前路径。
3.3.2 AOF持久化
AOP是以日志的形式记录服务器的每一次操作,在redis启动时,就会读取该文件重新构建数据库,以保证数据库的完整。
优势:AOF可以带来更高的数据安全性,redis提供了三种同步策略:每秒同步(异步,高效,但一旦宕机,这1s秒修改的数据就会丢失)、每修改同步(同步持久化,最安全但效率最低)、不同步,对于日志的修改是append形式(追加的形式),即使出现宕机,也不会破坏之前以保存的日志,如果日志过大,redis可以启动自动重写机制。AOF有一个非常清晰的日志模式。
劣势:往往AOF的文件要比RDB文件大一些,而且运行效率比RDB低(因为每修改同步);
配置:同样在redis.conf文件中,开启追加的模式将appendonly no改为yes,下面也标明了日志文件是appendonly.aof,同步策略如下
其中always表示是每修改同步(最常用),everysec则表示每秒同步,no表示不同步。使用AOF的案列:将appendonly no改为appendonly yes,使用appendsync always(每修改同步)策略,保存退出,然后在redis中随便写一些数据,最后执行flushall(清空数据库数据),再断开redis,然后进入日志文件appendonly.aof将最后的flushall命令删除重启redis,redis数据即可恢复
3.3 无持久化
直接禁用redis的持久化机制,就是用来做缓存。
3.4 RDB和AOF结合
4. java对Redis的操作
下载jedis的jar包,Linux必须在配置开放对应的6379端口,redis的配置文件中要需要将绑定的ip改成0.0.0.0,不能为127.0.0.1,否则将会出现ping通,redis却链接超时的情况(redis后台启动必须带上配置文件,否则将是前台启动),代码如下
import org.junit.Test;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
public class JedisDemo1 {
/*
* 单个链接的方式
*/
@Test
public void demo1() {
//设置IP地址和端口
Jedis jedis = new Jedis("10.4.62.6",6379);
//保存数据
jedis.set("name", "Hello Redis!");
//获取值
String value = jedis.get("name");
jedis.del("name");
System.out.println(value);
//释放资源
jedis.close();
}
/*
* 使用连接池链接
*/
@Test
public void demo2() {
//创建连接池的配置对象
JedisPoolConfig config = new JedisPoolConfig();
//设置最大连接数
config.setMaxTotal(30);
//设置最大空闲连接数
config.setMaxIdle(10);
//获得连接池
JedisPool jedisPool = new JedisPool(config, "10.4.62.6", 6379);
//获取和新对象
Jedis jedis = null;
try {
//通过连接池获得jedis
jedis = jedisPool.getResource();
jedis.set("jedisPool", "利用jedisPool设置的值");
System.out.println(jedis.get("jedisPool"));
} catch(Exception e) {
e.printStackTrace();
} finally {
if(jedis!=null) {
jedis.close();
}
if(jedisPool!=null) {
jedisPool.close();
}
}
}
}
5. 关于redis的问题排查
redis连接占满,服务端无法连接启动不了
# 1. 查看客户端连接概况
info clients
# 样例输出
connected_clients:730
client_longest_output_list:0
client_biggest_input_buf:0
blocked_clients:15
# 2. 查看所有客户端和连接状态
client list
# 样例输出,age表示连接存在的时间(单位秒),idle表示连接空闲的时间(单位秒)
id=799726 addr=10.129.60.202:43568 fd=43 name= age=80530 idle=80530 flags=b db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=brpoplpush
# 3. 查看客户端超时时间
config get timeout
# 样例输出,0表示不开启空闲清除
1) "timeout"
2) "0"
# 4. 设置空闲清理时间
config set timeout 600
# 5. 查看redis服务端最大允许的连接数
config get maxclients
6. redis 特殊场景的示例
6.1 使用 jedis 批量插入的场景
很多时候,使用List
数据结构时,需要批量插入然后读取。这里面的批量插入可以利用 redis 的管道来操作,它是原子操作,示例:
Jedis jedis = getJedis();
Pipeline pipeline = jedis.pipelined();
try {
for (T d : dataList) {
pipeline.rpush(getKey(key), HessianUtils.encode(d));
}
pipeline.sync();
} finally {
close(jedis);
}
6.2 List数据结构按序写入和读取
再有,就是写入的方向和读取的方向,大部分场景都是期望按序写入、按序读取,写入使用rpush
,读使用lrange
即可完成顺序写入、读取的行为。
6.3 List数据结构的分页读取
可以这样来封装(pageNo
从1开始):
public <T> List<T> getList(String cacheKey, Class<T> clazz, int pageNo, int pageSize) {
int startIndex = (pageNo - 1) * pageSize;
return jedisSentinelPoolCacheManager.listRangeWithJsonSource(cacheKey, startIndex, startIndex + pageSize - 1,
clazz);
}
6.4 Jedis 客户端设置过期时间
jedis 设置过期时间有个 jedis.expireAt(key, timestemp)
方法,这里的时间戳是指的 unix 时间戳,代码中需要将当前时间的时间戳除以1000才是正确的设置,如date.getTime()/1000
。