Redis

Redis

一、全球数据库排名:

https://db-engines.com/en/ranking

全球数据库排名

json编程,数据库发展

Key-value 类型,Redis第一,redis内存数据库!

MySQL单表数据最多千万,百万级别;
Oracle单表数据最多亿,千万级别;
Elasticsearch(ES)起点1亿,几十亿级别。大数据检索都用这个ES
Solr
Elasticsearch和Solr基于 lucene。
SAP 德国公司 ERP起家 。HANA基于云平台

Redis历史:
redis本来是一个小的系统,写日志 – log4j
业界都向redis过滤

二、常见缓存框架

1. EhCache,很多框架底层就直接利用它来做缓存,hibernate,mybatis,spring,单机
2. MemCache,分布式内存数据库(不落地),多线程,性能最快,KV(String , 512k)
3. Redis,落地(持久化)内存数据库+磁盘,单线程+多实例,“丰富”数据类型:提供5种(string实际开发中用的最多 512M,list,set,hash,zset排序,2^32次方,4亿)

落地重要:

redis数据可以丢吗? 可以
缓存框架,真正的数据在数据库中。缓存中的数据只是数据库中的数据快照(副本)。

Redis和MemCache比较
持久化数据落地,保存到磁盘。(支持方式非常多,每秒落地一次,如果内容很大也可以直接落地)。解决缓存雪崩问题。MemCache不落地,纯内存。Redis支持丰富的类型:string(512m),list,hash,set,zset(2^32大概4亿)五种。MemCache只支持string。Redis操作是原子性,单线程+多实例。MemCache多线程。业界实际开发中,大多项目都由MemCache向Redis迁移。在很少的地方,功能单一节点MemCache仍然在用。

京东阿里Redis集群应该达到1000+,已经比数据库重要。会把大量数据(热点查询数据)缓存。大多用户读取数据都来自缓存。才能抗住海量的高并发。6.18节日双11。
垂直拆分,后台炸了,前台正常使用。多部署几个服务器,多条通路。

三、缓存雪崩及解决方案

数据库前面搭建缓存服务器,缓存服务器缓存数据库大量数据,正常时,用户访问后台,后台去读取数据,先到缓存中读取,如果缓存中存在,就直接返回数据给调用者,如果数据不存在,继续访问数据库,数据库返回结果,嗲用者吧数据库中的数据写一份到缓存中,然后返回。

在海量数据时,高并发访问情况下,因为高并发用户的请求非常多,远远超出了数据库访问压力。型号有缓存,缓存就挡住了海量的用户请求,少量的用户请求因为缓存没有数据,穿透缓存,还是直接访问数据库,但是此时数据量非常小,数据库可以承受住这些少量用户请求。

如果是MemCache,经过上面长期使用,MemCache中国已经缓存了大量的数据。意外,缓存的服务器宕机。当海量用户请求过来,缓存服务器宕机,这些海量用户的请求继续访问数据库。海量的请求造成数据库无法响应,直接宕机。重启数据库,用户的请求又涌入,数据库又会宕机。启动缓存服务器,缓存服务器没有数据,用户请求一判断没有数据,继续访问数据库,数据库又宕机。把网络断开,数据库重启,启动,启动缓存服务器。不能正常工作,因为缓存服务器没有数据,所以一旦把网络接通,继续宕机。

看似无解。。。无奈。。。
上面解决办法只能是(限流)

如果是reids,缓存服务器宕机,海量请求穿透缓存访问数据库,数据库也宕机。重启缓存服务器。有数据吗?在缓存服务器宕机前,每时每刻把数据放入本地的磁盘。重启后,大量的数据,直接的读入内存。缓存重建过程非常快,第一、不需要转类型,结构一致。第二、它从本地恢复,不需要跨网络。就可以很快创建结构,秒级内就重建缓存结构。大量用户请求过来,大都被缓存服务器拦截,有数据直接返回。数据库重启后,少量用户的请求穿透缓存访问数据库,但是访问的量就非常小,数据库能承受。整个服务器就正常了。

缓存有没有上限?

肯定有,基于内存,内存有限。

内存的数据<数据库的数据

缓存不是缓存所有的数据,只缓存热点数据,常用的查询,数据不频繁变化(修改删除)的数据。

驱逐策略:当需要缓存新的内容,但是缓存已经存满,但是缓存已经存满。必须有人腾出空间。

LRU算法:最近最久未使用的数据就把它干掉
命中率

四、NOSQL-Redis介绍

缓存在电商项目乃至互联网项目使用之广,使用的点也非常之多,基本你想到的地方就都有缓存。

1.为什么要用缓存?

电商网站的行为非常特殊,大量的商品的浏览,对热门的商品,卖的好的商品,评价好的商品浏览频繁。而这部分数据遵循二八定律。只占整体的二成。既然大部分的业务访问都集中在这一小部分数据上,那么如果把这一小部分数据缓存在内存中,是不是就可以减少数据库的访问压力,从而提高整个网站的数据访问速度,改善数据库的写入性能了呢?答案是显而易见的。我们都知道数据库访问创建链接,执行查询,销毁链接是一个非常耗费资源和缓慢的过程。而大量的拦截请求直接从内存读取数据返回给用户。性能提高数百倍。

缓存是改善性能的第一手段。

2.什么是nosql?

Nosql=no only sql 泛指非关系型数据库。
随着互联网web2.0网站的兴起,传统的关系数据库在应付web2.0网站,特别是超大规模和高并发的SNS类型的web2.0纯动态网站已经显得力不从心,暴露了很多难以克服的问题,而非关系型的数据库则由于其本身的特点得到了非常迅速的发展。NoSQL数据库的产生就是为了解决大规模数据集合多重数据种类带来的挑战,尤其是大数据应用难题。

3.主流的NOSQL

这里写图片描述

4.Redis简介

官网:http://redis.io

Redis是一个key-value存储系统。和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set –有序集合)和hash(哈希类型)。这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。在此基础上,redis支持各种不同方式的排序。与memcached一样,为了保证效率,数据都是缓存在内存中。区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。
Redis 是一个高性能的key-value数据库。 redis的出现,很大程度补偿了memcached这类key/value存储的不足,在部分场合可以对关系数据库起到很好的补充作用。它提供了Java,C/C++,C#,PHP,JavaScript,Perl,Object-C,Python,Ruby,Erlang等客户端,使用很方便。
Redis支持主从同步。数据可以从主服务器向任意数量的从服务器上同步,从服务器可以是关联其他从服务器的主服务器。这使得Redis可执行单层树复制。存盘可以有意无意的对数据进行写操作。由于完全实现了发布/订阅机制,使得从数据库在任何地方同步树时,可订阅一个频道并接收主服务器完整的消息发布记录。同步对读取操作的可扩展性和数据冗余很有帮助。
可以充当缓存、队列等作用。
源码脱管: https://github.com/antirez/redis

历史和发展

2008年,意大利的一家创业公司Merzia推出一款基于MySQL的网站实时统计系统LLOOGG,然而没过多久该公司的创始人Salvatore Sanfilippo便对MySQL的性能感到失望,于是他决定亲自为LLOOGG量身定做一个数据库,并于2009年开发完成,这个数据库就是Redis。不过Salvatore Sanfilippo并不满足只将Redis用于LLOOGG这一款产品,而是希望更多的人使用它,于是在同一年Salvatore Sanfilippo将Redis开源发布,并开始和Redis的另一名主要的代码贡献者Pieter Noordhuis一起继续着Redis的开发,直到今天。
短短几年,Redis就拥有了庞大的用户群体。Hacker News在2012年发布了一份数据库的使用情况调查,结果显示有近12%的公司在使用Redis。国内如京东、淘宝、新浪微博、街旁网、知乎网、国外如GitHub、Stack Overflow、Flickr等都是Redis的用户。
VMware公司从2010年开始赞助Redis的开发,Salvatore Sanfilippo和Pieter Noordhuis也分别在3月5月加入VMware,全职开发Redis。

特性

