前面有写过一篇Redis集群实战详解,主要是针对部署redis集群实战操作。可参考:

本篇主要是针对Redis的数据类型、Jedis-Api 、(持久化、事务)的原理、集群(哨兵、主从)原理及秒杀案例的剥析-所以更加详细篇幅更长(参考某钩课程)。

1、概述

1.1 Redis入门介绍

  • 互联网需求的3高

    高并发,高可扩,高性能
  • Redis 是一种运行速度很快,并发性能很强,并且运行在内存上的NoSql(not only sql)数据库 NoSQL数据库 和 传统数据库
    1. 相比的优势 NoSQL数据库无需事先为要存储的数据建立字段,随时可以存储自定义的数据格式。
    2.而在关系数据库里,增删字段是一件非常麻烦的事情。如果是非常大数据量的表,增加字段 简直就是一个噩梦
     
  • Redis的常用使用场景
    1.缓存,毫无疑问这是Redis当今最为人熟知的使用场景。在提升服务器性能方面非常有效;一 些频繁被访问的数据,经常被访问的数据如果放在关系型数据库,每次查询的开销都会很 大,而放在redis中,因为redis 是放在内存中的可以很高效的访问
    2.排行榜,在使用传统的关系型数据库(mysql oracle 等)来做这个事儿,非常的麻烦,而利 用Redis的SortSet(有序集合)数据结构能够简单的搞定;
    3.计算器/限速器,利用Redis中原子性的自增操作,我们可以统计类似用户点赞数、用户访问 数等,这类操作如果用MySQL,频繁的读写会带来相当大的压力;限速器比较典型的使用场 景是限制某个用户访问某个API的频率,常用的有抢购时,防止用户疯狂点击带来不必要的压 力;
    4.好友关系,利用集合的一些命令,比如求交集、并集、差集等。可以方便搞定一些共同好 友、共同爱好之类的功能;
    5.简单消息队列,除了Redis自身的发布/订阅模式,我们也可以利用List来实现一个队列机制, 比如:到货通知、邮件发送之类的需求,不需要高可靠,但是会带来非常大的DB压力,完全 可以用List来完成异步解耦;
    6.Session共享,以jsp为例,默认Session是保存在服务器的文件中,如果是集群服务,同一个 用户过来可能落在不同机器上,这就会导致用户频繁登陆;采用Redis保存Session后,无论 用户落在那台机器上都能够获取到对应的Session信息。

1.3 Redis/Memcache/MongoDB对比

1.3.1 Redis和Memcache

  • Redis和Memcache都是内存数据库。不过memcache还可用于缓存其他东西,例如图片、视频等 等。
  • memcache 数据结构单一kv,redis 更丰富一些,还提供 list,set, hash 等数据结构的存储,有 效的减少网络 IO 的次数
  • 虚拟内存–Redis当物理内存用完时,可以将一些很久没用到的value交换到磁盘
  • 存储数据安全–memcache挂掉后,数据没了(没有持久化机制);redis可以定期保存到磁盘(持 久化)
  • 灾难恢复–memcache挂掉后,数据不可恢复; redis数据丢失后可以通过RBD或AOF恢复

memcache学习参考:

1.3.2 Redis和MongoDB

  • redis和mongodb并不是竞争关系,更多的是一种协作共存的关系。
  • mongodb本质上还是硬盘数据库,在复杂查询时仍然会有大量的资源消耗,而且在处理复杂逻辑 时仍然要不可避免地进行多次查询。
  • 这时就需要redis或Memcache这样的内存数据库来作为中间层进行缓存和加速。
  • 比如在某些复杂页面的场景中,整个页面的内容如果都从mongodb中查询,可能要几十个查询语 句,耗时很长。如果需求允许,则可以把整个页面的对象缓存至redis中,定期更新。这样 mongodb和redis就能很好地协作起来

MongoDB学习参考:

1.4 分布式数据库CAP原理

1.4.1 CAP简介

  • 传统的关系型数据库事务具备ACID: A:原子性 C:一致性 I:独立性 D:持久性
  • 分布式数据库的CAP:
    1、C:强一致性,
    “all nodes see the same data at the same time”,即更新操作成功并返回客户端后,所 有节点在同一时间的数据完全一致,这就是分布式的一致性。一致性的问题在并发系统 中不可避免,对于客户端来说,一致性指的是并发访问时更新过的数据如何获取的问 题。从服务端来看,则是更新如何复制分布到整个系统,以保证数据最终一致。

    2、A(Availability):高可用性,可用性指“Reads and writes always succeed”,即服务一直可用,而且要是正常的响应 时间。好的可用性主要是指系统能够很好的为用户服务,不出现用户操作失败或者访问 超时等用户体验不好的情况

    3、P(Partition tolerance):分区容错性,即分布式系统在遇到某节点或网络分区故障时,仍然能够对外提供满足一致性或可用性 的服务。分区容错性要求能够使应用虽然是一个分布式系统,而看上去却好像是在一个可以运转 正常的整体。比如现在的分布式系统中有某一个或者几个机器宕掉了,其他剩下的机器 还能够正常运转满足系统需求,对于用户而言并没有什么体验上的影响。

