Redis入门
1、No SQL概述
全称为Not Only SQL,不仅仅是SQL,即非关系型数据库。是对不同于传统的关系型数据库的数据库管理系统的统称。NoSQL用于超大规模数据的存储。(例如谷歌或Facebook每天为他们的用户收集万亿比特的数据)。这些类型的数据存储不需要固定的模式,无需多余操作就可以横向扩展。
NoSQL是一项全新的数据库革命性运动,早期就有人提出,发展至2009年趋势越发高涨。NoSQL的拥护者们提倡运用非关系型的数据存储,相对于铺天盖地的关系型数据库运用,这一概念无疑是一种全新的思维的注入。
1.1、使用No SQL的理由
今天我们可以通过第三方平台(如:Google,Facebook等)可以很容易的访问和抓取数据。用户的个人信息,社交网络,地理位置,用户生成的数据和用户操作日志已经成倍的增加。我们如果要对这些用户数据进行挖掘,那SQL数据库已经不适合这些应用了, NoSQL 数据库的发展却能很好的处理这些大的数据。主要从以下三个方面来概述:
- High performance:高并发读写。
- Huge Storage:海量数据的高效率存储和访问。
- High Scalability&&High Availability:高扩展性和高可用性。
1.2、No SQL的优缺点
优点:
- 高可扩展性。
- 分布式计算。
- 低成本。
- 架构的灵活性,半结构化数据。
- 没有复杂的关系。
缺点:
- 没有标准化。
- 有限的查询功能(到目前为止)。
- 最终一致是不直观的程序。
1.3、No SQL和RDBMS的对比
传统的RDBMS特点:
- 高度组织化结构化数据。
- 结构化查询语言(SQL)。
- 数据和关系都存储在单独的表中。
- 数据操纵语言,数据定义语言。
- 严格的一致性。
- 基础事务。
No SQL特点:
- 代表着不仅仅是SQL。
- 没有声明性查询语言。
- 没有预定义的模式。
- 键 - 值对存储,列存储,文档存储,图形数据库。
- 最终一致性,而非ACID属性。
- 非结构化和不可预知的数据。
- CAP定理。
- 高性能,高可用性和可伸缩性。
1.4、主流NoSQL产品
- Redis
- mongoDB
- riak
- CouchDB
1.5、NoSQL数据库分类
说明:
- 键值(Key-Value)存储:优点是快速查询,缺点是存储的数据缺少结构化。
- 列存储:优点是查找速度快,可扩展性强,更容易进行分布式扩展。缺点是功能相对局限。
- 文档数据库:优点是数据结构要求不严格,缺点是查询性能不高,而且缺乏统一的查询语法。
- 图形数据库:优点是利用图结构相关算法。缺点是需要对整个图做计算才能得出结果,不容易做分布式的集群方案。
2、Redis使用
Redis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。从2010年3月15日起,Redis的开发工作由VMware主持。
Redis支持的数据类型如下:
- 字符串类型(string)。
- 散列类型(hash)。
- 列表类型(list)。
- 集合类型(set)。
- 有序集合类型(zset)。
2.1、Redis使用场景
- 用作缓存。
- 任务队列。
- 应用排行榜。
- 网站访问统计。
- 数据过期处理。
- 分布式集群架构中的session分离。
2.2、Redis下载安装
去Redis官网下载合适版本,解压。
修改查看配置文件redis.windows.conf:
绑定到本机ip地址。
Redis默认的服务端口是6379。
一共有16个默认的数据库,从0号开始到15。
设置reids的最大内存容量。
设置连接密码,redis默认是无密码的,可以自行设置。
目录下运行命令redis-server.exe redis.windows.conf
进行安装。
测试是否安装成功:
运行命令:redis-cli.exe
,然后命令auth 密码
,输出OK就是安装成功了。
最后需要将redis加到windows服务中:
运行命令:
redis-server.exe --service-install redis.windows.conf --loglevel verbose
查看Windows服务:
多了一个Redis服务那么就是添加成功了。
2.3、Redis数据结构
对于key的定义,需要注意:不要过长,也不要过短,使用统一的命名规范。
对于value,Redis支持5种数据结构,如下:
- 字符串(String)。
- 哈希(hash)。
- 字符串列表(list)。
- 字符串集合(set)。
- 有序字符串集合(zset)。
2.3.1、string
二进制安全的,存入和获取的数据相同。Value最多可以容纳的数据长度是512M。
存取string的常用命令:
- set key value:存键值对。
- get key:按键取值。
- del key:删除键值对。
- incr key:将key指定的value值递增1,如果数据库中不存在这样的key,则会默认生成一个键值对,并将其value值设置为1。
- incrby key n:将key指定的value值增加n。
- decr key:将key指定的value值递减1,如果数据库中不存在这样的key,则会默认生成一个键值对,并将其value值设置为-1。
- decrby key n:将key指定的value值减少n。
- append key value:将指定的值追加到key对应的value后面,属于字符串拼接。
测试:
给同一个key多次赋值的话,会覆盖掉前面的value:
2.3.2、hash
String Key和String Value的map容器。每一个Hash可以存储4294967295个键值对。
存取hash的常用命令:
- hset hash名称 key value:存储键值对到hash容器中,这种方式只能存储一个键值对。
- hmset hash名称 key value key value …:存储键值对到hash容器中,这种方式可以同时存储多个键值对。
- hget hash名称 key:从hash中取出指定key对应的值。
- hmget hash名称 key key …:从hash中取出多个key对应的值。
- hgetall hash名称:从hash中获取所有key和value值。
- hdel hash名称 key key …:删除hash中的指定键值对,可以删除单个,也可以同时删除多个。
- del hash名称:删除hash中的所有键值对。
- hincrby hash名称 key n:给hash中key对应的值增加n,n可以为负数。
- hexists hash名称 key:判定hash中key键是否存在,存在返回1,不存在返回0。
- hlen hash名称:返回hash中的字段长度(多少个键值对)。
- hkeys hash名称:获取hash中的所有key。
- hvals hash名称:获取hash中的所有value。
测试:
2.3.3、list
ArrayList使用数组方式,LinkedList使用双向链接方式,双向链表中增加数据、删除数据。
存取list的常用命令:
- lpush list名称 value value value…:向list中左侧添加值。
- rpush list名称 value value value…:向list中右侧添加值。
- lrange list名称 start end:查看list列表,索引可为负值,但不能越界。
- lpop list名称:弹出左侧第一个元素,即移除左侧第一个元素。
- rpop list名称:弹出右侧第一个元素,即移除右侧第一个元素。
- llen list名称:获取list中的元素个数。
- lpushx list名称 value:向list最左侧插入一个元素。
- rpushx list名称 value:向list最右侧插入一个元素。
- lrem list名称 count value:从list中左侧开始删除count个value,count<0时从右侧开始删除,count>0时从左侧开始删除,count=0时,删除所有value值。
- lset list名称 index value:将list中索引为index的值替换为value值。
- linsert list名称 before value value1:在list中第一个value值之前插入value1值。
- linsert list名称 after value value1:在list中第一个value值之后插入value1值。
- rpoplpush list名称 list2名称:将list中最右侧元素弹出,然后压入到list2最左侧。多用于消息发布系统。未达到条件时,在原有队列中继续循环,直到满足条件,在达到条件时才会弹栈。
测试:
2.3.4、set
和List类型不同的是,Set集合中不允许出现重复的元素。Set可包含的最大元素数量是4294967295。
存取set的常用命令:
- sadd set名称 value1 value2…:向set中添加元素。
- srem set名称 value1 value2…:从set中移除指定元素。
- sismember set名称 value:判断set中是否存在元素value,存在返回1,不存在返回0。
- smembers set名称:获取set中的所有元素。
- scard set:获取set集合的元素个数。
- srandmember set [count]:随机返回set集合中的任意单个或count个元素。
- sdiff set1 set2:获取set1和set2两集合的差集。
- sinter set1 set2:获取set1和set2两集合的交集。
- sunion set1 set2:获取set1和set2两集合的并集。
- sdiffstore set1 set2 set3:将set2中和set3相差的元素添加到set1中,会覆盖原来集合中的元素。
- sinterstore set1 set2 set3:将set2和set3的交集添加到set1中,,会覆盖原来集合中的元素。
- sunionstore set1 set2 set3:将set2和set3的并集添加到set1中,会覆盖原来集合中的元素。
测试:
可以看到set是无序的,元素并不是按照添加顺序存储的。
存储set的使用场景:
- 跟踪一些唯一性数据。
- 用于维护数据对象之间的关联关系。
2.3.5、zset
zset中的成员在集合中的位置是有序的,对比之下,set成员在集合中是无序的,注意区别。
存取zset的常用命令:
- zadd set名称 score value score value…:按照指定分数添加元素,分数用于排序。
- zscore set名称 value:获取某个指定值对应的score分数。
- zcard set名称:获取集合中的元素个数。
- zrange set名称 0 -1:获取所有元素,不带分数,按分数升序排列。
- zrevrange set名称 0 -1:获取所有元素,不带分数,按分数降序排列。
- zrange set名称 0 -1 withscores:获取所有元素,带分数,升序排列。
- zrevrange set名称 0 -1 withscores:获取所有元素,带分数,降序排列。
- zrem set名称 value:删除set中的指定value元素。
- zremrangebyrank set名称 index1 index2:删除index1-index2索引范围内的所有元素。
- zremrangebyscore set名称 score1 score2:删除score1-score2分数范围内的所有元素。
- zrangebyscore set名称 score1 score2:获取score1-score2分数范围内所有元素,不带分数。
- zrangebyscore set名称 score1 score2 withscores:获取score1-score2分数范围内所有元素,带分数。
- zrangebyscore set名称 score1 score2 limit index count:获取指定分数范围内的元素,从索引index开始,只显示count个。
- zincrby set名称 int value:给指定元素value增加分数int,int可以为负数。
- zcount set名称 score1 score2:统计score1-score2分数范围内的元素个数。
测试:
sorted-set使用场景:
- 大型在线游戏积分排行榜。
- 构建索引数据。
2.4、Redis中key的通用操作
- keys * :获取该数据库中所有的key。
- keys my*:获取该数据库中所有以my开头的key。
- keys my??:获取该数据库中所有以my开头,并且后面是两个字符的key。
- del key1 key2…:删除指定的单个或多个key。
- exists key:判断某个key是否存在,存在返回1,不存在返回0。
- rename key key2:重命名key为key2。
- expire key long:为指定key对应的键值对设置过期时间,超过这个时间后会自动删除。
- ttl key:获取某个key对应的键值对所剩余的过期时间。
- type key:获取指定key对应value的数据类型。
测试:
先添加几个key。
2.5、Redis的特性
- 多数据库:一个Redis实例最多可提供16个数据库,下标从0到15,客户端默认连接的是0数据库。可以通过
select index
选择连接的数据库,index范围[0,15],注意索引不要越界。move myset index
将myset这个key移动到index数据库。 - 事务:
multi
开启事务,exec
提交,discard
回滚。具备一般事务特性。
Redis中事务相关的命令如下:
Redis事务可以一次执行多个命令, 并且带有以下三个重要的保证:
- 批量操作在发送EXEC命令前被放入队列缓存。
- 收到EXEC命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行。
- 在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中。
事务的开始到执行经历以下三个阶段:
- 开始事务。
- 命令入队。
- 执行事务。
测试:
multi来开启事务,exec执行事务,multi到exec之间可以有多条命令,可以看到每条命令都加入到了队列QUEUED中,如果有错误,会自动discard回滚到错误之前并结束事务。
2.6、Redis持久化
Redis中提供两种持久化的方式:RDB持久化和AOF持久化。
2.6.1、RDB
RDB全称为Redis DataBase,就是在不同的时间点,将redis存储的数据生成快照并存储到磁盘等介质上。
默认支持的,无需配置, 在指定的时间间隔内将内存中的数据集快照写入磁盘, 也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里。Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到 一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。 整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。
优点:
- 适合大规模的数据恢复。
- 对数据完整性和一致性要求不高。
缺点:
- 在一定间隔时间做一次备份,所以如果redis意外down掉的话,就会丢失最后一次快照后的所有修改。
- fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑。
可以在配置文件中配置RDB:
会生成一个dump.rdb文件。
2.6.2、AOF
AOF全称Append Only File,就是将redis执行过的所有写指令记录下来,在下次redis重新启动时,只要把这些写指令从前到后再重复执行一遍,就可以实现数据恢复了。
以日志的形式来记录每个写操作。将Redis执行过的所有写指令记录下来(读操作不记录), 只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。Aof保存的是appendonly.aof文件。
优点:
- 每修改同步:
appendfsync always
同步持久化,每次发生数据变更会被立即记录到磁盘,性能较差但数据完整性比较好。 - 每秒同步:
appendfsync everysec
异步操作,每秒记录,如果一秒内宕机,有数据丢失。 - 不同步:
appendfsync no
从不同步。
缺点:
- 相同数据集的数据而言aof文件要远大于rdb文件,恢复速度慢于rdb。
- aof运行效率要慢于rdb,每秒同步策略效率较好,不同步效率和rdb相同。
可在配置文件中进行AOF的配置:
用的最多的其实是always,这样风险最小。
会生成一个appendonly.aof的文件。
2.7、Jedis的使用
Jedis是Redis官方首选的Java客户端开发jar包,一般会配合commons-pool.jar包使用,因为会用到连接池,合理的利用连接资源。
先加入依赖:
<dependencies>
<!-- jedis客户端依赖 -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
<!-- commons-pool2依赖 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.8.0</version>
</dependency>
<!-- json转换包 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.73</version>
</dependency>
<!-- junit测试包 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.7.0</version>
<scope>test</scope>
</dependency>
</dependencies>
创建一个工具类:
/*
* 工具类,管理Jedis对象
*/
public class JedisManager {
private static JedisPoolConfig config;
private static JedisPool jedisPool;
private static Jedis jedis;
// 定义一个方法,获取Jedis对象
public static Jedis getJedis() {
// 获取Jedis连接池对象
JedisPool pool = getPool();
if (jedis == null) {
// 获取Jedis对象
jedis = pool.getResource();
}
// 密码验证
jedis.auth("123456");
return jedis;
}
// 定义一个方法,创建JedisPool对象
private static JedisPool getPool() {
if (config == null) {
config = new JedisPoolConfig();
}
// 设置最大连接数
config.setMaxTotal(20);
// 设置最大空闲连接数
config.setMaxIdle(10);
// 创建连接池对象
if (jedisPool == null) {
jedisPool = new JedisPool(config, "127.0.0.1", 6379);
}
return jedisPool;
}
// 定义一个方法,关闭Jedis对象和连接池
public static void closeJedisAndPool() {
if (jedisPool != null) {
jedisPool.close();
}
if (jedis != null) {
jedis.close();
}
}
}
测试工具类:
public static void main(String[] args) {
// 测试获取Jedis对象
Jedis jedis = JedisManager.getJedis();
if (jedis != null) {
System.out.println("Redis连接成功!");
System.out.println(jedis);
// 关闭
JedisManager.closeJedisAndPool();
System.out.println("连接已关闭!");
}
}
新建一个测试类进行测试:
public class JedisTest {
@Test
public void testString() {
// 获取jedis对象
Jedis jedis = JedisManager.getJedis();
// 存数据
jedis.set("username", "云过梦无痕");
// 取值
String username = jedis.get("username");
System.out.println("值是:" + username);
// 关闭
JedisManager.closeJedisAndPool();
}
}
添加新方法:
// 测试Hash
@Test
public void testHash() {
Jedis jedis = JedisManager.getJedis();
Map<String,String> map = new HashMap<>();
map.put("name","yanchengzhi");
map.put("age","25");
map.put("sex","男");
map.put("address","湖北");
// 存值
jedis.hmset("user", map);
// 取值
List<String> list = jedis.hmget("user","name","age","sex","address");;
System.out.println("姓名:" + list.get(0));
System.out.println("年龄:" + list.get(1));
System.out.println("性别:" + list.get(2));
JedisManager.closeJedisAndPool();
}
添加新方法:
// 测试list
@Test
public void testList() {
Jedis jedis = JedisManager.getJedis();
// 存值
jedis.lpush("nums", "1","2","3","4","5","2");
// 取值
List<String> list = jedis.lrange("nums", 0, 5);
System.out.println("集合元素个数:" + list.size());
System.out.println("第1个元素:" + list.get(0));
System.out.println("第2个元素:" + list.get(1));
System.out.println("第3个元素:" + list.get(2));
System.out.println("第4个元素:" + list.get(3));
System.out.println("第5个元素:" + list.get(4));
System.out.println("第6个元素:" + list.get(5));
JedisManager.closeJedisAndPool();
}
添加新方法:
// 测试set
@Test
public void testSet() {
Jedis jedis = JedisManager.getJedis();
jedis.sadd("person", "yanchengzhi","25","湖北","重庆");
// 取值
Set <String> set = jedis.smembers("person");
System.out.println("元素个数:" + set.size());
for(String s:set) {
System.out.println(s + "\t");
}
JedisManager.closeJedisAndPool();
}
添加新方法:
// 测试zset
@Test
public void testZset() {
Jedis jedis = JedisManager.getJedis();
Map<String, Double> map = new HashMap<>();
map.put("yan", 70.0);
map.put("cheng", 75.0);
map.put("zhi", 90.0);
// 存值
jedis.zadd("sort", map);
// 倒序取值
Set<String> set = jedis.zrevrange("sort", 0, -1);
for(String s:set) {
System.out.println(s + "\t");
}
JedisManager.closeJedisAndPool();
}
实际开发中,一般都是将对象先转换成json字符串,再存进redis数据库中的,无论是List,Map还是Set,都先转换成json格式的字符串。如下测试:
// 将对象转换成json串存进redis
@Test
public void test() {
Jedis jedis = JedisManager.getJedis();
Map<String,Object> map = new HashMap<>();
map.put("name","云过梦无痕");
map.put("age", 25);
map.put("birth", new Date());
// map转成json串
String jsonStr = JSON.toJSONString(map);
// 存值
jedis.set("demo", jsonStr);
// 设置键值对的过期时间
jedis.expire("demo", 12000);
// 取值
String str = jedis.get("demo");
// 字符串转成map对象
Map<String,Object> demo = JSON.parseObject(str);
System.out.println("姓名:" + demo.get("name"));
System.out.println("年龄:" + demo.get("age"));
System.out.println("生日:" + demo.get("birth"));
// 获取过期时间
long expire = jedis.ttl("demo");
System.out.println("剩余过期时间:" + expire);
JedisManager.closeJedisAndPool();
}
小结:Jedis将每一个Redis手动命令都封装成了一个方法,所以需要对Redis命令非常熟悉,才能熟练的使用Jedis提供的各种API方法。
2.8、Redis视图工具
Redis自提供的界面并不友好,只是一个命令窗口,可以使用一个Redis的视图工具。比如Redis Desktop Manager,可以去网上下载并安装。
安装完毕后打开:
输入名称、ip地址、端口号和密码,点击OK。
可以看到Redis提供的所有数据库。
0号数据库有刚才存的值,可以查看:
右上角TTL是剩余过期时间。
TTL的值为-1表示永不过期,没有过期时间,也就是添加的时候并没有设置,所以自动设置为-1了,一般添加的时候都会设置过期时间,否则时间久了Redis内存不够。