1)多种数据类型存储
字符串类型 string 512M
散列类型 hash2^32-1
列表类型 list 2^32-1
集合类型 set 2^32-1
有序集合类型 zset
2)内存存储与持久化
内存的读写速度远快于硬盘
自身提供了持久化功能(RDB、AOF两种方式)
3)功能丰富
可以用作缓存、队列、消息订阅/发布
支持键的生存时间
按照一定规则删除相应的键
4)简单稳定
相比SQL而言更加简洁
不同语言的客户端丰富
基于C语言开发,代码量只有3万多行
5)所有操作是原子性的
Redis 使用单个 Lua 解释器去运行所有脚本,并且, Redis 也保证脚本会以原子性(atomic)的方式执行:当某个脚本正在运行的时候,不会有其他脚本或 Redis 命令被执行。这和使用 MULTI / EXEC 包围的事务很类似。在其他别的客户端看来,脚本的效果(effect)要么是不可见的(not visible),要么就是已完成的(already completed)

版本说明

Redis的版本规则如下:
次版本号(第一个小数点后的数字)为偶数的版本是稳定版本(2.4、2.6等),奇数为非稳定版本(2.5、2.7),一般推荐在生产环境使用稳定版本。
说明:Redis官方是不支持windows平台的,windows版本是由微软自己建立的分支,基于官方的Redis源码上进行编译、发布、维护的,所以windows平台的Redis版本要略低于官方版本。
所以使用redis推荐在linux下使用。从这也可以知道电商项目、互联网项目都是在linux上运行的。

五、安装Redis

安装配置reids,redis分片、哨兵、集群
1. 安装redis(安装多实例) ,redis.conf
2. 了解常用的命令,string , hash , list , set,事务(秒杀),管道

注意:

1. redis中默认有16个数据库,不能自定义名称,默认0~15。如果是分布式数据库,不支持多个数据库,只操作0数据库。
2. key值必须唯一,如果相同,值可能就被覆盖
3. mset/mget在分布式集群方式不支持!

注意:redis3.0之后引入了集群
mkdir –p /usr/local/src/redis
cd /usr/local/src/redis

把redis-3.2.11.tar.gz复制进来

tar -xvf redis-3.2.11.tar.gz

cd redis-3.2.11

make #下载后编译,过程稍长

make install #进行安装

redis-server #启动

- - - - - - - - - - -

在另外窗口中执行,

ps -ef|grep redis #验证服务启动状态
redis-cli #启动客户端
127.0.0.1:6379>ping #测试
PONG

六、Redis常用命令:

支持类型:5种,string,list,set,hash,zset
redis自增自减实际项目中非常有用,redis命令是原子性,单线程。论坛:帖子数量,QQ:文章数。传统写入数据库,慢。redis都直接用它来实现。数据还有备份。c语言!

例:

127.0.0.1:6379>set name tony
OK
127.0.0.1:6379>get name
“tony”
127.0.0.1:6379>keys *
1) “name”
127.0.0.1:6379>set num 100
OK
127.0.0.1:6379>get num
“100”
127.0.0.1:6379>keys *
1) “name”
2) “num”
127.0.0.1:6379>set a 1
OK
127.0.0.1:6379>get a
“1”
127.0.0.1:6379>keys *
1) “name”
2) “a”
3) “num”
127.0.0.1:6379>keys n*
1) “name”
2) “num”

1.redis-cli的使用之发送命令

默认连接:IP 127.0.0.1 端口 6379
redis-cli
指定IP端口:
redis-cli –h 127.0.0.1 –p 6379
Redis提供了PING-PONG机制,测试与客户端和服务器链接是否正常
redis-cli ping

redis-cli
redis 127.0.0.1:6379>ping
PONG

2.redis-cli的使用之命令返回值

状态回复(最简单的回复-redis提供的测试命令)
redis>PING
PONG
127.0.0.1:6379>SET test 123
OK
错误回复(以error开头,后面跟着错误信息)
127.0.0.1:6379>TEST
(error) ERR unknown command ‘TEST’
整数回复
127.0.0.1:6379>INCR test_incr
(integer) 1
字符串回复(最长久的一种回复,双引号包裹)
127.0.0.1:6379>get test
“123”
多行字符串回复
127.0.0.1:6379>KEYS *
1) “test_incr”
2) “test”

3.exit

退出
127.0.0.1:6379> exit

4.shutdown

关闭
127.0.0.1:6379>shutdown

5.keys

字符串类型是redis中最基本的数据类型,它能存储任何形式的字符串,包括二进制数据。可以存储JSON化的对象、字节数组等。一个字符串类型键允许存储的数据最大容量是512MB。
赋值与取值:
SET key value
GET key
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> set test 123
OK
127.0.0.1:6379> set test1 ab
OK
127.0.0.1:6379> keys *
1) “test1”
2) “test”
127.0.0.1:6379> get test
“123”
127.0.0.1:6379> get test1
“abc”
127.0.0.1:6379> get test2
(nil)
127.0.0.1:6379>

6.select

redis默认支持16个数据库,对外都是以一个从0开始的递增数字命名,可以通过参数database来修改默认数据库个数。客户端连接redis服务后会自动选择0号数据库,可以通过select命令更换数据库,例如选择1号数据库:
127.0.0.1:6379>SELECT 1
OK
127.0.0.1:6379>GET test
(nil)
说明:
Redis不支持自定义数据库名称。
Redis不支持为每个数据库设置访问密码。
Redis的多个数据库之间不是安全隔离的,FLUSHALL命令会清空所有数据库的数据。

7.keys

获取符合规则的建名列表。
KEYS *
keys test[_]*
keys t[a-d]
说明:
? 匹配一个字符
* 匹配任意个(包括0个)字符
[] 匹配括号间的任一字符,可以使用“-“表示范围。如a[a-d]匹配ab/ac/ad
\x 匹配字符x,用于转义符合,如果要匹配“?“就需要使用\?

8.clear

清除屏幕内容

9.exists

判断一个键是否存在。
如果键存在则返回整数类型1,否则返回0。
127.0.0.1:6379> keys *
1) “test_incr”
2) “test”
127.0.0.1:6379> exists test
(integer) 1
127.0.0.1:6379> exists test1
(integer) 0
127.0.0.1:6379>

10.del

删除键,可以删除一个或者多个键,多个键用空格隔开,返回值是删除的键的个数。
127.0.0.1:6379> del test
(integer) 1
127.0.0.1:6379> del test
(integer) 0
127.0.0.1:6379> del test test_incr
(integer) 1
127.0.0.1:6379>

11.type

获得键值的数据类型,返回值可能是string(字符串)、hash(散列类型)、list(列表类型)、set(集合类型)、zset(有序集合类型)。
127.0.0.1:6379> keys *
1) “test1”
2) “test”
127.0.0.1:6379> type test
string
127.0.0.1:6379> type test1
string

12.help

127.0.0.1:6379> help
redis-cli 2.8.19
Type: “help @< group>” to get a list of commands in < group>
“help < command>” for help on < command>
“help < tab>” to get a list of possible help topics
“quit” to exit
127.0.0.1:6379> help type

TYPE key
summary: Determine the type stored at key
since: 1.0.0
group: generic

官网:http://www.redis.io帮助

13.flushall

清空所有数据库。
127.0.0.1:6379> FLUSHALL
OK

14.flushdb

清空当前数据库。
127.0.0.1:6379> FLUSHDB
OK

注意:keys flushall flushdb不要再线上使用,
keys命令可能长时间执行,锁表,其他操作不了
flushall和flushdb清空缓存,会直接访问数据库,宕机危险

七、Redis数据类型之字符串

存放的字符串为二进制是安全的。字符串长度支持到512M。

1.incr/incrby

递增数字INCR key
当存储的字符串是整数时,redis提供了一个实用的命令INCR,其作用是让当前键值递增,并返回递增后的值。
127.0.0.1:6379> keys *
1) “test1”
2) “test”
127.0.0.1:6379> get test
“123”
127.0.0.1:6379> get test1
“abc”
127.0.0.1:6379> get test2
(nil)
127.0.0.1:6379> incr num
(integer) 1
127.0.0.1:6379> keys *
1) “num”
2) “test1”
3) “test”
127.0.0.1:6379> incr num
(integer) 2
127.0.0.1:6379> incr num
(integer) 3
127.0.0.1:6379>
从上面例子可以看出,如果num不存在,则自动会创建,如果存在自动+1。
指定增长系数
语法:INCRBY key increment
127.0.0.1:6379> incr num
(integer) 2
127.0.0.1:6379> incr num
(integer) 3
127.0.0.1:6379> incrby num 2
(integer) 5
127.0.0.1:6379> incrby num 2
(integer) 7
127.0.0.1:6379> incrby num 2
(integer) 9
127.0.0.1:6379> incr num
(integer) 10
127.0.0.1:6379>