1.4.2 CAP理论

  • CAP理论提出就是针对分布式数据库环境的,所以,P这个属性必须容忍它的存在,而且是必须具 备的。
  • 因为P是必须的,那么我们需要选择的就是A和C。
  • 大家知道,在分布式环境下,为了保证系统可用性,通常都采取了复制的方式,避免一个节点损 坏,导致系统不可用。那么就出现了每个节点上的数据出现了很多个副本的情况,而数据从一个节 点复制到另外的节点时需要时间和要求网络畅通的,所以,当P发生时,也就是无法向某个节点复 制数据时,这时候你有两个选择:
    1、选择可用性 A,此时,那个失去联系的节点依然可以向系统提供服务,不过它的数据就不能 保证是同步的了(失去了C属性)。
    2、选择一致性C,为了保证数据库的一致性,我们必须等待失去联系的节点恢复过来,在这个过 程中,那个节点是不允许对外提供服务的,这时候系统处于不可用状态(失去了A属性)。
  • 选择一致性C,为了保证数据库的一致性,我们必须等待失去联系的节点恢复过来,在这个过 程中,那个节点是不允许对外提供服务的,这时候系统处于不可用状态(失去了A属性)。

1.4.3 CAP总结

  • 分区是常态,不可避免,三者不可共存
     
  • 可用性和一致性是一对冤家
    一致性高,可用性低。
    一致性低,可用性高
     
  • 因此,根据 CAP 原理将 NoSQL 数据库分成了满足 CA 原则、满足 CP 原则和满足 AP 原则三 大 类:
    1、CA - 单点集群,满足一致性,可用性的系统,通常在可扩展性上不太强大。
    2、CP - 满足一致性,分区容忍性的系统,通常性能不是特别高。
    3、AP - 满足可用性,分区容忍性的系统,通常可能对一致性要求低一些。

2.下载与安装(略)

详情可查看:

3. 使用Redis

命令

语法

描述

redis-benchmark

在redis目录下执行

测试性能

dbsize

数据库键的数量

flushdb

清空当前库

flushall

清空所有(16个)库,慎用!

exists key

exists key

判断某个key是否存在

move  key  db 

move  key  db 

移动(剪切,粘贴)键到几号库

ttl key

ttl key

查看键还有多久过期(-1永不过期,-2已失效)

expire key秒

expire key秒

为键设置过期时间(生命倒计时)

type  key

type  key

查看键的数据类型

3.1 五大数据类型

string,hash,list,zset,set

操作文档: Redis 命令参考 — Redis 命令参考 (redisdoc.com)

3.1.1 字符串String

1、基础命令:get、set、del、append、strlen

redis核心技术与实战下载 redis技术分享_数据

命令

描述

set  key value

保存数据

get key 

获取数据

del key 

删除数据

append key value

给某个键的值追加数据,返回长度

strlen key

计算长度

2、incr、decr、incrby、decrby:加减操作,操作的必须是数字类型

redis核心技术与实战下载 redis技术分享_redis_02

命令

描述

incr key

自增+1

decr key

自减-1

incrby key value

给key的值加value

decrby key value

给key的值减value

 3、getrange/setrange:类似between...and... 

redis核心技术与实战下载 redis技术分享_数据_03

命令

描述

getrange key start end

查询k1全部的值,start=0 从第一位开始,end=-1表示获取全部的值,end = 3表示获取下标0到3的值

setrange key start value

替换key的值,从下标start开始 替换为value

 4、setex、setnx 

set with expir:添加数据的同时设置生命周期

set if not exist:添加数据的时候判断是否已经存在,防止已存在的数据被覆盖掉

命令

描述

setex key time(秒) value

添加数据设置生命周期

setnx key value

添加数据的时候判断是否已经存在,防止已存在的数据被覆盖掉,如果k1存在返回0(添加失败)。

5、mset/mget/msetnx、 getset:先get后set

m:more更多

redis核心技术与实战下载 redis技术分享_Redis_04

命令

描述

mset key value [key,value....]

插入多个键值对

get key [key1....keyn]

获取多个键值对

msetnx key value [key,value....]

判断是否存在,不存在则插入,存在则插入失败

getset  key value 

先获取数据,如果为null 就插入

 3.1.2 列表List

push和pop,类似机枪AK47:push,压子弹,pop,射击出子弹

1、lpush/rpush/lrange、lpop/rpop:移除第一个元素(上左下右)

l:left 自左向右→添加 (从上往下添加) r:right 自右向左←添加(从下往上添加)

redis核心技术与实战下载 redis技术分享_nosql_05

命令

描述

lpush key value [value ...]

从左往右插入key集合value值

lrange key start stop

从左往右取出下标从start到stop的key集合值

rpush key value [value ...]

从右往左插入key集合value值

lpop key

左(上)边移除第一个元素

rpop key

右(下)边移除第一个元素

2、lindex、llen、lrem

lindex:根据下标查询元素(从左向右,自上而下)

llen:返回集合长度

lrem:删除n个value

redis核心技术与实战下载 redis技术分享_数据_06

redis核心技术与实战下载 redis技术分享_redis核心技术与实战下载_07

命令

描述

lindex key index 

查看key集合的指定索引的值

llen key

查看key集合的长度

lrem key count value

删除key集合中的count个value值

 3、ltrim/rpoplpush/lset/linsert

ltrim:截取指定范围的值,别的全扔掉 

rpoplpush:从一个集合搞一个元素到另一个集合中(右出一个,左进一个)

lset:改变某个下标的某个值

linsert:插入元素(指定某个元素之前/之后) 

redis核心技术与实战下载 redis技术分享_Redis_08

 

redis核心技术与实战下载 redis技术分享_redis核心技术与实战下载_09

redis核心技术与实战下载 redis技术分享_Redis_10

redis核心技术与实战下载 redis技术分享_数据_11

redis核心技术与实战下载 redis技术分享_nosql_12

语法

描述

 ltrim key begindex endindex

截取指定范围的值,别的全扔掉 

lset key index value

改变某个下标的某个值

linsert key before/after oldvalue newvalue 

插入元素(指定某个元素之前/之后) 

rpoplpush source(key) target (key)

从一个集合搞一个元素到另一个集合中(右出一个,左进一个)

