Redis数据结构服务器
1. redis 是个啥?
- redis(Remote Dictionary Server)远程字典服务,是一个内存 NoSql 数据库,即非关系型数据库。
- redis是,存储
key-value
形式的数据。 - redis 中的
key-value
中 的 value 比较强大,它的value可以不仅仅是一个byte[] (zookeeper只能存byte[])- redis 的
value
可以有结构 :可以是一个List,也可以是一个hash(简单的理解为:value里也可以存key-value),也可以是set…所以redis经常被称作为----数据结构服务器。
- redis 的
1.1 为什么需要redis
- 纵观现在互联网的发展趋势,电商几乎独占天下,电商业务的特点:
- CURD 的操作中,80%是查询业务, 20%是新增,修改,删除业务。
- 数据不会频繁的变更 (电商中的店小二只维护商品的价格)。
- 上面说了这么多,之所以选择redis就是,因为它的快。
- 相比于mySql来说,redis基于 内存操作的,这样好处就是减少了磁盘IO的操作。
- 合理的数据编码,如根据需求能存储相应的数据类型,存储的 5种数据类型。
- 并且有自己的虚拟机制。
- 它是单线程操作,避免了频繁的线程切换。
2. Redis 安装
2.1 安装前准备
-
因为redis安装需要 依赖c语言环境,所以需要gcc进行编译。
- 如果下载最新版本的redis
redis -6.0.6
的话就会需要 Linux, gcc编译版本在5.0以上,这里实验使用的是redis-3.2.8.tar.gz
gcc版本在 4就可以使用。 gcc --version
查看gcc版本。
- 如果下载最新版本的redis
-
然后上传
redis-3.2.8.tar.gz
到Linux目录下/usr/java
,并解压该tar包。-
进入
/usr/java/redis-3.2.8/src
执行默认有gcc的话,在该目录下执行make
。 会产生相应的库文件。
-
make之后 ,在使用
make install
即确认安装。将可以执行文件放到相应目录。
-
-
注意:
make install
默认会安装在/usr/local/bin
,也可以自己指定目录make PREFIX=/usr/java/redis install
中间加路径即可。
2.2 启动 redis
1 . 启动 redis 服务端- 服务器端启动 /usr/local/bin
执行 redis-server
。
2. 客户端启动: 然后再启动一个连接,/usr/local/bin
目录下执行 redis-cli
客户端启动。
-
注意,有时候set 中文时会有乱码,中文乱码问题 就需要启动时
redis-cli --raw
。
4. 测试客户端连接。 输入ping
即可
3. redis 存储数据结构
3.1 支持的数据类型
- redis可以存储类型
key-value
形式。其实还有3种特殊类型但是一般不刻意使用。- 3 种特殊类型: Geo,HyperLogLog,Bitmaps。
3.2 命令行的基本操作 String set list 为例
3.2.1 String 命令行操作
- 既然是存储就离不开增删改查,以String类型为例子。
set key value
存放数据 ,如果添加的key值相同,则覆盖value值。
// 添加数据 set key value set name zhangsan
get key
查看value值
// 查看value值 get key get name
keys *
显示所有key值
// 显示所有key值 keys * keys *
del key
删除key值,相应的数据也会删除。
// 删除key del key del name
dbsize
显示key总数。 如,有多少个键值对。
// 查看当前key总数 dbsize dbsize
rename key newkey
重命名key
// 将key值重命名 rename key newkey rename name name1
exists key
检查键值是否存在
// 检查键值是否存在 exists key exists name
type key
显示键的类型
// 查看键的类型 type key type name
3.2.2 List 命令行操作
-
添加List操作,
lpush/rpush
明白什么是从左侧添加? 什么是从右侧添加?- 添加操作从,左边开始添加
lpush key value1[value2...]
- 添加操作从,右边开始添加
rpush key value 1[value2...]
- 添加操作从,左边开始添加
// 1.往redis中添加list 从左边开始添加
lpush grils liuyifei yangmi angelbaby zhaoliying
// 2.往redis中添加list 从右侧开始添加
rpush boys liudehua zhangxueyou guofucheng liming
-
查询操作
- 根据索引查询
lindex key index
- 通过获取list起始元素开始查询
lrange key start index stop index
- 显示 列表长度
llen key
//1. 根据索引查询 lindex girls 0 //左插入,返回结果 "zhaoliying" lindex boys 0 // 右插入,返回结果 "liudehua" //2. 通过元素获取 lrange girls 0 -1 //0~-1指获取全部 lrange girls 0 1 // zhaoliying angelbaby // 3.显示列表长度 llen boys // 4
- 根据索引查询
-
删除操作,每次只删除一个。
- 从左边删除
lpop key
- 从右边删除
rpop key
//1.从左边删除一个 lpop girls // zhaoliying //2.从右边删除第一个 rpop girls // liuyifei
- 从左边删除
-
删除操作 ,删除指定元数 。
- 假设list 列表里面有 10个yangmi 元数
lrem girls count yangmi
// 删除指定元素 count 指定数量 lrem girls 8 yangmi // 即删除list 中 8个yangmi
- 假设list 列表里面有 10个yangmi 元数
-
修改操作
- 修改指定索引lset key index value
// 修改索引 0 的元素为ruhua lset girls 0 ruhua
3.2.3 set 命令行操作
-
添加元素 不存储相同的集合元素。
sadd key value1 [value2] ...
添加元素,
// 添加元素 sadd friends a b c d d d e // 只能添加一个d
-
显示出集合所有元素。
smembers key
,显示无序!- 获取成员集合成员个数
scard key
// 1.显示出集合中所有元素 smembers friends //列出所有 b c d e a 因为是无序! //2 .获取成员个数 scard friends // 5
- 获取成员集合成员个数
-
删除集合中元素
srem key value 1 [ value2]...
。- 随机删除元素并返回删除元素
spop key
// 1 删除一个或者多个 srem friends a b c // 删除 a b c // 2.随机删除元素 spop friends // 随机删除 并返回删除元素
- 随机删除元素并返回删除元素
-
集合之间的操作。
- 返回集合之间的交集。
sinter key1[key2]...
//1.返回多个集合之间的交集 sinter friends friendss //
- 返回集合之间的并集。
sunion key1 key2 ...
//1.返回集合之间的并集,即集合合并去除重复。 sunion friends friendss //相当于合并集合
- 返回集合之间的 差集。
sdiff key1 key2 ...
//1.取集合之间的差集,即去除相同元素 sdiff friends friendss //去除同类元素
- 返回集合之间的交集。
3.3 flushall 和 flushdb
-
二者区别在于 “程序员删库跑路事件”
// 清空所有数据库 1. flushall // 清空当前数据库 2. flushdb
4. Jedis客户端Api的示范
4.1 存String类型
- 因为是用底层c开发的,就需要java开发程序去连接,这里我们选择的是jedis java连接redis开发,首先需要导入pom.xml 依赖文件。
- 可以使用中央仓库,但是需要网络 中央仓库 。
- pom.xml 。
<dependencies> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.9.0</version> </dependency> </dependencies>
- 使用 java 操作Api 连接redis操作。
- 注意修改
redis.conf
文件,配置ip 权限保护,后台启动。 - 并且重启之后需要启动服务时读取该文件,
redis-server /usr/java/redis-3.2.8/redis.conf
- 注意修改
public class Redis_Client {
Jedis jedis =null;
@Before
public void init() {
/* 1.通过jedis对象创建连接。
new jedis(host,prot)即可
*/
jedis = new Jedis("192.168.150.140", 6379);
String ping = jedis.ping();
System.out.println(ping); //返回 pong
}
@Test
public void testSet(){
String data = jedis.set("name", "刘亦菲");
System.out.println(data);
}
@Test
public void testGet(){
String name = jedis.get("name");
System.out.println(name);
}
}
4.2 存储对象
- 使用redis存储对象。
- 访问电商京东,思考一下,那些商品存储在什么地方 ? 每次都需要访问数据库么?
- 访问电商京东,思考一下,那些商品存储在什么地方 ? 每次都需要访问数据库么?
public class Product_redis {
@Test
public void setProduct() throws IOException {
Jedis jedis = new Jedis("192.168.150.140", 6379);
Product p = new Product("001","明星同款空调房披肩外搭女秋冬季米灰刘-亦菲同款 190*65cm",89);
/*
将对象转成字节数组 object to bytearray
set(byte[],byte[])
*/
ByteArrayOutputStream bs = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bs);
oos.writeObject(p);
jedis.set(p.getP_id().getBytes(),bs.toByteArray());
jedis.close();//关闭
}
@Test
public void getProduct() throws IOException, ClassNotFoundException {
Jedis jedis = new Jedis("192.168.150.140", 6379);
byte[] bytes = jedis.get("001".getBytes());
/*
//打印的是字节数组,不是我们想要的。
for (byte b:bytes){
System.out.println(b);
}
*/
/*需要将对象进行反序列化 bytearray to object
*/
ByteArrayInputStream bi = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bi);
//转换类型将 object 转为product
Product p=(Product)ois.readObject();
System.out.println(p.getP_id()+" ----"+p.getTitle()+"-----"+p.getPrice());
jedis.close();
}
}
- Product 对象。
public class Product implements Serializable { //实现序列化
private String p_id; //商品id
private String title; //商品标题
private double price; //价格
public Product(String p_id, String title, double price) {
this.p_id = p_id;
this.title = title;
this.price = price;
}
public Product() {
}
public String getP_id() {
return p_id;
}
public void setP_id(String p_id) {
this.p_id = p_id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
}
4.3 存List类型
- 针对客户端的List操作。
public class Redis_Client {
Jedis jedis =null;
@Before
public void init() {
/* 1.通过jedis对象创建连接。
new jedis(host,prot)即可
*/
jedis = new Jedis("192.168.150.140", 6379);
String ping = jedis.ping();
System.out.println(ping); //返回 pong
}
/*
list操作
*/
@Test
public void testList(){
//1.从左侧开始插入
Long lpush = jedis.lpush("nba", "kobe", "jordan", "rose", "ai", "curry");
System.out.println(lpush); //返回长度
//2.从右侧开始插入
jedis.rpush("wnba","canglaoshi","xiaozhelaoshi","guanyuelaoshi");
}
@Test
public void testLrange(){
List<String> nba = jedis.lrange("nba", 0, -1);
System.out.println(nba); //[curry, ai, rose, jordan, kobe]
}
@Test
public void testPop(){
String rnba = jedis.rpop("nba");
System.out.println("从右边删除:" +rnba); //kobe
String lnba = jedis.lpop("nba");
System.out.println("从左边删除:" +lnba); //curry
}
@Test
public void testListSet(){
String lset = jedis.lset("nba", 0, "caixukun");
System.out.println(lset); // //[caixukun, ai, rose, jordan, kobe]
}
}
4.4 存set类型
- set 集合 java API 操作。
public class Redis_Client {
Jedis jedis =null;
@Before
public void init() {
/* 1.通过jedis对象创建连接。
new jedis(host,prot)即可
*/
jedis = new Jedis("192.168.150.140", 6379);
String ping = jedis.ping();
System.out.println(ping); //返回 pong
}
/*
set操作
*/
@Test
public void test_Set(){
//1. 添加操作
Long sadd = jedis.sadd("num", "a", "b", "c", "d", "e");
//2.获取成员个数
Long num = jedis.scard("num");
System.out.println("set集合中value个数"+num);
//3.显示所有
Set<String> word = jedis.smembers("num");
for (String n : word){
System.out.println("循环遍历个数:" +n);
}
//4. 删除
Long srem = jedis.srem("num", "e", "a");
System.out.println("删除的set集合元素"+srem);
}
}
- 集合之间操作。
- 集合之间交集。
- 集合之间并集。
- 集合之间去除相同。
public class Redis_Client {
Jedis jedis =null;
@Before
public void init() {
/* 1.通过jedis对象创建连接。
new jedis(host,prot)即可
*/
jedis = new Jedis("192.168.150.140", 6379);
String ping = jedis.ping();
System.out.println(ping); //返回 pong
}
@Test
public void test_SetandSet(){
//1.取交集
Set<String> sinter = jedis.sinter("num", "num1");
System.out.println("取交集:"+sinter);
//2.取并集
Set<String> sunion = jedis.sunion("num", "num1");
System.out.println("取并集:"+sunion);
//3.取差集 前面的集合为主,去除包含后面的元素。
Set<String> sdiff = jedis.sdiff("num1", "num");
System.out.println("取差集:"+sdiff);
}
}
4.5 存hash类型
- 存key-value形式的hash值。
key - field(字段属性) - value
- 添加数据
- 查询一个,查询全部
- 增量数据
- 判断是否存在
- 删除
public class Redis_Hash {
private Jedis jedis = null;
@Before
public void init (){
jedis = new Jedis("192.168.150.140", 6379);
System.out.println(jedis);
}
@Test
public void testHash(){
/*
1.添加数据, hset(String var1, String field, String var3);
*/
jedis.hset("cart","苹果","1");
jedis.hset("cart","香蕉","3");
jedis.hset("cart","橘子","5");
/*1.1
添加多个key value
*/
Map<String, String> hash = new HashMap();
hash.put("猕猴桃","1");
hash.put("冬枣","2");
hash.put("青瓜","5");
jedis.hmset("cart",hash);
/*2. 获取数据
hget(key,fild); //第一个key值 第二个是value中的 key
*/
String hget = jedis.hget("cart", "苹果");
System.out.println(hget);
/*2.1 返回多个
hgetAll(key),通过key返回里面的value(key -value)。
*/
Map<String, String> cart = jedis.hgetAll("cart");
System.out.println(cart);//{青瓜=5, 猕猴桃=1, 苹果=1, 香蕉=3, 橘子=5, 冬枣=2}
/*3. 向cart中 冬枣 添加4个
青瓜 减掉3个
hincrBy(key,field,value)
*/
jedis.hincrBy("cart","冬枣",4);
jedis.hincrBy("cart","青瓜",-3);
Map<String, String> cart1 = jedis.hgetAll("cart");
System.out.println(cart1);//{青瓜=2, 猕猴桃=1, 苹果=1, 香蕉=3, 橘子=5, 冬枣=6}
/*4. 删除hash表中的一个字段
*/
jedis.hdel("cart","橘子");
Map<String, String> cart2 = jedis.hgetAll("cart");
System.out.println(cart2);//{青瓜=2, 猕猴桃=1, 苹果=1, 香蕉=3, 冬枣=6}
/*5.判断是否存在hash值
hexists(key,field)
*/
Boolean hexists = jedis.hexists("cart", "橘子");
System.out.println(hexists);//false
}
}
4.6 存带排序的Zset集合类型
- set集合无序,如果想排序需要通过代码实现,但是redis中有封装号的有序set。
public class Redis_Zset {
private Jedis jedis =null;
@Before
public void init(){
jedis = new Jedis("192.168.150.140", 6379);
System.out.println(jedis);
}
/**
* 带排序的set集合
* 例如:qq音乐排行榜
* 收听 万单位 人名
*/
@Test
public void test_Zset(){
// 1.添加数据
jedis.zadd("music",300,"周杰伦");
jedis.zadd("music",80,"林俊杰");
jedis.zadd("music",90,"王力宏");
jedis.zadd("music",10,"蔡徐坤");
//2.获取数据 升序
Set<String> music = jedis.zrange("music", 0, -1);
System.out.println(music);
//2.1 获取数据 降序 reverse
Set<String> revmusic = jedis.zrevrange("music", 0, -1);
System.out.println(revmusic);
//3. 取某个元素的索引 从升序中取
Long zrank = jedis.zrank("music", "王力宏");
System.out.println(zrank);
//3.1 取某个元素索引 从降序中取
Long zrevrank = jedis.zrevrank("music", "王力宏");
System.out.println(zrevrank);
//4.返回某个成员的分数值。
Double music1 = jedis.zscore("music","王力宏");
System.out.println(music1); //90
//5.删除元素
Long zrem = jedis.zrem("music", "林俊杰");
System.out.println(zrem);
//6. 对有序集合上进行增量
Double zincrby = jedis.zincrby("music", 3, "周杰伦");
System.out.println(zincrby);
}
}
[蔡徐坤, 林俊杰, 王力宏, 周杰伦]
[周杰伦, 王力宏, 林俊杰, 蔡徐坤]
2
1
90.0
1
303.0
5 统计场景案例
- 查看电影观看次数。
- 提示:
zrangeByScoreWithScores()
方法使用。
- 提示:
/**
客户端采集
*/
public class Movie_Customer {
public static void main(String[] args) throws InterruptedException {
//1.连接客户端
Jedis jedis = new Jedis("192.168.150.140", 6379);
//2. 创建一个数组里面存储电影
String[] movies = {"战狼","战狼2","复仇者联盟","复仇者联盟2","复仇者联盟3","复仇者联盟4"};
//3.随机访问观看
Random random = new Random();
while (true){
//4 随机挑一个电影观看
String movie = movies[random.nextInt(movies.length)];
/*4.1
放入redis中,后面会统计观看
相当于更新 观看点击次数,每次进行加1 在挑选的基础上
*/
jedis.zincrby("movie",1,movie);
//5. 看时间不定,假设在看电影
System.out.println("电影观看中-------");
Thread.sleep(300);
}
}
}
/**
显示观看排行榜
*/
public class Movie_Box {
public static void main(String[] args) throws InterruptedException {
Jedis jedis = new Jedis("192.168.150.140", 6379);
while (true){
// //1.显示 redis数据,即显示观看次数 ,选择升序还是降序!
// Set<String> movie = jedis.zrevrange("movie", 0, -1);//显示全部
// System.out.println(movie);
// 1.1 需要显示members 和 key值 zrangeByScoreWithScores 指定区间
Set<Tuple> movie = jedis.zrangeByScoreWithScores("movie", 0, 100, 0, 10);
for (Tuple t : movie){
System.out.println(t.getElement()+"---"+t.getScore());
}
//2.显示查看, 在15秒查询5次。
Thread.sleep(1500);
System.out.println("------------------------");
}
}
}
6 话题
6.1 redis的两种持久化方案
- redis是基于内存的存储key-value服务器,可想而知,内存不是磁盘,一旦断电或者宕机就会造成数据的缺失。所以它不同于
MemoryCache
它提供了持久化的能力。- RDB 相当于快照来保存。利用一个进程fork,遍历整个hashtable 然后将整个db dump都保存下来。没有实时性
- AOF 采用日志的形式来记录每个写操作, 追加到文件中,重启时再重新执行AOF文件中的命令来恢复数据。它主要解决数据持久化的实时性问题。 默认是不开启的。
- 至于怎么选择持久化看业务需求,也可以二者同时使用。
6.2 redis 和数据库 双写一致性问题
- redis作用就是缓存,提高读的效率操作,它只是应用和数据库 之间的一个读操作的缓存层。 目的就是减少数据库的IO,客户端读数据有缓存就从缓存中读取,没有缓存就从数据库中读取,然后将读取的数据写入缓存中,
- 这是就会出现的问题,一份数据同时保存在redis和数据库中,当数据发声变化时,需要更新 redis和数据库,因为更新操作是有先后顺序的,不像mysql中的多表事务操作,可以满足ACID的特性,此时就会出现数据一致性的问题。一致性问题如果具体在细分的话,还可以分为最终一致性和强一致性。
- 数据库和缓存双写,就必然会出现这样的问题,不一致问题。前提是如果对数据有强一致性要求就不能放缓存。
- 只能说从方案上处理,最多能降低不一致发生的概率,可以使用消息队列保证一致性,如redismq等组件。
6.3 缓存穿透问题
- 所谓穿透, 可以理解为,去缓存中访问缓存不存在的数据,这样缓存中没有,从而就会向数据库中访问数据,这样压力都会指向持久层,根本在于,当查询缓存中数据没有命中, 由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,进而给数据库带来压力。
- 如: 用户想要查询一个数据,发现redis内存数据库没有,也就是缓存未命中,于是向持久层数据库查询。发现也没有,于是本次查询结束。
- 当很多的用户都发起这个相同的操作,缓存都没有命中 (抢演唱会门票) 于是都去请求了持久层数据库,这会给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透。
- 黑客攻击缓存中不存在的数据,也会造成上述表现。
- 如果会造成这样的结果就失去了缓存的意义,其redis本身就是为了解决数据库压力而使用的。
- 解决办法:
-
- 使用互斥锁,缓存失效时,先获取锁在请求数据库,没有锁就休眠时间在去请求,这样可以缓解数据库压力。
-
- 使用 布隆过滤器{布隆过滤器是一种数据结构,布隆过滤器对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃,从而避免了对底层存储系统的查询压力} 快速判断数据是否存在。内部维护一系列合法有效的key,然后迅速做出判断,请求的key是否合法,如果合法则有效访问,如果不合法就直接返回。
6.4 缓存雪崩问题
- redis 缓存中可以设置有效时间,此时相当于大批的缓存失效,而此时查询量又很大,结果造成了数据库的压力倍增,从而导致数据库连接异常。
- redis故障也能引起雪崩问题, 可以使用redis高可用解决。
- 解决办法:
-
- 给缓存的失效时间,让过期时间离散一点,不在同一个时间点上大量过期,例如:在上一个随机值,避免集体失效。
-
- 使用锁机制,限流,这样也可以降低访问。
-
- 双缓存机制, A 缓存, B 缓存, 一个设置有时间,另一个不设置过去时间。问题是需要自己做缓存的同步操作。 例如: 从a缓存读取数据,有就返回,如果没有在去B中读取, 这时在同步A和B的数据。相当于提前做了缓存预热。
6.5 热key问题
- 在redis中有些key是访问比较高的,如: 音乐排行榜 ,新闻热点 。这种访问比较高的称之为热key。
- 由于请求量特别大,不断的请求服务器操作,可能会导致主机资源不足,从而影响正常的服务。
- 解决方案:
-
- 凭经验判断哪些可能成为热key,然后把热key分散到不同的服务器上。
-
- 在客户端进行统计收集:这个方式就是在操作redis之前,加入一行代码进行数据统计。那么这个数据统计的方式有很多种,也可以是给外部的通讯系统发送一个通知信息。
-
- 做二级缓存,将热key缓存到本地jvm中即可。不走redis读取。