2.decr/decrby

减少指定的整数
DECR key
DECRBY key decrement
127.0.0.1:6379> incr num
(integer) 10
127.0.0.1:6379> decr num
(integer) 9
127.0.0.1:6379> decrby num 3

3.incrbyfloat

整数时,第一次加可以得到正确结果,浮点数后再加浮点就会出现精度问题。
原来下面的例子2.8.7注意在新版本中已经修正了这个浮点精度问题。3.0.7
INCRBYFLOAT key decrement
127.0.0.1:6379>set num 131
(integer) 131
127.0.0.1:6379>incrbyfloat num 0.7
“131.7”
127.0.0.1:6379>incrbyfloat num 0.7
“132.3999999999999999”

4.append

向尾部追加值。如果键不存在则创建该键,其值为写的value,即相当于SET key value。返回值是追加后字符串的总长度。
语法:APPEND key value
127.0.0.1:6379> keys *
1) “num”
2) “test1”
3) “test”
127.0.0.1:6379> get test
“123”
127.0.0.1:6379> append test “abc”
(integer) 6
127.0.0.1:6379> get test
“123abc”
127.0.0.1:6379>

5.strlen

字符串长度,返回数据的长度,如果键不存在则返回0。注意,如果键值为空串,返回也是0。
语法:STRLEN key
127.0.0.1:6379> get test
“123abc”
127.0.0.1:6379> strlen test
(integer) 6
127.0.0.1:6379> strlen tnt
(integer) 0
127.0.0.1:6379> set tnt “”
OK
127.0.0.1:6379> strlen tnt
(integer) 0
127.0.0.1:6379> exists tnt
(integer) 1
127.0.0.1:6379>

6.mset/mget

mset/mget在分布式集群方式不支持!
同时设置/获取多个键值
语法:MSET key value [key value …]
MGET key [key …]
127.0.0.1:6379> flushall
OK
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> mset a 1 b 2 c 3
OK
127.0.0.1:6379> mget a b c
1) “1”
2) “2”
3) “3”
127.0.0.1:6379>

八、Redis生存时间

LRU驱逐策略,自动
flushall直接删除所有内容,手动
del key 删除一个key,手动
expire定时删除,时间到了,over就把它删除
pexpire毫秒级别,秒杀。Java精度,超过秒杀时间,能买(超卖)

1.expire(秒)

Redis在实际使用过程中更多的用作缓存,然而缓存的数据一般都是需要设置生存时间的,即到期后数据销毁。

语法:EXPIRE key seconds
127.0.0.1:6379> flushall
OK
127.0.0.1:6379> set bomb tnt
OK
127.0.0.1:6379> expire bomb 10
(integer) 1
127.0.0.1:6379> ttl bomb
(integer) 5
127.0.0.1:6379> ttl bomb
(integer) 3
127.0.0.1:6379> ttl bomb
(integer) 3
127.0.0.1:6379> ttl bomb
(integer) 2
127.0.0.1:6379> ttl bomb
(integer) 1
127.0.0.1:6379> ttl bomb
(integer) -2
127.0.0.1:6379> ttl bomb
(integer) -2
127.0.0.1:6379>

TTL查看key的剩余时间,当返回值为-2时,表示键被删除。
当 key 不存在时,返回 -2 。当 key 存在但没有设置剩余生存时间时,返回 -1 。否则,以毫秒为单位,返回 key 的剩余生存时间。
注意:在 Redis 2.8 以前,当 key 不存在,或者 key 没有设置剩余生存时间时,命令都返回 -1 。

2.persist

语法:PERSIST key
127.0.0.1:6379> set bomb tnt
OK
127.0.0.1:6379> expire bomb 60
(integer) 1
127.0.0.1:6379> ttl bomb
(integer) 49
127.0.0.1:6379> persist bomb
(integer) 1
127.0.0.1:6379> ttl bomb
(integer) -1
127.0.0.1:6379>

persist命令用于删除键所指定的过期时间。(相当于终止倒计时的意思)

设置新的数据时需要重新设置该key的生存时间。重新设置值也会清除生存时间。

3.pexpire(毫秒)

语法:PEXPIRE key milliseconds
127.0.0.1:6379> set bomb tnt
OK
127.0.0.1:6379> pexpire bomb 10000
(integer) 1
127.0.0.1:6379> ttl bomb
(integer) 6
127.0.0.1:6379> ttl bomb
(integer) 3
127.0.0.1:6379> ttl bomb
(integer) -2
127.0.0.1:6379>

设置生存时间为毫秒,可以做到更精确的控制。

九、Redis高级中的hash结构

在redis中用的最多的就是hash和string类型。

1.问题

假设有User对象以JSON序列化的形式存储到redis中,User对象有id、username、password、age、name等属性,存储的过程如下:
保存、更新:
User对象->json(string)->redis
如果在业务上只是更新age属性,其他的属性并不做更新应该怎么做呢?
Redis数据类型之散列类型hash
散列类型存储了字段(field)和字段值的映射,但字段值只能是字符串,不支持其他类型,也就是说,散列类型不能嵌套其他的数据类型。一个散列类型可以包含最多232-1个字段。

2.hset/hget

HSET和HGET赋值和取值
HSET key field value
HGET key field
HMSET key field value [field value…]
HMGET key field value [field value…]
HGETALL key

127.0.0.1:6379> hset user username chenchen
(integer) 1
127.0.0.1:6379> hget user username
“chenchen”
127.0.0.1:6379> hset user username chen
(integer) 0
127.0.0.1:6379> keys user
1) “user”
127.0.0.1:6379> hgetall user
1) “username”
2) “chen”
127.0.0.1:6379>
127.0.0.1:6379> hset user age 30
(integer) 1
127.0.0.1:6379> hgetall user
1) “username”
2) “chen”
3) “age”
4) “30”
127.0.0.1:6379>
HSET命令不区分插入和更新操作,当执行插入操作时HSET命令返回1,当执行更新操作时返回0。

3.hincrby

HINCRBY和INCR区别在于HINCRBY如果没有KEY 则自动创建KEY然后赋值而INCR只能添加不能判断。
127.0.0.1:6379> hdecrby article total 1
127.0.0.1:6379> hincrby article total -1 #没有hdecrby自减命令
(integer) 1

4.hmset/hmget

HMSET和HMGET设置和获取对象属性
127.0.0.1:6379> hmset person username tony age 18
OK
127.0.0.1:6379> hmget person age username
1) “18”
2) “tony”
127.0.0.1:6379> hgetall person
1) “username”
2) “tony”
3) “age”
4) “18”
127.0.0.1:6379>
注意:上面HMGET字段顺序可以自行定义

5.hexists

属性是否存在
127.0.0.1:6379> hexists killer
(error) ERR wrong number of arguments for ‘hexists’ command
127.0.0.1:6379> hexists killer a
(integer) 0
127.0.0.1:6379> hexists user username
(integer) 1
127.0.0.1:6379> hexists person age
(integer) 1
127.0.0.1:6379>

6.hdel

删除属性
127.0.0.1:6379> hdel user age
(integer) 1
127.0.0.1:6379> hgetall user
1) “username”
2) “chen”
127.0.0.1:6379> hgetall person
1) “username”
2) “tony”
3) “age”
4) “18”
127.0.0.1:6379>

7.hkeys/hvals

只获取字段名HKEYS或字段值HVALS
127.0.0.1:6379> hkeys person
1) “username”
2) “age”
127.0.0.1:6379> hvals person
1) “tony”
2) “18”

8.hlen

元素个数
127.0.0.1:6379> hlen user
(integer) 1
127.0.0.1:6379> hlen person
(integer) 2
127.0.0.1:6379>