性能总结:类似添加火车皮一样,头尾操作效率高,中间操作效率惨;

3.1.3 集合Set

和java中的set特点类似,不允许重复

1、sadd/smembers/sismember:添加/查看/判断是否存在

redis核心技术与实战下载 redis技术分享_Redis_13

命令

描述

sadd key value[value...]

添加元素(自动排除重复元素)

smembers key

查询key集合

sismember key member

key集合是否存在member,返回0 or 1表示布尔

 2、scard/srem/srandmember/spop/smove

scard:获得集合中的元素个数

srem:删除集合中的元素

srandmember: 从集合中随机获取几个元素

spop:随机出栈(移除)

smove:移动元素:将key1某个值赋值给key2

redis核心技术与实战下载 redis技术分享_nosql_14

redis核心技术与实战下载 redis技术分享_redis_15

命令

描述

scard key

获得集合中的元素个数

srem key value[value...]

删除集合中的元素

srandmember key [count]

从集合中随机获取几个元素

spop key [count]

随机出栈(移除),count表示个数

smove source(key1) target(key2) member

移动元素:将key1某个值赋值给key2

 3、数学集合类

交集:sinter

并集:sunion

差集:sdiff

redis核心技术与实战下载 redis技术分享_数据_16

命令

描述

sinter key1 key2

交集(key1 key2重复的元素)

sunion key1 key2

并集(将key1和key2的所有元素合并起来,排除重复)

sdiff key1 key2

差集(key1 中存在,在key2中不存在)

 3.1.4 哈希Hash

类似java里面的Map KV模式不变,但V是一个键值对

1、hset/hget/hmset/hmget/hgetall/hdel:添加/得到/多添加/多得到/得到全部/删除属性

127.0.0.1:6379> hset user id 1001    #添加user,值为id=1001 
(integer) 0
127.0.0.1:6379> hget user id     # 获取user,必须指明具体的字段
"1001"
127.0.0.1:6379> hmset user id 1001 name tom age 23  # 添加user,属性一堆
OK
127.0.0.1:6379> hmget user id name age    # 获取user,id name age属性
1) "1001"
2) "tom"
3) "23"
127.0.0.1:6379> hgetall user    # 获取user全部信息
1) "id"
2) "1001"
3) "name"
4) "tom"
5) "age"
6) "23"
127.0.0.1:6379> hdel user age    # 删除user  age属性
(integer) 1    #删除成功
127.0.0.1:6379> hgetall user
1) "id"
2) "1001"
3) "name"
4) "tom"

2、hlen:返回元素的属性个数

127.0.0.1:6379> hgetall user
1) "id"
2) "1001"
3) "name"
4) "tom"
127.0.0.1:6379> hlen user  #获取元素的属性个数
(integer) 2   #user属性的数量,id name 
 

3、hexists:判断元素是否存在某个属性

 127.0.0.1:6379> hexists user name  #user中是否存在属性name
(integer) 1   #存在
127.0.0.1:6379> hexists user id
(integer) 1  #存在

4、hkeys/hvals:获得属性的所有key/获得属性的所有value

127.0.0.1:6379> hkeys user   #获取user所有的属性名
1) "id"
2) "name"
127.0.0.1:6379> hvals user  #获取user所有属性的值
1) "1001"
2) "tom"

5、hincrby/hincrbyfloat:自增(整数)/自增(小数)

127.0.0.1:6379> hincrby user id 3   # 自增整数 2 
(integer) 1004
127.0.0.1:6379> hincrbyfloat user id 1.1    #自增小数 1.1
"1005.09999999999999998"
127.0.0.1:6379> hvals user   
1) "1005.09999999999999998"
2) "tom"
 

6、hsetnx:添加的时候,先判断是否存在

127.0.0.1:6379> hsetnx user name tom  #添加时,判断name 是否存在
(integer) 0   #添加失败,因为name 已经存在
127.0.0.1:6379> hsetnx user sex 男
(integer) 1
127.0.0.1:6379> hvals user
1) "1005.09999999999999998"
2) "tom"
3) "\xe7\x94\xb7"     #可以添加中文,但是现实为乱码
 

3.1.5 有序集合Zset

真实需求:

充10元可享vip1; 充20元可享vip2; 充30元可享vip3;

1、zadd/zrange (withscores):添加/查询

127.0.0.1:6379> zadd zset01 10 vip1 20 vip2 30 vip3 40 vip4 50 vip5
(integer) 5
127.0.0.1:6379> zrange zset01 0  -1
1) "vip1"
2) "vip2"
3) "vip3"
4) "vip4"
5) "vip5"
127.0.0.1:6379> zrange zset01 0  -1 withscores
 1) "vip1"
 2) "10"
 3) "vip2"
 4) "20"
 5) "vip3"
 6) "30"
 7) "vip4"
 8) "40"
 9) "vip5"
10) "50"

 2、zrangebyscore:模糊查询

( : 不包含

limit:跳过几个截取几个

127.0.0.1:6379> zrangebyscore zset01 20 40 withscores  #查询20 - 40的值  包含40
1) "vip2"
2) "20"
3) "vip3"
4) "30"
5) "vip4"
6) "40"
127.0.0.1:6379> zrangebyscore zset01 20 (40 withscores #查询20 - 40的值  不包含40
1) "vip2"
2) "20"
3) "vip3"
4) "30"
127.0.0.1:6379> zrangebyscore zset01 20 (40 withscores limit 1 1   #查询20 - 40的值  不包含40 跳过前1个 取 1个
1) "vip3"
2) "30"
 

3、zrem:删除元素

127.0.0.1:6379> zrem zset01 vip5   #删除元素 vip5
(integer) 1
127.0.0.1:6379> zrange zset01 0 -1 withscores
1) "vip1"
2) "10"
3) "vip2"
4) "20"
5) "vip3"
6) "30"
7) "vip4"
8) "40"