9.Jredis示例

package cn.redis;

import java.util.Map;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

public class JedisPoolDemoCMD {

public static void main(String[] args) {
    // 构建连接池配置信息
    JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
    // 设置最大连接数
    jedisPoolConfig.setMaxTotal(50);

    // 构建连接池
    JedisPool jedisPool = new JedisPool(jedisPoolConfig, "127.0.0.1", 6379);

    // 从连接池中获取连接
    Jedis jedis = jedisPool.getResource();

    jedis.hset("USER_1", "username", "zhangsan");
    jedis.hset("USER_1", "password", "123456");

    Map<String, String> val = jedis.hgetAll("USER_1");
    for (Map.Entry<String, String> entry : val.entrySet()) {
        System.out.println(entry.getKey() + "  " + entry.getValue());
    }

    // 将连接还回到连接池中
    jedisPool.returnResource(jedis);

    // 释放连接池
    jedisPool.close();

}

}
Redis中测试:
127.0.0.1:6379> hgetall USER_1
1) “username”
2) “zhangsan”
3) “password”
4) “123456”
127.0.0.1:6379> hgetall user_1
(empty list or set)
127.0.0.1:6379>
注意:key值的大小写是区分的。

十、Redis高级中的list结构

一个列表最多可以包含232-1个元素(4294967295,每个表超过40亿个元素)

1.问题

Redis的list类型其实就是一个每个子元素都是string类型的双向链表。可以通过push,pop操作从链表的头部或者尾部添加删除元素。这使得list既可以用作栈,也可以用作队列。
有意思的是list的pop操作还有阻塞版本的,当我们[lr]pop一个list对象时,如果list是空,或者不存在,会立即返回nil。但是阻塞版本的b[lr]pop可以则可以阻塞,当然可以加超时时间,超时后也会返回nil。为什么要阻塞版本的pop呢,主要是为了避免轮询。举个简单的例子如果我们用list来实现一个工作队列。执行任务的thread可以调用阻塞版本的pop去获取任务这样就可以避免轮询去检查是否有任务存在。当任务来时候工作线程可以立即返回,也可以避免轮询带来的延迟。

2.lpush

在key对应list的头部添加字符串元素
redis 127.0.0.1:6379> lpush mylist “world”
(integer) 1
redis 127.0.0.1:6379> lpush mylist “hello”
(integer) 2
redis 127.0.0.1:6379> lrange mylist 0 -1
1) “hello”
2) “world”
redis 127.0.0.1:6379>

3.rpush

在key对应list的尾部添加字符串元素
redis 127.0.0.1:6379> rpush mylist2 “hello”
(integer) 1
redis 127.0.0.1:6379> rpush mylist2 “world”
(integer) 2
redis 127.0.0.1:6379> lrange mylist2 0 -1
1) “hello”
2) “world”
redis 127.0.0.1:6379>

4.查看list

redis 127.0.0.1:6379> lrange mylist3 0 -1

5.del

redis 127.0.0.1:6379> del mylist

6.linsert

在key对应list的特定位置之前或之后添加字符串元素
redis 127.0.0.1:6379> rpush mylist3 “hello”
(integer) 1
redis 127.0.0.1:6379> rpush mylist3 “world”
(integer) 2
redis 127.0.0.1:6379> linsert mylist3 before “world” “there”
(integer) 3
redis 127.0.0.1:6379> lrange mylist3 0 -1
1) “hello”
2) “there”
3) “world”
redis 127.0.0.1:6379>

7.lset

设置list中指定下标的元素值
redis 127.0.0.1:6379> rpush mylist4 “one”
(integer) 1
redis 127.0.0.1:6379> rpush mylist4 “two”
(integer) 2
redis 127.0.0.1:6379> rpush mylist4 “three”
(integer) 3
redis 127.0.0.1:6379> lset mylist4 0 “four”
OK
redis 127.0.0.1:6379> lset mylist4 -2 “five”
OK
redis 127.0.0.1:6379> lrange mylist4 0 -1
1) “four”
2) “five”
3) “three”
redis 127.0.0.1:6379>

8.lrem

从key对应list中删除count个和value相同的元素,count>0时,按从头到尾的顺序删除
redis 127.0.0.1:6379> rpush mylist5 “hello”
(integer) 1
redis 127.0.0.1:6379> rpush mylist5 “hello”
(integer) 2
redis 127.0.0.1:6379> rpush mylist5 “foo”
(integer) 3
redis 127.0.0.1:6379> rpush mylist5 “hello”
(integer) 4
redis 127.0.0.1:6379> lrem mylist5 2 “hello”
(integer) 2
redis 127.0.0.1:6379> lrange mylist5 0 -1
1) “foo”
2) “hello”
redis 127.0.0.1:6379>
count<0时,按从尾到头的顺序删除
redis 127.0.0.1:6379> rpush mylist6 “hello”
(integer) 1
redis 127.0.0.1:6379> rpush mylist6 “hello”
(integer) 2
redis 127.0.0.1:6379> rpush mylist6 “foo”
(integer) 3
redis 127.0.0.1:6379> rpush mylist6 “hello”
(integer) 4
redis 127.0.0.1:6379> lrem mylist6 -2 “hello”
(integer) 2
redis 127.0.0.1:6379> lrange mylist6 0 -1
1) “hello”
2) “foo”
redis 127.0.0.1:6379>
count=0时,删除全部
redis 127.0.0.1:6379> rpush mylist7 “hello”
(integer) 1
redis 127.0.0.1:6379> rpush mylist7 “hello”
(integer) 2
redis 127.0.0.1:6379> rpush mylist7 “foo”
(integer) 3
redis 127.0.0.1:6379> rpush mylist7 “hello”
(integer) 4
redis 127.0.0.1:6379> lrem mylist7 0 “hello”
(integer) 3
redis 127.0.0.1:6379> lrange mylist7 0 -1
1) “foo”
redis 127.0.0.1:6379>

9.ltrim

保留指定key 的值范围内的数据
redis 127.0.0.1:6379> rpush mylist8 “one”
(integer) 1
redis 127.0.0.1:6379> rpush mylist8 “two”
(integer) 2
redis 127.0.0.1:6379> rpush mylist8 “three”
(integer) 3
redis 127.0.0.1:6379> rpush mylist8 “four”
(integer) 4
redis 127.0.0.1:6379> ltrim mylist8 1 -1
OK
redis 127.0.0.1:6379> lrange mylist8 0 -1
1) “two”
2) “three”
3) “four”
redis 127.0.0.1:6379>

10.lpop

从list的头部删除元素,并返回删除元素
redis 127.0.0.1:6379> lrange mylist 0 -1
1) “hello”
2) “world”
redis 127.0.0.1:6379> lpop mylist
“hello”
redis 127.0.0.1:6379> lrange mylist 0 -1
1) “world”
redis 127.0.0.1:6379>

11.rpop

从list的尾部删除元素,并返回删除元素:
redis 127.0.0.1:6379> lrange mylist2 0 -1
1) “hello”
2) “world”
redis 127.0.0.1:6379> rpop mylist2
“world”
redis 127.0.0.1:6379> lrange mylist2 0 -1
1) “hello”
redis 127.0.0.1:6379>

12.llen

返回key对应list的长度:
redis 127.0.0.1:6379> llen mylist5
(integer) 2
redis 127.0.0.1:6379>

13.index

返回名称为key的list中index位置的元素:
redis 127.0.0.1:6379> lrange mylist5 0 -1
1) “three”
2) “foo”
redis 127.0.0.1:6379> lindex mylist5 0
“three”
redis 127.0.0.1:6379> lindex mylist5 1
“foo”
redis 127.0.0.1:6379>

14.rpoplpush

从第一个list的尾部移除元素并添加到第二个list的头部,最后返回被移除的元素值,整个操作是原子的.如果第一个list是空或者不存在返回nil:

15.拓展:利用链表形成安全消息队列

RPOPLPUSH命令实现安全消息队列。Redis链表经常会被用于消息队列的服务,以完成多程序之间的消息交换。假设一个应用程序正在执行LPUSH操作向链表中添加新的元素,我们通常将这样的程序称之为”生产者(Producer)”,而另外一个应用程序正在执行RPOP操作从链表中取出元素,我们称这样的程序为”消费者(Consumer)”。如果此时,消费者程序在取出消息元素后立刻崩溃,由于该消息已经被取出且没有被正常处理,那么我们就可以认为该消息已经丢失,由此可能会导致业务数据丢失,或业务状态的不一致等现象的发生。然而通过使用RPOPLPUSH命令,消费者程序在从主消息队列中取出消息之后再将其插入到备份队列中,直到消费者程序完成正常的处理逻辑后再将该消息从备份队列中删除。同时我们还可以提供一个守护进程,当发现备份队列中的消息过期时,可以重新将其再放回到主消息队列中,以便其它的消费者程序继续处理。

十一、Redis高级中的set结构

Redis的Set是string类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。Redis中集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。集合中最大的成员数为232 - 1 (4294967295每个集合可存储40多亿个成员)。

1.sadd

添加元素,重复元素添加失败,返回0
127.0.0.1:6379> sadd name tony
(integer) 1
127.0.0.1:6379> sadd name hellen
(integer) 1
127.0.0.1:6379> sadd name rose
(integer) 1
127.0.0.1:6379> sadd name rose
(integer) 0

2.smembers

获取内容
127.0.0.1:6379> smembers name
1) “hellen”
2) “rose”
3) “tony”

3.spop

移除并返回集合中的一个随机元素
127.0.0.1:6379> smembers internet
1) “amoeba”
2) “redis”
3) “rabbitmq”
4) “nginx”
127.0.0.1:6379> spop internet
“rabbitmq”
127.0.0.1:6379> spop internet
“nginx”
127.0.0.1:6379> smembers internet
1) “amoeba”
2) “redis”

4.scard

获取成员个数
127.0.0.1:6379> scard name
(integer) 3

5.smove

移动一个元素到另外一个集合
127.0.0.1:6379> sadd internet amoeba nginx redis
(integer) 3
127.0.0.1:6379> sadd bigdata hadopp spark rabbitmq
(integer) 3
127.0.0.1:6379> smembers internet
1) “amoeba”
2) “redis”
3) “nginx”
127.0.0.1:6379> smembers bigdata
1) “hadopp”
2) “spark”
3) “rabbitmq”
127.0.0.1:6379> smove bigdata internet rabbitmq
(integer) 1
127.0.0.1:6379> smembers internet
1) “amoeba”
2) “redis”
3) “rabbitmq”
4) “nginx”
127.0.0.1:6379> smembers bigdata
1) “hadopp”
2) “spark”
127.0.0.1:6379>
1.2.6 sunion
并集
127.0.0.1:6379> sunion internet bigdata
1) “redis”
2) “nginx”
3) “rabbitmq”
4) “amoeba”
5) “hadopp”
6) “spark”

十二、redis事务管理

如果提交多个命令,事务。
它没有数据库那么丰富。
数据库开启事务,执行事务,回滚事务。redis简单,开启事务,执行事务。
把多个命令放入一个队列,利用队列去执行,要么这一批都成功,要么这一批都失败。

redis是单线程,提交命令时,其它命令无法插入其中,轻松利用单线程实现了事务的原子性。那如果执行多个redis命令呢?自然就没有事务保证,于是redis有下列相关的redis命令来实现事务管理。
multi 开启事务
exec 提交事务
discard 取消事务
watch 监控,如果监控的值发生变化,则提交事务时会失败
unwatch 去掉监控
Redis保证一个事务中的所有命令要么都执行,要么都不执行。如果在发送EXEC命令前客户端断线了,则Redis会清空事务队列,事务中的所有命令都不会执行。而 一旦客户端发送了EXEC命令,所有的命令就都会被执行,即使此后客户端断线也没关系,因为Redis中已经记录了所有要执行的命令。

Redis事务multi开启事务类似数据库beginTransaction),exec提交事务(没有执行那些命令,把所有的命令写入到一个队列中,作用:放在一起,排序。遇到exec时,整体提交。事务在数据库rollback,没有回滚,如果出错,不做。),watch(监控,在线程执行前,对key进行监控,如个对应key的值,执行完命令它还没有变化,提交事务。如果对应的key的值,在开启事务后,执行提交前,这个值发生变化,提交废除),unwatch(取消监控),discard主动废除提交。

1.exec提交事务

例如:模拟转账,王有200,张有700,张给王转100。过程如下:
127.0.0.1:6379> set w 200
OK
127.0.0.1:6379> set z 700
OK
127.0.0.1:6379> mget w z
1) “200”
2) “700”
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby z 100
QUEUED #注意此命令根本没有执行,而是把其放在一个队列中
127.0.0.1:6379> incrby w 100
QUEUED
127.0.0.1:6379> mget w z
QUEUED
127.0.0.1:6379> get w #同时,这些相关的变量也不能再读取
QUEUED
127.0.0.1:6379> get z
QUEUED
127.0.0.1:6379> exec
1) (integer) 600
2) (integer) 300
3) 1) “300”
2) “600”
4) “300”
5) “600”
127.0.0.1:6379> mget w z
1) “300”
2) “600”
127.0.0.1:6379>

watch命令,它把当前线程值监视
如果这个值还是监视前的状态:ticket=1,这时本身提交事务前会做这个判断
如果和监视前的值没有变化,事务提交,操作正常,获取到这个ticket,也就是说买到这张票。
如果和监视前的值变化,另外一个客户端线程操作提交事务ticket变化了。
监视发现这个值和监视时的值不同的。提交事务,失败!
这个不是最佳的解决方案,还有票,但是它不扣。
比没有事务好很多。再次提交。这时只要没有其他扣票的,它就直接扣掉。

2.如果有错误命令,自动取消

127.0.0.1:6379> mget w z
1) “300”
2) “600”
127.0.0.1:6379> multi
OK
127.0.0.1:6379> get w
QUEUED
127.0.0.1:6379> set w 100
QUEUED
127.0.0.1:6379> abc
(error) ERR unknown command ‘abc’
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get w
“300”
127.0.0.1:6379>

3.discard取消事务

注意redis事务太简单,没有回滚,而只有取消。
127.0.0.1:6379> mget z w
1) “600”
2) “300”
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incrby z 100
QUEUED
127.0.0.1:6379> discard
OK
127.0.0.1:6379> get z
“600”
127.0.0.1:6379> exec
(error) ERR EXEC without MULTI

4.秒杀

客户端1:
127.0.0.1:6379> clear
127.0.0.1:6379> set ticket 1
OK
127.0.0.1:6379> set money 0
OK
127.0.0.1:6379> watch ticket #乐观锁,对值进行观察,改变则事务失败
OK
127.0.0.1:6379> multi #开启事务
OK
127.0.0.1:6379> decr ticket
QUEUED
127.0.0.1:6379> incrby money 100
QUEUED
客户端2:还没等客户端1提交事务,此时客户端2把票买到了。
127.0.0.1:6379> get ticket
“1”
127.0.0.1:6379> decr ticket
(integer) 0
客户端1:
127.0.0.1:6379> exec
(nil) #执行事务,失败
127.0.0.1:6379> get ticket
“0”
127.0.0.1:6379> unwatch #取消监控

十三、管道-海量数据导入

很多海量数据放入redis中,缓存预热。
热点数据在系统刚搭建完毕时,没有。如果让先访问数据库,再缓存,速度慢。还会造成对数据库有一定访问压力。
在用户访问之前,先把热点数据代码自动把它先加入到缓存中。
如果数据少,直接执行set命令。如果数据多就可以利用管道功能。
1) 准备数据文件


set name tony
格式要求:*代表命令开始
*3\r\n 代表这行命令有几个部分
$3 名称长度
set 代表命令
$4 第一参数长度4
name 第一个参数内容
$4 第二个参数长度4
tony 第二个参数内容
set ticket 100
*3
$3
set
$6
ticket
$3
100

2) 利用管道命令把这个文件的内容写入redis

由于做性能测试,需要往redis中导出千万级的数据。得知redis-cli工具支持pipeline导入可以达到最佳性能。测试下500万条命令导入耗时43秒。

1.格式要求

官方文档:http://redis.io/topics/mass-insert