4、zcard/zcount/zrank/zscore:集合长度/范围内元素个数/得元素下标/通过值得分数

127.0.0.1:6379> zcard zset01 # 集合中元素的个数

(integer) 4

127.0.0.1:6379> zcount zset01 20 30 # 分数在20~40之间,共有几个元素

(integer) 2

127.0.0.1:6379> zrank zset01 vip3 # vip3在集合中的下标(从上向下)

(integer) 2

127.0.0.1:6379> zscore zset01 vip2 # 通过元素获得对应的分数

"20"

5、zrevrank:逆序找下标(从下向上)

127.0.0.1:6379> zrevrank zset01 vip3
(integer) 1

6、zrevrange:逆序查询

127.0.0.1:6379> zrevrange zset01 0 -1
1) "vip4"
2) "vip3"
3) "vip2"
4) "vip1"
127.0.0.1:6379> zrange zset01 0 -1
1) "vip1"
2) "vip2"
3) "vip3"
4) "vip4"
 

7、zrevrangebyscore:逆序范围查找

127.0.0.1:6379> zrevrangebyscore zset01 40 (10 withscores limit 1 1   #逆序查询分数在40 - 10 不包含10 跳过1个 取1个,注意先写大值 再写小值
1) "vip3"
2) "30"

3.2 持久化

3.3.1 RDB

Redis DataBase

  • 在指定的时间间隔内,将内存中的数据集的快照写入磁盘;
  • 默认保存在/usr/local/bin中,文件名dump.rdb;

3.3.1.1 自动备份

  • redis是内存数据库,当我们每次用完redis,关闭linux时,按道理来说,内存释放,redis中的数 据也会随之消失
  • 为什么我们再次启动redis的时候,昨天的数据还在,并没有消失呢?
  • 正是因为,每次关机时,redis会自动将数据备份到一个文件中 :/usr/local/bin/dump.rdb
  • 接下来我们就来全方位的认识 自动备份机制

1. 默认的自动备份策略不利于我们测试,所以修改redis.conf文件中的自动备份策略

vim redis.conf 

/SNAP   #搜索

save 900 1 # 900秒内,至少变更1次,才会自动备份

save 120 10 # 120秒内,至少变更10次,才会自动备份

save 60 10000 # 60秒内,至少变更10000次,才会自动备份

当然如果你只是用Redis的缓存功能,不需要持久化,那么你可以注释掉所有的 save 行来停用保 存功能。可以直接一个空字符串来实现停用:save ""

2. 使用shutdown模拟关机 ,关机之前和关机之后,对比dump.rdb文件的更新时间

注意:当我们使用shutdown命令,redis会自动将数据库备份,所以,dump.rdb文件创建时间更 新了

3. 开机启动redis,我们要在120秒内保存10条数据,再查看dump.rdb文件的更新时间(开两个终端 窗口,方便查看)

4. 120秒内保存10条数据这一动作触发了备份指令,目前,dump.rdb文件中保存了10条数据,将 dump.rdb拷贝一份dump10.rdb,此时两个文件中都保存10条数据

5. 既然有数据已经备份了,那我们就肆无忌惮的将数据全部删除flushall,再次shutdown关机

6. 再次启动redis,发现数据真的消失了,并没有按照我们所想的 将dump.rdb文件中的内容恢复到 redis中。为什么?

因为,当我们保存10条以上的数据时,数据备份起来了; 然后删除数据库,备份文件中的数据,也没问题; 但是,问题出在shutdown上,这个命令一旦执行,就会立刻备份,将删除之后的空数据库 生成备份文件,将之前装10条数据的备份文件覆盖掉了。所以,就出现了上图的结果。自动 恢复失败。 怎么解决这个问题呢?要将备份文件再备份

7. 将dump.rdb文件删除,将dump10.rdb重命名为dump.rdb

8. 启动redis服务,登录redis,数据10条,全部恢复!

3.3.1.2 手动备份

  • 之前自动备份,必须更改好多数据,例如上边,我们改变了十多条数据,才会自动备份;
  • 现在,我只保存一条数据,就想立刻备份,应该怎么做?
  • 每次操作完成,执行命令 save 就会立刻备份

3.3.1.3 与RDB相关的配置

  • stop-writes-on-bgsave-error:进水口和出水口,出水口发生故障与否

        yes:当后台备份时候反生错误,前台停止写入

        no:不管死活,就是往里怼

  • rdbcompression:对于存储到磁盘中的快照,是否启动LZF压缩算法,一般都会启动,因为这点 性能,多买一台电脑,完全搞定N个来回了。
    yes:启动 no:不启动(不想消耗CPU资源,可关闭)
  • rdbchecksum:在存储快照后,是否启动CRC64算法进行数据校验;
    开启后,大约增加10%左右的CPU消耗; 如果希望获得最大的性能提升,可以选择关闭;
     
  • dbfilename:快照备份文件名字
  • dir:快照备份文件保存的目录,默认为当前目录

 优势and劣势

  • 优:适合大规模数据恢复,对数据完整性和一致行要求不高;
  • 劣:一定间隔备份一次,意外down掉,就失去最后一次快照的所有修改

3.3.2 AOF

Append Only File

  • 以日志的形式记录每个写操作;
  • 将redis执行过的写指令全部记录下来(读操作不记录);
  • 只许追加文件,不可以改写文件;
  • redis在启动之初会读取该文件从头到尾执行一遍,这样来重新构建数据;

3.3.2.1 开启AOF

1. 为了避免失误,最好将redis.conf总配置文件备份一下,然后再修改内容如下:

redis核心技术与实战下载 redis技术分享_redis_17

2. 重新启动redis,以新配置文件启动

3. 连接redis,加数据,删库,退出

4. 查看当前文件夹多一个aof文件,看看文件中的内容,保存的都是

文件中最后一句要删除,否则数据恢复不了

编辑appendonly.aof 这个文件,最后要 :wq! 强制执行

5. 只需要重新连接,数据恢复成功

3.3.2.2 共存?谁优先?

我们查看redis.conf文件,AOF和RDB两种备份策略可以同时开启,那系统会怎样选择?

  • 1. 动手试试,编辑appendonly.aof,胡搞乱码,保存退出
  • 2. 启动redis 失败,所以是AOF优先载入来恢复原始数据!因为AOF比RDB数据保存的完整性更高!
  • 3. 修复AOF文件,杀光不符合redis语法规范的代码

reids-check-aof --fix appendonly.aof

3.3.2.3 与AOF相关的配置

redis核心技术与实战下载 redis技术分享_nosql_18

3.3.3 总结(如何选择?)

RDB:只用作后备用途,建议15分钟备份一次就好

AOF: 在最恶劣的情况下,也只丢失不超过2秒的数据,数据完整性比较高,但代价太大,会带来持 续的IO 对硬盘的大小要求也高,默认64mb太小了,企业级最少都是5G以上;

3.4 事务

  • 可以一次执行多个命令,是一个命令组,一个事务中,所有命令都会序列化(排队),不会被插 队;
  • 一个队列中,一次性,顺序性,排他性的执行一系列命令
  • 三特性 1、隔离性:所有命令都会按照顺序执行,事务在执行的过程中,不会被其他客户端送来的命令 打断
    2、没有隔离级别:队列中的命令没有提交之前都不会被实际的执行,不存在“事务中查询要看到 事务里的更新,事务外查询不能看到”这个头疼的问题
    3、不保证原子性:冤有头债有主,如果一个命令失败,但是别的命令可能会执行成功,没有回 滚
  • 三步走     开启multi    入队queued    执行exec
  • 与关系型数据库事务相比,
    multi:可以理解成关系型事务中的 begin
    exec :可以理解成关系型事务中的 commit
    discard :可以理解成关系型事务中的 rollback
     

3.4.1 一起生

开启事务,加入队列,一起执行,并成功

127.0.0.1:6379> multi # 开启事务 OK

127.0.0.1:6379> set k1 v1

QUEUED # 加入队列

127.0.0.1:6379> set k2 v2

QUEUED # 加入队列

127.0.0.1:6379> get k2

QUEUED # 加入队列

127.0.0.1:6379> set k3 v3

QUEUED # 加入队列

127.0.0.1:6379> exec # 执行,一起成功!

1) OK

2) OK

3) "v2"

4) OK

3.4.2 一起死

放弃之前的操作,恢复到原来的值

127.0.0.1:6379> multi # 开启事务

OK

127.0.0.1:6379> set k1 v1111

QUEUED

127.0.0.1:6379> set k2 v2222

QUEUED

127.0.0.1:6379> discard # 放弃操作

OK

127.0.0.1:6379> get k1

"v1" # 还是原来的值

3.4.3 一粒老鼠屎坏一锅汤

一句报错,全部取消,恢复到原来的值

127.0.0.1:6379> multi

OK

127.0.0.1:6379> set k4 v4

QUEUED

127.0.0.1:6379> setlalala # 一句报错

(error) ERR unknown command `setlalala`, with args beginning with: 127.0.0.1:6379> set k5 v5

QUEUED

127.0.0.1:6379> exec  # 队列中命令全部取消

(error) EXECABORT Transaction discarded because of previous errors.

127.0.0.1:6379> keys * # 还是原来的值

1) "k2"

2) "k3"

3) "k1"

3.4.4 冤有头债有主

追究责任,谁的错,找谁去

127.0.0.1:6379> multi

OK

127.0.0.1:6379> incr k1 # 虽然v1不能++,但是加入队列并没有报错,类似java中的通过编 译

QUEUED

127.0.0.1:6379> set k4 v4

QUEUED

127.0.0.1:6379> set k5 v5

QUEUED 127.0.0.1:6379> exec

1) (error) ERR value is not an integer or out of range # 真正执行的时候,报错

2) OK # 成功

3) OK # 成功

127.0.0.1:6379> keys *

1) "k5"

2) "k1"

3) "k3"

4) "k2"

5) "k4"

3.4.5 watch监控

测试:模拟收入与支出

正常情况下:

127.0.0.1:6379> set in 100 # 收入100元

OK

127.0.0.1:6379> set out 0 # 支出0元

OK

127.0.0.1:6379> multi

OK 127.0.0.1:6379> decrby in 20 # 收入-20

QUEUED

127.0.0.1:6379> incrby out 20 # 支出+20

QUEUED

127.0.0.1:6379> exec

1) (integer) 80

2) (integer) 20 # 结果,没问题!

特殊情况下:

127.0.0.1:6379> watch in # 监控收入in

OK

127.0.0.1:6379> multi

OK

127.0.0.1:6379> decrby in 20

QUEUED

127.0.0.1:6379> incrby out 20

QUEUED

127.0.0.1:6379> exec

(nil)        # 在exec之前,我开启了另一个窗口(线程),对监控的in做了修改,所以本次的事务将 被打断(失效),类似于“乐观锁”

unwatch:取消watch命令对所有key的操作;一旦执行了exec命令,那么之前加的所有监控自动失效!

3.5 Redis的发布订阅

  • 进程间的一种消息通信模式:发送者(pub-窗口1)发送消息,订阅者(sub-窗口2)接收消息。例如:微信订阅号
  • 订阅一个或多个频道
127.0.0.1:6379> subscribe cctv1 cctv5 cctv6 # 1.订阅三个频道
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "cctv1"
3) (integer) 1
1) "subscribe"
2) "cctv5"
3) (integer) 2
1) "subscribe"
2) "cctv6"
3) (integer) 3
1) "message" # 3.cctv5接收到推送过来的信息
2) "cctv5"
3) "NBA"
127.0.0.1:6379> publish cctv5 NBA # 2.发送消息给cctv5
(integer) 1

3.5 主从复制

  • 就是 redis集群的策略
  • 配从(库)不配主(库):小弟可以选择谁是大哥,但大哥没有权利去选择小弟
  • 读写分离:主机写,从机读

详情可参考:

3.5.1 一主二仆

1. 准备三台服务器,并修改redis.conf

bind 0.0.0.0

2. 启动三台redis,并查看每台机器的角色,都是master

info replication

3. 测试开始

  • 首先,将三个机器全都清空,第一台添加值

    mset k1 v1 k2 v2
     
  • 其余两台机器,复制(找大哥) (此方法是临时的,从机关闭后重启 成为master)

    slaveof 192.168.204.141 6379

    也可以在redis.conf 配置文件中配置 
  • 第一台再添加值

    set k3 v3

 思考1:slave之前的k1和k2是否能拿到?

可以获得,只要跟了大哥,之前的数据也会立刻同步

思考2:slave之后的k3是否能拿到?

可以获得,只要跟了大哥,数据会立刻同步

思考3同时添加k4,结果如何?

主机(141master)可以添加成功,从机(142和143是slave)失败,从机只负责读取 数据,无权写入数据,这就是“读写分离”

redis核心技术与实战下载 redis技术分享_nosql_19

思考4:主机shutdown,从机如何?

101和102仍然是slave,并显示他们的master已离线

思考5:主机重启,从机又如何?

101和102仍然是slave,并显示他们的master已上线

思考6:从机死了,主机如何?从机归来身份是否变化?

  • 主机没有变化,只是显示少了一个slave 
  • 主机和从机没有变化,而重启归来的从机自立门户成为了master,不和原来的集群在一 起了

3.5.2 血脉相传

  • 一个主机理论上可以多个从机,但是这样的话,这个主机会很累
  • 我们可以使用java面向对象继承中的传递性来解决这个问题,减轻主机的负担
  • 形成祖孙三代:

127.0.0.1:6379> slaveof 192.168.204.100 6379 # 101跟随100

OK

127.0.0.1:6379> slaveof 192.168.204.101 6379 # 102跟随101

OK

3.5.3 谋权篡位

  • 1个主机,2个从机,当1个主机挂掉了,只能从2个从机中再次选1个主机
  • 国不可一日无君,军不可一日无帅
  • 手动选老大
  • 模拟测试:1为master,2和3为slave,当1挂掉后,2篡权为master,3跟2

slaveof no one # 2上执行,没有人能让我臣服,那我就是老大

slaveof 192.168.204.142 6379 # 3跟随2号

思考:当1再次回归,会怎样?

2和3已经形成新的集群,和1没有任何的关系了。所以1成为了光杆司令

3.5.4 复制原理

redis核心技术与实战下载 redis技术分享_数据_20

 完成上面几个步骤后就完成了从服务器数据初始化的所有操作,从服务器此时可以接收来自用户的读请 求

  • 全量复制:Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份slave接收到数据 文件后,存盘,并加载到内存中;(步骤1234)
  • 增量复制:Slave初始化后,开始正常工作时主服务器发生的写操作同步到从服务器的过程;(步 骤56)
    但,只要是重新连接master,一次性(全量复制)同步将自动执行;
  • Redis主从同步策略:主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步。
  • 当然,如果有需要,slave 在任何时候都可以发起全量同步。
  • redis 策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步。

4 Jedis

java和redis打交道的API客户端

<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.1.0</version>
</dependency>

4.1 连接redis

public class Test01 {
    // 运行前:
    // 1.关闭防火墙 systemctl stop firewalld.service
    // 2.修改redis.conf [ bind 0.0.0.0 ] 允许任何ip访问,以这个redis.conf启动redis服务
    //(重启redis)
    // redis-server /opt/redis5/redis.conf
    public static void main(String[] args) {
        Jedis jedis = new Jedis("192.168.40.101", 6379);
        String pong = jedis.ping();
        System.out.println("pong = " + pong);
    }
}

4.2 常用API

public class Test2_API {
    public static void main(String[] args) {
//        testHash();
        testZset();
    }

    private static void testHash(){
        Jedis jedis = new Jedis("192.168.40.101", 6379);
        jedis.hset("hash01","username","james");
        String hget = jedis.hget("hash01", "username");
        System.out.println(hget);

        HashMap<String,String> map = new HashMap<>();
        map.put("gender","boy");
        map.put("name","phl");
        map.put("address","beijing");
        map.put("phone","13");

        jedis.hmset("user2",map);
        List<String> user2 = jedis.hmget("user2","name","phone");
        System.out.println(user2);

    }

    private static void testString(){
        Jedis jedis = new Jedis("192.168.40.101", 6379);

        //String
        jedis.set("k1","v1");
        jedis.set("k2","v2");
        jedis.set("k3","v3");

        Set<String> keys = jedis.keys("*");

        for (String key : keys) {
            System.out.println("key->"+key);
        }

        Boolean k2Exists = jedis.exists("k2"); //查看k2是否存在
        System.out.println("k2Exists-->"+k2Exists);
        System.out.println(jedis.ttl("k1")); //查看k1的过期时间
        jedis.mset("k4","v4","k5","v5");
        System.out.println(jedis.mget("k1","k2","k4"));
        System.out.println("========================");

    }