数据格式要求:

  • 以*开始
  • *n n代表此条命令分成n个部分
  • 每个部分以\r\n结束

set name tony 表达为:

*3\r\n
$3\r\n
set\r\n
$4\r\n
name\r\n
$4\r\n
tony\r\n

注意:此处的\r\n为换行符,不是输入的字符。

2.示例

package redis;

import java.io.BufferedWriter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;

import org.junit.Test;

public class TestRedisPipe {
    /**
     * 格式化成输入字符串
     */
    private String getString(String... args) {
        StringBuilder sb = new StringBuilder();
        sb.append("*").append(args.length).append("\r\n");
        for (String arg : args) {
            sb.append("$").append(arg.length()).append("\r\n");
            sb.append(arg).append("\r\n");
        }
        return sb.toString();
    }

    @Test
    public void initFile2() {
        Long startTime = System.currentTimeMillis();
        String file = "d:\\d.txt";
        BufferedWriter w = null;
        StringBuilder sb = new StringBuilder();
        try {
            w = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), "utf-8"));
            for(int i=100000000 ;i < 100110000;i++){
            //for (int i = 1; i <= 100; i++) {
                if (i / 30000 == 0) {
                    w.flush();
                }
                sb.setLength(0);
                sb.append(this.getString("set", "u" + i, "name" + i));
//sb.append(this.getString("hmset", "usr" + i, "userid", "usr" + i, "username", "usrname" + i));
                w.append(sb.toString());
            }
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                w.flush();
                w.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
        long endTime = System.currentTimeMillis();
        System.out.println("耗时: "+(endTime - startTime)/1000+" s。");
    }
}

执行
[root@localhost redis]# cat d.txt |redis-cli –pipe
这里写图片描述

查看
127.0.0.1:6379>keys *

这里写图片描述

3.常见问题

ERR Protocol error: too big mbulk count string
Error writing to the server: Connection reset by peer
文件太大,和所分配的内存大小密切相关,内存太少则会导致文件太大导入失败

十四、安装多个服务

1.建立多个节点

类比tomcat:
1. tomcat集群,多个实例,a.修改server.xml3个端口8080,8005,8009;b.复制目录
2. redis集群,

关闭redis服务
[root@localhost src]# redis-cli #开启服务
127.0.0.1:6379>shutdown #正常关闭服务
[root@localhost src]#ps -ef |grep redis #查看服务是否存在

复制改端口无需再次安装

redis不用复制整个目录,只需要复制配置文件,启动时选择配置文件即可。

cd /usr/local/src/redis/redis3.2.11
cp redis.conf redis6379.conf
vi redis6379.conf
redis-server redis6379.conf
注意:启动后,会残留些数据,不完全,必须flushall清除掉。

redis启动方式不同,结果不同
1)redis-server 默认去找redis.conf文件,启动后可以远程访问 *:6379
2)redis-server redis6379.conf指定配置文件,启动后127.0.0.1:6379。只允许本地程序访问。

2.修改redis.conf

vim redis6379.conf

1. bind 127.0.0.1 限定访问的IP,注释掉所有的机器都可以连接
开放远程访问
redis.conf中bind默认绑定127.0.0.1,只有本地可以访问。
ps -ef |grep redis
root 2545 2532 005:51 pts/0 00:00:07 redis-server *:6379
# 假设开启了6380的服务(修改之前服务的状态)
root 2710 2674 006:14 pts/2 00:00:05 redis-server 127.0.0.1:6380

将bind 127.0.0.1注释掉,前面加个#即可

root 2545 2532 005:51 pts/0 00:00:07 redis-server *:6379
# 假设开启了6380的服务(修改之后服务的状态,多了个*)
root 2710 2674 006:14 pts/2 00:00:05 redis-server *:6380

变成两个*即可远程访问,可以看出默认的redis.conf和复制后的文件还是有差异的
2. protected-mode yes 早期redis默认false,不需密码;no,性能高(改为no)

3. daemonize no守护进程,默认no,部署时设置为yes,生产环境(改为yes)

4. 端口号port 6379 (这是第一个配置文件无需修改端口号)

5. #maxmemory < type >改为:maxmemory 200mb,在生产环境必须设置,默认没有设置,redis会吃掉所有内存。一般30%的物理内存。

3.复制多个redis.conf

[root@localhost redis-3.2.11]#cp redis6379.conf redis6380.conf
[root@localhost redis-3.2.11]#vim redis6380.conf
端口号改为6380(port 6380)

[root@localhost redis-3.2.11]#cp redis6379.conf redis6381.conf
[root@localhost redis-3.2.11]#vim redis6381.conf
端口号改为6380(port 6381)

4.启动多个redis实例

redis-server redis6379.conf &
(加个&让其后台启动)

查看是否启动
[root@localhost redis-3.2.11]#ps -ef |grep redis

启动
redis-server redis6380.conf &
redis-server redis6381.conf &

5.登录实例

redis-cli -h 127.0.0.1 -p 6379
redis-cli -p 6380
redis-cli -p 6381

(-h 127.0.0.1本机可以省略)

登录6380
redis-cli -p 6380
查看
127.0.0.1:6380>keys *
刚启动 发现里面有数据,(此时和6379一样)

5.为什么第二个实例有数据

1.redisxxx.conf,使用同一个备份文件dump.rdb
dump.rdb

2.使用第二个实例操作,它的数据会不会影响第一个实例?

a.不会,服务没关掉。(服务没有关掉的情况下多个实例之间互不影响)

b.如果服务重启,重启时会自动读取dump中的内容加载到内存。(服务重启加载的情况下会互相影响)

3.最好的方法在每个配置文件中把这个名字改掉,例:dump6379.rdb
redis.conf

6.打开端口

/sbin/iptables -I INPUT -p tcp –dport 6379 -j ACCEPT
/sbin/iptables -I INPUT -p tcp –dport 6380 -j ACCEPT
/sbin/iptables -I INPUT -p tcp –dport 6381 -j ACCEPT
/etc/rc.d/init.d/iptables save #修改生效
/etc/init.d/iptables status #查看配置

以上是centos6 端口号打开方式
CentOS7打开端口号:

开启永久端口号:
firewall-cmd –add-port=6379/tcp –permanent
这里把6379替换为需要开的端口号, –permanent是指永久的意思。

一行命令开多个端口号:
开启永久端口号:firewall-cmd –add-port=6379/tcp –permanent&&firewall-cmd –add-port=6380/tcp –permanent&&firewall-cmd –add-port=6381/tcp –permanent

查询端口号是否开启:
firewall-cmd –query-port=6379/tcp
查询端口号6379是否开启!

重启防火墙
service firewalld restart

查看服务状态
[root@localhost redis-3.2.11]#ps -ef |grep redis

7.三个实例,是一体的还是分开的?

示例1:

package redis;

import org.junit.Test;

import redis.clients.jedis.Jedis;

public class TestJedis{
    //用jedis来操作redis,set/get
    @Test    //操作一个节点
    public void redis(){
        //创建链接
        Jedis jedis = new Jedis("192.168.166.20",6379);
        jedis.set("name","tony");//调用redis的set命令
        jedis.close();//释放连接
    }
}

127.0.0.1:6379>flushall
OK
然后运行上述java代码

然后
127.0.0.1:6379>keys *
1)”name”
127.0.0.1:6379>get name
“tony”

1)java客户端程序jedis,各自连接,意义不大,各自分开的。
2)统一,不是覆盖所有,随机x

示例2:

package redis;

import java.util.List;

import org.junit.Test;

import redis.clients.jedis.Jedis;

public class TestJedis{
    //用jedis来操作redis,set/get
    @Test    //操作一个节点
    public void redis(){
        //创建链接
        Jedis jedis = new Jedis("192.168.166.20",6379);
        jedis.set("name","tony");//调用redis的set命令
        jedis.close();//释放连接
    }
        @Test    //分片
    public void shard(){
        List<JedisShardInfo> listInfo = new ArrayList<JedisShardInfo>();
        listInfo.add(new JedisShardInfo("192.168.166.20",6379));
        listInfo.add(new JedisShardInfo("192.168.166.20",6380));
        //listInfo.add(new JedisShardInfo("192.168.166.20",6381));
        ShardedJedis jedis = new ShardedJedis(listInfo);
        for(int i=0;i<100;i++){
            jedis.set("name"+i,"tony"+i);
        }
        jedis.close();
    }
}