    private static void testSet(){
        Jedis jedis = new Jedis("192.168.40.101", 6379);

        //set
        jedis.sadd("order","jd001");
        jedis.sadd("order","jd002");
        jedis.sadd("order","jd003");

        Set<String> order = jedis.smembers("order");
        for (String s : order) {
            System.out.println("orderVal-->"+s);
        }

        jedis.srem("order","jd002");
        System.out.println(jedis.smembers("order"));
    }

    private static void testZset(){
        Jedis jedis = new Jedis("192.168.40.101", 6379);
        jedis.zadd("zset01",80d,"zs1");
        jedis.zadd("zset01",70d,"zs2");
        jedis.zadd("zset01",66d,"zs3");
        jedis.zadd("zset01",90d,"zs4");
        Set<String> zset01 = jedis.zrange("zset01", 0, -1);
        System.out.println(zset01);
    }
}

4.3 事务

初始化余额和支出


set yue 100

set zhichu 0

public class Test03_Multi {
    public static void main(String[] args) throws InterruptedException {
        Jedis jedis = new Jedis("192.168.40.101", 6379);
        int yue = Integer.valueOf(jedis.get("yue")); //余额
        int zhichu = 10; //支出

        jedis.watch("yue");//监控余额
        TimeUnit.SECONDS.sleep(5); //模拟网络延迟

        if (yue < zhichu) {
            jedis.unwatch(); //解除监控
            System.out.println("余额不足!");
        } else {
            Transaction transaction = jedis.multi(); //开启事务
            transaction.decrBy("yue", zhichu); //余额减少
            transaction.incrBy("zhichu", zhichu);//累计消费增加
            transaction.exec();
            System.out.println("余额:---》" + jedis.get("yue"));
            System.out.println("支出:---》" + jedis.get("zhichu"));
        }
    }
}

模拟网络延迟:,10秒内,进入linux修改余额为5,这样,余额<支出,就会进入if

4.4 JedisPool

redis的连接池技术

详情:https://help.aliyun.com/document_detail/98726.html

<dependency>
            <groupId>commons-pool</groupId>
            <artifactId>commons-pool</artifactId>
            <version>1.6</version>
        </dependency>

使用单例模式进行优化

public class JedisPoolUtil {

    private JedisPoolUtil() {

    }

    private volatile static JedisPool jedisPool = null;
    private volatile static Jedis jedis = null;

    //返回一个连接池
    private static JedisPool getJedisPool() {
        //双重检测锁
        if (jedisPool == null) {
            synchronized (JedisPoolUtil.class) {
                if (jedisPool == null) {
                    JedisPoolConfig config = new JedisPoolConfig();
                    config.setMaxTotal(1000);
                    config.setMaxIdle(30);
                    config.setMaxWaitMillis(60 * 1000);
                    config.setTestOnBorrow(true);
                    jedisPool = new JedisPool(config, "192.168.40.101", 6379);
                }
            }
        }
        return jedisPool;
    }

    //返回jedis对象
    public static Jedis getJedis() {
        if (jedis == null) {
            jedis = getJedisPool().getResource();
        }
        return jedis;
    }


}

测试类:

public class Test_JedisPool {
    public static void main(String[] args) {
        Jedis jedis1 = JedisPoolUtil.getJedis();
        Jedis jedis2 = JedisPoolUtil.getJedis();
        System.out.println(jedis1==jedis2);
    }
}

4.6 高并发下的分布式锁

经典案例:秒杀,抢购优惠券等

4.6.1 搭建工程并测试单线程

<packaging>war</packaging>
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.2.7.RELEASE</version>
        </dependency>
        <!--实现分布式锁的工具类-->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.6.1</version>
        </dependency>
        <!--spring操作redis的工具类-->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-redis</artifactId>
            <version>2.3.2.RELEASE</version>
        </dependency>
        <!--redis客户端-->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.1.0</version>
        </dependency>
        <!--json解析工具-->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.9.8</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.tomcat.maven</groupId>
                <artifactId>tomcat7-maven-plugin</artifactId>
                <configuration>
                    <port>8001</port>
                    <path>/</path>
                </configuration>
                <executions>
                    <execution>
                        <!-- 打包完成后,运行服务 -->
                        <phase>package</phase>
                        <goals>
                            <goal>run</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         id="WebApp_ID" version="3.1">
    <servlet>
        <servlet-name>springmvc</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet
        </servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring/spring.xml</param-value>
        </init-param>
    </servlet>
    <servlet-mapping>
        <servlet-name>springmvc</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
    <context:component-scan base-package="com.lagou.controller"/>
    <bean id="stringRedisTemplate"
          class="org.springframework.data.redis.core.StringRedisTemplate">
        <property name="connectionFactory" ref="connectionFactory"></property>
    </bean>
    <bean id="connectionFactory"
          class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
        <property name="hostName" value="192.168.40.101"></property>
        <property name="port" value="6379"/>
    </bean>
</beans>
package com.lagou.controller;

import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.concurrent.TimeUnit;

/**
 * @Author panghl
 * @Date 2021/8/3 22:25
 * @Description 测试秒杀
 **/
@Controller
public class TestKillController {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private Redisson redisson;

    @Bean
    public Redisson redisson() {
        Config config = new Config();
        //使用单个redis服务器
        config.useSingleServer().setAddress("redis://192.168.40.101:6379").setDatabase(0);
        //使用集群redis
//        config.useClusterServers().setScanInterval(2000).addNodeAddress("redis://192.168.40.101:6379","redis://192.168.40.100:6379","redis://192.168.40.102:6379");
        return (Redisson) Redisson.create(config);
    }