上述代码运行后:
说明:多个节点共用(不会重复)

这个56;
这里写图片描述

这个44;
这里写图片描述

算法支持:hash一致性算法。麻省理工学院学员写的。基于hash取余算法又补充一些,目的:不能根本的解决hash的数据倾斜问题,但是得到了极大的缓解,甚至可以忽略不计。(海量)
这里写图片描述

证明4个问题:

1) 分片,统一还是各是各的?统一的。分布式内存数据库!
2) hash散列特性,不是完全平均,而是尽量平均
3) hash跟key名称有关系
nameX 56/44
tX 52/48 #将分片key名称改一下,发现比例又不一样了,说明跟key名称有关系(可以测试找一个分配均匀的)
4) hash跟n节点数量有关系

将上述代码第三个节点打开再运行(发现三个几点之间共用,同上:尽量均匀分配)

测试
先清空三个节点
127.0.0.1:6379>flushall
OK
先执行前两个节点,不删除
再执行三个节点
改变部分代码

for(int i=0;i<100;i++){
            //jedis.set("name"+i,"tony"+i);
        }
        String s = jedis.get("t88");
        System.out.println(s);
        jedis.close();

执行java代码,此时分不清是哪个节点的t88
把一个节点的t88值改为newt
再执行java代码,
newt
说明:写和读都按hash算法

部分数据迁移,而之前的数据就会成为僵尸数据,这个数据没法访问。
这个僵尸数据,根本不用管,LRU算法!(自动驱逐策略)

hash有数据倾斜问题

十五、Redis分片

1.访问redis的驱动包。

这里写图片描述
这里写图片描述
这里写图片描述

使用最为广泛的是Jedis和Redisson(官方推荐),在企业中采用最多的是Jedis。

Jedis官网地址:https://github.com/xetorthio/jedis

2.jedis示例

package redis;

import java.util.List;

import redis.clients.jedis.Jedis;

public class TestRedis {
    public static void main(String[] args) {
        //设置连接服务器IP地址和访问端口
        Jedis jedis = new Jedis("192.168.115.115",6379);

        //单个值
        //jedis.set("test", "456789");              //设置值
        //System.out.println(jedis.get("test"));        //获取值

        //多个值
        //jedis.mset("test1","1","test2","2");
        List<String> oList = jedis.mget("test1","test2");
        for(String s : oList){
            System.out.println(s);
        }

        jedis.close();  //关闭
    }
}

命令窗口:
127.0.0.1:6379> keys *
1) “bomb”
127.0.0.1:6379> get bomb
“tnt”
127.0.0.1:6379>

3.连接池JedisPool创建jedis连接

package cn.redis;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

public class JedisPoolDemo {

    public static void main(String[] args) {
        // 构建连接池配置信息
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        // 设置最大连接数
        jedisPoolConfig.setMaxTotal(200);

        // 构建连接池
        JedisPool jedisPool = new JedisPool(jedisPoolConfig, "127.0.0.1", 6379);

        // 从连接池中获取连接
        Jedis jedis = jedisPool.getResource();

        // 读取数据
        System.out.println(jedis.get("bomb"));

        // 将连接还回到连接池中
        jedisPool.returnResource(jedis);

        // 释放连接池
        jedisPool.close();
    }
}
|

4.分片ShardedJedisPool

实现分布式缓存,Redis多个节点的透明访问

    @Test   //分片
    public void shard(){


        //构造各个节点链接信息,host和port
        List<JedisShardInfo> infoList = new ArrayList<JedisShardInfo>();
        JedisShardInfo info1 = new JedisShardInfo("192.168.163.200",6379);
        //info1.setPassword("123456");
        infoList.add(info1);
        JedisShardInfo info2 = new JedisShardInfo("192.168.163.200",6380);
        infoList.add(info2);
        JedisShardInfo info3 = new JedisShardInfo("192.168.163.200",6381);
        infoList.add(info3);

        //分片jedis

        JedisPoolConfig config = new JedisPoolConfig();
        config.setMaxTotal(500);    //最大链接数

        ShardedJedisPool pool = new ShardedJedisPool(config, infoList);
        //ShardedJedis jedis = new ShardedJedis(infoList);
        ShardedJedis jedis = pool.getResource();    //从pool中获取
        for(int i=0;i<10;i++){
            jedis.set("n"+i, "t"+i);
        }
        System.out.println(jedis.get("n9"));
        jedis.close();
    }

5.数据倾斜

3个节点,可以看到n为key时会发生数据倾斜,而换成text就缓解很多。
redis CRC16
name+I 43/29/27 38/26/35
text+I 29/34/36 28/35/36

CRC16hash测试

package redis;

import java.util.ArrayList;
import java.util.List;

import org.junit.Test;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.JedisShardInfo;
import redis.clients.jedis.ShardedJedis;
import redis.clients.jedis.ShardedJedisPool;

public class Crc16Mod {

    @Test
    public void runCrc() {
        for (int i = 1; i < 100; i++) {
            System.out.println(this.getCrc(("name" + i).getBytes()) % 3);
        }
    }

    private static Integer getCrc(byte[] data) {
        int high;
        int flag;

        // 16位寄存器,所有数位均为1
        int wcrc = 0xffff;
        for (int i = 0; i < data.length; i++) {
            // 16 位寄存器的高位字节
            high = wcrc >> 8;
            // 取被校验串的一个字节与 16 位寄存器的高位字节进行“异或”运算
            wcrc = high ^ data[i];

            for (int j = 0; j < 8; j++) {
                flag = wcrc & 0x0001;
                // 把这个 16 寄存器向右移一位
                wcrc = wcrc >> 1;
                // 若向右(标记位)移出的数位是 1,则生成多项式 1010 0000 0000 0001 和这个寄存器进行“异或”运算
                if (flag == 1)
                    wcrc ^= 0xa001;
            }
        }

        // return Integer.toHexString(wcrc);
        return wcrc;
    }
}

6.原理

这里写图片描述
在分布式集群中,对机器的添加删除,或者机器故障后自动脱离集群这些操作是分布式集群管理最基本的功能。如果采用常用的hash(object)%N算法,那么在有机器添加或者删除后,很多原有的数据就无法找到了,这样严重的违反了单调性原则。

7.hash一致性算法

一致性哈希算法在1997年由麻省理工学院提出。
hash取余产生的问题:新增节点、删除节点会让绝大多数的缓存失效,除了导致性能骤降外很有可能会压垮后台服务器。
解决点一:
集群中节点挂掉或新增节点的时候,要对已有节点的影响降到最小。其解决思路,就是对缓存的object和Node使用同一个hash函数(实际不需要完全一致,但至少保证产生的hash空间相同),让他们映射到同一个hash空间中去,当然这很容易实现,因为大多数的hash函数都是返回uint32类型,其空间即为1~232 232-1(2^32 = 4 294 967 296,近43亿)。然后各个Node就将整个hash空间分割成多个interval空间,然后对于每个缓存对象object,都按照顺时针方向遇到的第一个Node负责缓存它。通过这种方法,在新增加Node和删除Node的时候,只会对顺时针方向遇到的第一个Node负责的空间造成影响,其余的空间都仍然有效。
这里写图片描述

虽然虚拟并不能百分百的解决缓存命中失效的问题,但把问题缩小化,这样影响面小,即使缓存失效,数据库也能承受起用户的负载,从而稳定过渡。

8.扩展:如何缩短key?

这么多对象的代表,我们熟知在spring框架中,我们保存spring的上下文时,是一个很长的KEY,那这样的KEY很多时,会导致内存过多的占用,同时这种自定义规则,也很难保证不冲突。如何找到一个规则能让他们避免重复呢?
md5/hashCode

9.两个node节点测试

6379节点:
127.0.0.1:6379> keys *
1) “bomb_79”
2) “bomb_54”
3) “bomb_66”
4) “bomb_76”
5) “bomb_44”
6) “bomb_11”
7) “bomb_6”
8) “bomb_64”
9) “bomb_84”
10) “bomb_72”
11) “bomb_47”
12) “bomb_40”
13) “bomb_86”
14) “bomb_57”
15) “bomb_61”
16) “bomb_31”
17) “bomb_73”
18) “bomb_81”
19) “bomb_74”
20) “bomb_28”
21) “bomb_90”
22) “bomb_5”
23) “bomb_23”
24) “bomb_65”
25) “bomb_97”
26) “bomb_9”
27) “bomb_71”
28) “bomb_77”
29) “bomb_48”
30) “bomb_24”
31) “bomb_94”
32) “bomb_34”
33) “bomb_0”
34) “bomb_89”
35) “bomb_49”
36) “bomb_62”
37) “bomb_35”
38) “bomb_63”
39) “bomb_39”
40) “bomb_37”
41) “bomb_53”
42) “bomb_67”
43) “bomb_91”
44) “bomb_82”
45) “bomb_87”
46) “bomb_38”
47) “bomb_92”
48) “bomb_25”
49) “bomb_20”
50) “bomb_83”
51) “bomb_18”
52) “bomb_69”
53) “bomb_50”
127.0.0.1:6379>
6380节点:
D:\javaenv\redis\node6380>redis-cli -p 6380
127.0.0.1:6380> keys *
1) “bomb_56”
2) “bomb_78”
3) “bomb_42”
4) “bomb_33”
5) “bomb_3”
6) “bomb_95”
7) “bomb_21”
8) “bomb_22”
9) “bomb_14”
10) “bomb_36”
11) “bomb_51”
12) “bomb_2”
13) “bomb_16”
14) “bomb_4”
15) “bomb_12”
16) “bomb_99”
17) “bomb_30”
18) “bomb_27”
19) “bomb_70”
20) “bomb_10”
21) “bomb_13”
22) “bomb_60”
23) “bomb_59”
24) “bomb_17”
25) “bomb_19”
26) “bomb_41”
27) “bomb_1”
28) “bomb_15”
29) “bomb_96”
30) “bomb_75”
31) “bomb_46”
32) “bomb_43”
33) “bomb_88”
34) “bomb_7”
35) “bomb_85”
36) “bomb_58”
37) “bomb_45”
38) “bomb_93”
39) “bomb_26”
40) “bomb_52”
41) “bomb_68”
42) “bomb_8”
43) “bomb_29”
44) “bomb_32”
45) “bomb_98”
46) “bomb_80”
47) “bomb_55”
127.0.0.1:6380>

十六、jedis和Spring整合访问redis

1.整合步骤

  • 引入依赖
  • 整合配置文件applicationContext-redis.xml
  • 伪service
  • 注入伪service

2.配置文件

applicationContext-redis.xml
<!-- 定义集群连接池 -->
<bean id="shardedJedisPool" class="redis.clients.jedis.ShardedJedisPool" destroy-method="close">
    <!-- 第一个参数 -->
    <constructor-arg index="0" ref="jedisPoolConfig"/>
    <constructor-arg index="1">
        <list>
            <!-- 第一个节点 -->
            <bean class="redis.clients.jedis.JedisShardInfo">
                <constructor-arg index="0" value="${redis.node1.ip}"/>
                <constructor-arg type="int" index="1" value="${redis.node1.port}"/>
            </bean>
            <!-- 第二个节点 -->
            <bean class="redis.clients.jedis.JedisShardInfo">
                <constructor-arg index="0" value="${redis.node2.ip}"/>
                <constructor-arg type="int" index="1" value="${redis.node2.port}"/>
            </bean>
            <!-- 第三个节点 -->
            <bean class="redis.clients.jedis.JedisShardInfo">
                <constructor-arg index="0" value="${redis.node3.ip}"/>
                <constructor-arg type="int" index="1" value="${redis.node3.port}"/>
            </bean>
        </list>
    </constructor-arg>
</bean>

redis.properties
redis.maxTotal=50
redis.node1.ip=127.0.0.1
redis.node1.port=6379
#redis.node2.ip=127.0.0.1
#redis.node2.port=6380

3.伪Service

package com.jt.manage.service;

import org.springframework.beans.factory.annotation.Autowired;

import redis.clients.jedis.ShardedJedis;
import redis.clients.jedis.ShardedJedisPool;

public class RedisService {
    @Autowired
    private ShardedJedisPool shardedJedisPool;

    //保存数据到redis中
    public String set(String key, String value){
        ShardedJedis shardedJedis = null;

        try{
            // 从连接池中获取到jedis分片对象
            shardedJedis = shardedJedisPool.getResource();
            return shardedJedis.set(key, value);
        } catch (Exception e){
            e.printStackTrace();
        } finally {
            if (null != shardedJedis){
                //关闭,检测连接是否有效,有效则放回到连接池中,无效则重置状态
                shardedJedis.close();
            }
        }
        return null;
    }

    //从redis获取数据
    public String get(String key){
        ShardedJedis shardedJedis = null;

        try{
            // 从连接池中获取到jedis分片对象
            shardedJedis = shardedJedisPool.getResource();
            return shardedJedis.get(key);
        } catch (Exception e){
            e.printStackTrace();
        } finally {
            if (null != shardedJedis){
                //关闭,检测连接是否有效,有效则放回到连接池中,无效则重置状态
                shardedJedis.close();
            }
        }
        return null;
    }
}

4.*重构RedisService

两个方法有很多重复代码,如何消除呢?
类似js中的回调来解决。目的简化代码,抽取公用逻辑。

com.jt.common.service.Function<E, T>
package com.jt.common.service;

public interface Function<E, T> {

    public T execute(E e);

}
com.jt.common.service.RedisService
package com.jt.manage.service;

import org.springframework.beans.factory.annotation.Autowired;

import redis.clients.jedis.ShardedJedis;
import redis.clients.jedis.ShardedJedisPool;

public class RedisService {
    @Autowired
    private ShardedJedisPool shardedJedisPool;

    //保存数据到redis中
    private <E,T>T execute(Function<ShardedJedis,T> function){
        ShardedJedis shardedJedis = null;

        try{
            // 从连接池中获取到jedis分片对象
            shardedJedis = shardedJedisPool.getResource();
            return function.execute(shardedJedis);
        } catch (Exception e){
            e.printStackTrace();
        } finally {
            if (null != shardedJedis){
                //关闭,检测连接是否有效,有效则放回到连接池中,无效则重置状态
                shardedJedis.close();
            }
        }
        return null;
    }

    //保存数据到redis中
    public String set(final String key, final String value){
        return this.execute(new Function<ShardedJedis, String>() {
            @Override
            public String execute(ShardedJedis shardedJedis) {
                return shardedJedis.set(key, value);
            }
        });
    }

    //保存数据到redis中,并设置生存时间
    public String set(final String key, final String value, final Integer seconds){
        return this.execute(new Function<ShardedJedis, String>() {
            @Override
            public String execute(ShardedJedis shardedJedis) {
                String result = shardedJedis.set(key, value);
                shardedJedis.expire(key, seconds);  //设置生存时间
                return result;
            }
        });
    }

    //从redis获取数据
    public String get(final String key){
        return this.execute(new Function<ShardedJedis, String>() {
            @Override
            public String execute(ShardedJedis shardedJedis) {
                return shardedJedis.get(key);
            }
        });
    }

    //设置key的生存时间,单位:秒
    public Long expire(final String key, final Integer seconds){
        return this.execute(new Function<ShardedJedis, Long>(){
            @Override
            public Long execute(ShardedJedis shardedJedis) {
                return shardedJedis.expire(key, seconds);
            }
        });
    }

    //删除key
    public Long del(final String key){
        return this.execute(new Function<ShardedJedis, Long>(){
            @Override
            public Long execute(ShardedJedis shardedJedis) {
                return shardedJedis.del(key);
            }
        });
    }
}

5.缓存的作用

缓存在项目中或者系统中,分担底层数据库的压力。缓存是不能影响业务逻辑的。比如说缓存服务器宕机了,能说因为缓存服务器宕机了,业务走不下去了。这种理由当然不行。缓存一定不能影响正常的业务逻辑的执行。

未完待续。。。。。。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值