    //127.0.0.1:6379> set phone 10
    @RequestMapping("/kill")
    @ResponseBody
    // 只能解决一个tomcat的并发问题:synchronized 锁的一个进程下的多线程并发,如果是分布式环境,这个就失效了!
    public /*synchronized*/ String kill() {
        //定义商品id
        String productKey = "HUAWEI-P40";
        //通过redisson 获取锁
        RLock rLock = redisson.getLock(productKey); //底层源码就是集成了setnx,过期时间等操作
        //上锁 (过期时间为30秒)
        rLock.lock(30, TimeUnit.SECONDS);

        try {
            //1.从redis中获取 手机的库存数量
            Integer phoneCount = Integer.valueOf(stringRedisTemplate.opsForValue().get("phone"));
            //2. 判断手机的数量是否够秒杀的
            if (phoneCount > 0) {
                phoneCount--;
                //库存减少后,再将库存的值保存到redis
                stringRedisTemplate.opsForValue().set("phone", phoneCount.toString());
                System.out.println("库存-1,剩余:" + phoneCount);
            } else {
                System.out.println("库存不足");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rLock.unlock(); //释放锁
        }


        return "over";
    }

}

4.6.2  高并发测试

1. 启动两次工程,端口号分别8001和8002

2. 使用nginx做负载均衡

upstream sga{
server 192.168.204.1:8001;
server 192.168.204.1:8002;
}
server {
listen 80;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
proxy_pass http://sga;
root html;
index index.html index.htm;
}

/usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf

3. 使用 JMeter 模拟1秒内发出100个http请求,会发现同一个商品会被两台服务器同时抢购!

4.6.3 实现分布式锁的思路

1. 因为redis是单线程的,所以命令也就具备原子性,使用setnx命令实现锁,保存k-v

        如果k不存在,保存(当前线程加锁),执行完成后,删除k表示释放锁

        如果k已存在,阻塞线程执行,表示有锁

2. 如果加锁成功,在执行业务代码的过程中出现异常,导致没有删除k(释放锁失败),那么就会造 成死锁(后面的所有线程都无法执行)!

        设置过期时间,例如10秒后,redis自动删除

3. 高并发下,由于时间段等因素导致服务器压力过大或过小,每个线程执行的时间不同

        第一个线程,执行需要13秒,执行到第10秒时,redis自动过期了k(释放锁)

        第二个线程,执行需要7秒,加锁,执行第3秒(锁 被释放了,为什么,是被第一个线程的 finally主动deleteKey释放掉了)

        。。。连锁反应,当前线程刚加的锁,就被其他线程释放掉了,周而复始,导致锁会永久失 效

4. 给每个线程加上唯一的标识UUID随机生成,释放的时候判断是否是当前的标识即可

5. 问题又来了,过期时间如果设定? 如果10秒太短不够用怎么办?

        设置60秒,太长又浪费时间

        可以开启一个定时器线程,当过期时间小于总过期时间的1/3时,增长总过期时间(吃仙丹续 命!)

自己实现分布式锁,太难了!

4.6.4 Redisson

  • Redis 是最流行的 NoSQL 数据库解决方案之一,而 Java 是世界上最流行(注意,我没有说“最 好”)的编程语言之一。
  • 虽然两者看起来很自然地在一起“工作”,但是要知道,Redis 其实并没有对 Java 提供原生支持。 相反,作为 Java 开发人员,我们若想在程序中集成 Redis,必须使用 Redis 的第三方库。
  • 而 Redisson 就是用于在 Java 程序中操作 Redis 的库,它使得我们可以在程序中轻松地使用 Redis。
  • Redisson 在 java.util 中常用接口的基础上,为我们提供了一系列具有分布式特性的工具类。
package com.lagou.controller;

import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.concurrent.TimeUnit;

/**
 * @Author panghl
 * @Date 2021/8/3 22:25
 * @Description 测试秒杀
 **/
@Controller
public class TestKillController {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private Redisson redisson;

    @Bean
    public Redisson redisson() {
        Config config = new Config();
        //使用单个redis服务器
        config.useSingleServer().setAddress("redis://192.168.40.101:6379").setDatabase(0);
        //使用集群redis
//        config.useClusterServers().setScanInterval(2000).addNodeAddress("redis://192.168.40.101:6379","redis://192.168.40.100:6379","redis://192.168.40.102:6379");
        return (Redisson) Redisson.create(config);
    }

    //127.0.0.1:6379> set phone 10
    @RequestMapping("/kill")
    @ResponseBody
    // 只能解决一个tomcat的并发问题:synchronized 锁的一个进程下的多线程并发,如果是分布式环境,这个就失效了!
    public /*synchronized*/ String kill() {
        //定义商品id
        String productKey = "HUAWEI-P40";
        //通过redisson 获取锁
        RLock rLock = redisson.getLock(productKey); //底层源码就是集成了setnx,过期时间等操作
        //上锁 (过期时间为30秒)
        rLock.lock(30, TimeUnit.SECONDS);

        try {
            //1.从redis中获取 手机的库存数量
            Integer phoneCount = Integer.valueOf(stringRedisTemplate.opsForValue().get("phone"));
            //2. 判断手机的数量是否够秒杀的
            if (phoneCount > 0) {
                phoneCount--;
                //库存减少后,再将库存的值保存到redis
                stringRedisTemplate.opsForValue().set("phone", phoneCount.toString());
                System.out.println("库存-1,剩余:" + phoneCount);
            } else {
                System.out.println("库存不足");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rLock.unlock(); //释放锁
        }


        return "over";
    }

}
  • 实现分布式锁的方案其实有很多,我们之前用过的zookeeper的特点就是高可靠性,现在我们用的 redis特点就是高性能。
  • 目前分布式锁,应用最多的仍然是“Redis