CentOS7安装Redis及基本的认识

1.Redis 下载地址:https://redis.io/download

2.上传到服务器指定文件夹 ,我这边传到了根目录下 /mysoft 这个目录下

 解压  tar  -zxvf redis-4.0.8.tar.gz  -C /mysoft/

3.进行编译安装

cd redis-4.0.8
make //编译
cd src
make install PREFIX=/usr/local/redis //安装到这个目录下

 编译可能会出错,这边需要安装  gcc 服务

yum install gcc

我这边把配置文件移到了 安装目录下 /usr/local/redis/etc

mkdir /usr/local/redis/etc
mv redis.conf /usr/local/redis/etc

4.简单配置  

配置密码:

//取消该注解并在后面配上密码
 requirepass newings2018

配置远程连接

# bind 127.0.0.1 //注释掉改行
protected-mode yes //yes改为no

配置以后台方式运行

daemonize yes //no改成yes

5.将redis加入到开机启动

  vi /etc/rc.local //在里面添加内容:/usr/local/redis/bin/redis-server  /usr/local/redis/etc/redis.conf (意思就是开机调用这段开启redis的命令)

6..最后一定要把redis对应的端口开放  我这里用的阿里云服务器 需要配置安全组规则

这样便可以启动 Redis 并加载配置文件

/usr/local/redis/bin/redis-server /usr/local/redis/etc/redis.conf

关闭服务

pkill redis-server

启动redis客户端:进入src目录  ./redis-cli -p 6399

以密码登陆 :进入src目录  ./redis-cli -h 127.0.0.1 -p 6399 -a password    flushall删除所有数据  

select  index进入指定数据库  flushdb   删除该db的数据

 数据类型:

  官网是这么描述的:

  Redis is an open source (BSD licensed), in-memory data structure store, used as a database, cache and message broker. It supports data structures such as strings, hashes, lists, sets, sorted sets with range queries, bitmaps, hyperloglogs, geospatial indexes with radius queries and streams. Redis has built-in replication, Lua scripting, LRU eviction, transactions and different levels of on-disk persistence, and provides high availability via Redis Sentinel and automatic partitioning with Redis Cluster.(Redis是开源的(BSD许可)内存数据结构存储,用作数据库,缓存和消息代理。它支持数据结构,例如字符串,哈希,列表,集合,带范围查询的排序集合,位图,超日志,带有半径查询和流的地理空间索引。Redis具有内置的复制,Lua脚本,LRU逐出,事务和不同级别的磁盘持久性,并通过Redis Sentinel和Redis Cluster自动分区提供高可用性。)

  这段文字描述了redis的书记结构以及数据持久化及淘汰,以及高可用相关。我们先来看看相关的数据结构及其基本操作:

1.String 字符串:

  存储类型:可以用来存储字符串、整数、浮点数。

//设置多个值(批量操作,原子性)
mset key1 value1 key2 value2
//设置值,如果 key 存在,则不成功 
setnx key1 value1
//基于setnx 可实现分布式锁。用 del key 释放锁。但如果释放锁的操作失败了,导致其他节点永远获取不到锁,怎么办?加过期时间。单独用 expire 加过期,也失败了,无法保证原子性,怎么办?多参数
set key value [expiration EX seconds|PX milliseconds][NX|XX]
//使用参数的方式
set lock1 1 EX 10 NX
//(整数)值原子递增
incr key1
incrby qingshan 100
//(整数)值递减
decr qingshan
decrby qingshan 100
//浮点数增量
set f 2.6
incrbyfloat f 7.3
//获取多个值
mget key1 key2
//获取值长度
strlen key1
//字符串追加内容
append key1 good
//获取指定范围的字符
getrange key 0 8
//位计算
set k1 a
setbit k1 6 1
setbit k1 7 0
get k1
//key 分层的方式 student 表 有ID 字段  sno字段 sname字段 company字段
mset student:1:sno 8888 student:1:sname 沐风 student:1:company 腾讯
//获取值的时候一次获取多个值:
mget student:1:sno 9999 student:1:sname java student:1:company alibaba

应用场景:

  •   缓存String 类型,例如:热点数据缓存,对象缓存,全页缓存。可以提升热点数据的访问速度。
  •   数据共享:因为 Redis 是分布式的独立服务,可以在多个应用之间共享.例如:分布式 Session
  •   分布式锁:setnx 方法,只有不存在时才能添加成功,返回 true。
  •   全局 ID:INCRBY,利用原子性.例如:文章的阅读量,微博点赞数,允许一定的延迟,先写入 Redis 再定时同步到数据库。(限流)以访问者的 IP 和其他信息作为 key,访问一次增加一次计数,超过次数则返回 false。

  缺点:key 太长,占用的空间太多。

2.Hash 哈希


  存储类型:包含键值对的无序散列表。value 只能是字符串,不能嵌套其他类型。

  同样是存储字符串,Hash 与 String 的主要区别?

  1. 把所有相关的值聚集到一个 key 中,节省内存空间
  2. 只使用一个 key,减少 key 冲突
  3. 当需要批量获取值的时候,只需要使用一个命令,减少内存/IO/CPU 的消耗

  Hash 不适合的场景:

  1. Field 不能单独设置过期时间
  2. 没有 bit 操作
  3. 需要考虑数据量分布的问题(value 值非常大的时候,无法分布到多个节点)
//设置值
hset wuzz name wuzz
//设置值
hset wuzz age 18
//设置多个值
hmset wuzz sex m height 180
//获取值
hget wuzz name
//批量获取
hmget wuzz name age sex height
//获取keys
hkeys wuzz
//获取vals
hvals wuzz
//获取所有
hgetall wuzz
//是否存在
hget exists wuzz
//删除
hdel wuzz age
hgetall wuzz
//原子递增
hincrby wuzz age 1

应用场景:

  hash 类型十分适合存储对象类数据,相对于在 string 中介绍的把对象转化为 json 字符串存储,hash 的结构可以任意添加或删除‘字段名’,更加高效灵活。String 可以做的事情,Hash 基本都可以做(位运算除外)。

3.List 列表

  存储类型:存储有序的字符串(从左到右),元素可以重复。可以充当队列和栈的角色。

//设置值 此时队列是  a
lpush queue a
//设置值 此时队列是  c b a
lpush queue b c
//设置值 此时队列是  c b a d e
rpush queue d e
//从左边取出值  则是 c 采用blpop会没有任何元素可以弹出的时候,连接会被阻塞
lpop queue
//取出所有值
lrange queue 0 -1
//获取指定位置的值
lindex queue 0
//取出右边第一个
rpop queue

应用场景:

  因为 List 是有序的,可以用来做用户时间线(排序相关)

  消息队列:List 提供了两个阻塞的弹出操作:BLPOP/BRPOP,可以设置超时时间。BLPOP:BLPOP key1 timeout 移出并获取列表的第一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。BRPOP:BRPOP key1 timeout 移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。队列:先进先出:rpush blpop,左头右尾,右边进入队列,左边出队列。栈:先进后出:rpush brpop

4.Set 集合

  存储类型:String 类型的无序集合,最大存储数量 2^32-1(40 亿左右)。

//添加一个或者多个元素
sadd myset a b c d e f g
//获取所有元素
smembers myset
//统计元素个数
scard myset
//随机获取一个元素
srandmember key
//随机弹出一个元素
spop myset
//移除一个或者多个元素
srem myset d e f
//查看元素是否存在
sismember myset a
//获取差集
sdiff set1 set2
//获取交集( intersection )
sinter set1 set2
//获取并集
sunion set1 set2

应用场景:

  • 抽奖:随机获取元素。
  • 点赞 、签到、打卡:比如有条微博的 ID 是 t1001,用户 ID 是 u3001。用 like:t1001 来维护 t1001 这条微博的所有点赞用户。点赞了这条微博:sadd like:t1001 u3001取消点赞:srem like:t1001 u3001是否点赞:sismember like:t1001 u3001点赞的所有用户:smembers like:t1001点赞数:scard like:t1001
  • 商品标签:维护商品所有的标签。某个商品有的一些标签都可以维护到set里。

5.ZSet 有序集合:

  sorted set,有序的 set,每个元素有个 score。score 相同时,按照 key 的 ASCII 码排序。数据结构对比:

//添加元素
zadd myzset 10 java 20 php 30 ruby 40 cpp 50 python
//获取全部元素
zrange myzset 0 -1 withscores
zrevrange myzset 0 -1 withscores
//根据分值区间获取元素
zrangebyscore myzset 20 30
//移除元素 也可以根据 score rank 删除
zrem myzset php cpp
//统计元素个数
zcard myzset
//分值递增
zincrby myzset 5 python
//根据分值统计个数
zcount myzset 20 60
//获取元素 rank
zrank myzset java
//获取元素 score
zsocre myzset java
//也有倒序的 rev 操作(reverse)

应用场景:

  排行榜:id 为 6001 的新闻点击数加 1:zincrby hotNews:20190926 1 n6001.获取今天点击最多的 15 条:zrevrange hotNews:20190926 0 15 withscores.

6.其他数据类型介绍:

  在 2.8.9 版本添加了 HyperLogLog 结构。Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,例如统计一个集合里面不同元数的个数,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定 的、并且是很小的。

  这里还需要说明的是另外一种数据类型 geospatial indexes  。这个东东是可以用来做地理位置信息存储及简单计算的,比如要获取某个经纬度的点方圆多少公里内的点的经纬度,两个点之间的距离等等。

//设置某个点的经纬度
geoadd location 120.1 29.2 hangzhou
//获取
geopos location hangzhou

  对于其他的一些操作,就Redis的java客户端来说,Jedis也已经提供了这个地理空间功能的基本操作Api

发布订阅模式:

  通过队列的 rpush 和 lpop 可以实现消息队列(队尾进队头出),但是消费者需要不停地调用 lpop 查看 List 中是否有等待处理的消息(比如写一个 while 循环)。为了减少通信的消耗,可以 sleep()一段时间再消费,但是会有两个问题:

  • 如果生产者生产消息的速度远大于消费者消费消息的速度,List 会占用大量的内存。
  • 消息的实时性降低。list 还提供了一个阻塞的命令:blpop,没有任何元素可以弹出的时候,连接会被阻塞。

  除了通过 list 实现消息队列之外,Redis 还提供了一组命令实现发布/订阅模式。这种方式,发送者和接收者没有直接关联(实现了解耦),接收者也不需要持续尝试获取消息。

  首先,我们有很多的频道(channel),我们也可以把这个频道理解成 queue。订阅者可以订阅一个或者多个频道。消息的发布者(生产者)可以给指定的频道发布消息。只要有消息到达了频道,所有订阅了这个频道的订阅者都会收到这条消息。需要注意的注意是,发出去的消息不会被持久化,因为它已经从队列里面移除了,所以消费者只能收到它开始订阅这个频道之后发布的消息。下面我们来看一下发布订阅命令的使用方法。订阅者订阅频道:可以一次订阅多个,比如这个客户端订阅了 3 个频道。

subscribe channel-1 channel-2 channel-3

  发布者可以向指定频道发布消息(并不支持一次向多个频道发送消息):

publish channel-1 hello

  取消订阅(不能在订阅状态下使用):

unsubscribe channel-1

按规则(P P attern ) 订阅频道。支持?和*占位符。?代表一个字符,*代表 0 个或者多个字符。

  • 消费端 1,关注运动信息:psubscribe *sport
  • 消费端 2,关注所有新闻:psubscribe news*
  • 消费端 3,关注天气新闻:psubscribe news-weather
  • 生产者,发布 3 条信息:publish news-sport yaoming,publish news-music jaychou,publish news-weather rain。

Redis 事务:

  为什么要用事务:

  我们知道 Redis 的单个命令是原子性的(比如 get set mget mset),如果涉及到多个命令的时候,需要把多个命令作为一个不可分割的处理序列,就需要用到事务。例如我们之前说的用 setnx 实现分布式锁,我们先 set,然后设置对 key 设置 expire,防止 del 发生异常的时候锁不会被释放,业务处理完了以后再 del,这三个动作我们就希望它们作为一组命令执行。Redis 的事务有两个特点:

  1. 按进入队列的顺序执行。
  2. 不会受到其他客户端的请求的影响。

  Redis 的事务涉及到四个命令:multi(开启事务),exec(执行事务),discard(取消事务),watch(监视)

  案例场景:user1 和 user2 各有 1000 元,user1 需要向 user2 转账 100 元。user1 的账户余额减少 100 元,user2 的账户余额增加 100 元。

//初始化数据
127.0.0.1:6379> set user1 1000
OK
127.0.0.1:6379>  set user2 1000
OK
//开启事务
127.0.0.1:6379> multi
OK
//执行命令入队
127.0.0.1:6379>  decrby user1 100
QUEUED
127.0.0.1:6379>  incrby user2 100
QUEUED
//执行事务
127.0.0.1:6379> exec
1) (integer) 900
2) (integer) 1100
127.0.0.1:6379> get user1
"900" 127.0.0.1:6379> get user2
"1100" 127.0.0.1:6379>

  通过 multi 的命令开启事务。事务不能嵌套,多个 multi 命令效果一样。multi 执行后,客户端可以继续向服务器发送任意多条命令, 这些命令不会立即被执行, 而是被放到一个队列中, 当 exec 命令被调用时, 所有队列中的命令才会被执行。通过 exec 的命令执行事务。如果没有执行 exec,所有的命令都不会被执行。如果中途不想执行事务了,怎么办?可以调用 discard 可以清空事务队列,放弃执行。

whatch 命令:

  在 Redis 中还提供了一个 watch 命令。它可以为 Redis 事务提供 CAS 乐观锁行为(Check and Set / Compare andSwap),也就是多个线程更新变量的时候,会跟原值做比较,只有它没有被其他线程修改的情况下,才更新成新的值。我们可以用 watch 监视一个或者多个 key,如果开启事务之后,至少有一个被监视key 键在 exec 执行之前被修改了, 那么整个事务都会被取消(key 提前过期除外)。可以用 unwatch 取消。例子如下:

127.0.0.1:6379> set balance 1000
OK
127.0.0.1:6379> watch balance
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incrby balance 100
QUEUED
//此时此刻  客户端2 对balance进行了修改
//127.0.0.1:6379> decrby balance 100
//(integer) 900
127.0.0.1:6379> exec
(nil)
127.0.0.1:6379> get balance
"900"

事务可能遇到的问题

  我们把事务执行遇到的问题分成两种,一种是在执行 exec 之前发生错误,一种是在执行 exec 之后发生错误。

  • 在执行 exec 之前 发生错误,比如:入队的命令存在语法错误,包括参数数量,参数名等等(编译器错误)。在这种情况下事务会被拒绝执行,也就是队列中所有的命令都不会得到执行。
  • 在执行 exec 之后 发生错误,比如,类型错误,比如对 String 使用了 Hash 的命令,这是一种运行时错误。在这种发生了运行时异常的情况下,只有错误的命令没有被执行,但是其他命令没有受到影响。这个显然不符合我们对原子性的定义,也就是我们没办法用 Redis 的这种事务机制来实现原子性,保证数据的一致。

  为什么在一个事务中存在错误,redis不会回滚呢?只有当被调用的Redis命令有语法错误时,这条命令才会执行失败(在将这个命令放入事务队列期间,Redis能够发现此类问题),或者对某个键执行不符合其数据类型的操作:实际上,这就意味着只有程序错误才会导致Redis命令执行失败,这种错误很有可能在程序开发期间发现,一般很少在生产环境发现。Redis已经在系统内部进行功能简化,这样可以确保更快的运行速度,因为Redis不需要事务回滚的能力。对于Redis事务的这种行为,有一个普遍的反对观点,那就是程序有可能会有缺陷(bug)。但是,你应当注意到:事务回滚并不能解决任何程序错误。例如,如果某个查询会将一个键的值递增2,而不是1,或者递增错误的键,那么事务回滚机制是没有办法解决这些程序问题的。请注意,没有人能解决程序员自己的错误,这种错误可能会导致Redis命令执行失败。正因为这些程序错误不大可能会进入生产环境,所以我们在开发Redis时选用更加简单和快速的方法,没有实现错误回滚的功能。

Lua 脚本:

  Redis中集成了Lua的编译和执行器,所以我们可以在Redis中定义Lua脚本去执行。同时,在Lua脚本 中,可以直接调用Redis的命令,来操作Redis中的数据。

  Lua/ˈluə/是一种轻量级脚本语言,它是用 C 语言编写的,跟数据的存储过程有点类似。 使用 Lua 脚本来执行 Redis 命令的好处:

  1. 一次发送多个命令,减少网络开销。
  2. Redis 会将整个脚本作为一个整体执行,不会被其他请求打断,保持原子性。
  3. 对于复杂的组合命令,我们可以放在文件中,可以实现程序之间的命令集复用。

在Redis Cli 中 调用 Lua 脚本:

  使用 eval /ɪ'væl/ 方法,语法格式:

127.0.0.1:6379> eval lua-script key-num [key1 key2 key3 ....] [value1 value2 value3 ....]
eval 代表执行 Lua 语言的命令。 lua
-script 代表 Lua 语言脚本内容。 key-num 表示参数中有多少个 key,需要注意的是 Redis 中 key 是从 1 开始的,如果没有 key 的参数,那么写 0。 [key1 key2 key3…]是 key 作为参数传递给 Lua 语言,也可以不填,但是需要和 key-num 的个数对应起来。 [value1 value2 value3 ….]这些参数传递给 Lua 语言,它们是可填可不填的。

  例如:

在 Lua 脚本 中调用 Redis 命令:

  使用 redis.call(command, key [param1, param2…])进行操作。语法格式:

redis> eval "redis.call('set',KEYS[1],ARGV[1])" 1 lua-key lua-value
command 是命令,包括 set、get、del 等。
key 是被操作的键。
param1,param2…代表给 key 的参数。

  注意跟 Java 不一样,定义只有形参,调用只有实参。Lua 是在调用时用 key 表示形参,argv 表示参数值(实参)。在 Redis 中调用 Lua 脚本执行 Redis 命令

  以上命令等价于 set name wuzz。

在 Redis 中调用 Lua 脚本 文件中的命令 ,

  操作 Redis创建 Lua 脚本文件:

//创建文件
cd /mysoft/redis-4.0.8/src
vim wuzz.lua
//Lua 脚本内容,先设置,再取值:
redis.call('set','wuzz',hello)
return redis.call('get',wuzz)
//在 Redis 客户端中调用 Lua 脚本 0 位参数个数
cd /mysoft/redis-4.0.8/src
redis-cli --eval wuzz.lua 0

  案例 :对 IP 进行限流

  需求:在 X 秒内只能访问 Y 次。

  设计思路:用 key 记录 IP,用 value 记录访问次数。拿到 IP 以后,对 IP+1。如果是第一次访问,对 key 设置过期时间(参数 1)。否则判断次数,超过限定的次数(参数 2),返回 0。如果没有超过次数则返回 1。超过时间,key 过期之后,可以再次访问。KEY[1]是 IP, ARGV[1]是过期时间 X,ARGV[2]是限制访问的次数 Y。

-- ip_limit.lua
-- IP 限流,对某个 IP 频率进行限制 ,5 秒钟访问 2 次
local num=redis.call('incr',KEYS[1])
if tonumber(num)==1 then
    redis.call('expire',KEYS[1],ARGV[1])
    return 1
elseif tonumber(num)>tonumber(ARGV[2]) then
    return 0
else
    return 1
end

  5 秒钟内限制访问 2 次,调用测试(连续调用 2 次):

./redis-cli  -h 127.0.0.1 -p 6379 -a wuzhenzhao   --eval "ip_limit.lua" app:ip:limit:192.168.8.111 , 5 2

Jedis 调用 lua 脚本相关操作:

public static void main(String[] args) {
        Jedis jedis = getJedisUtil();
        for(int i=0; i<5; i++){
            limit();
        }
}

/**
* 5秒内限制访问2次
*/
public static void limit(){
        Jedis jedis = getJedisUtil();
        // 只在第一次对key设置过期时间
        String lua = "local num = redis.call('incr', KEYS[1])\n" +
                "if tonumber(num) == 1 then\n" +
                "\tredis.call('expire', KEYS[1], ARGV[1])\n" +
                "\treturn 1\n" +
                "elseif tonumber(num) > tonumber(ARGV[2]) then\n" +
                "\treturn 0\n" +
                "else \n" +
                "\treturn 1\n" +
                "end\n";
        Object result = jedis.evalsha(jedis.scriptLoad(lua), Arrays.asList("localhost"), Arrays.asList("5", "2"));
        System.out.println(result);
}

private static Jedis getJedisUtil() {
        String ip = ResourceUtil.getKey("redis.host");
        int port = Integer.valueOf(ResourceUtil.getKey("redis.port"));
        String password = ResourceUtil.getKey("redis.password");
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        JedisPool pool = new JedisPool(jedisPoolConfig, ip, port, 10000, password);
        return pool.getResource();
}

缓存 Lua 脚本:

  为什么要缓存?在脚本比较长的情况下,如果每次调用脚本都需要把整个脚本传给 Redis 服务端,会产生比较大的网络开销。为了解决这个问题,Redis 提供了 EVALSHA 命令,允许开发者通过脚本内容的 SHA1 摘要来执行脚本。

  Redis 在执行 script load 命令时会计算脚本的 SHA1 摘要并记录在脚本缓存中,执行 EVALSHA 命令时 Redis 会根据提供的摘要从脚本缓存中查找对应的脚本内容,如果找到了则执行脚本,否则会返回错误:"NOSCRIPT No matching script. Please useEVAL."

  脚本超时:

  Redis 的指令执行本身是单线程的,这个线程还要执行客户端的 Lua 脚本,如果 Lua脚本执行超时或者陷入了死循环,是不是没有办法为客户端提供服务了呢?

  使用  eval 'while(true) do end' 0 来模拟死循环,为了防止某个脚本执行时间过长导致 Redis 无法提供服务,Redis 提供了 lua-time-limit 参数限制脚本的最长运行时间,默认为 5 秒钟。

  lua-time-limit 5000(redis.conf 配置文件中) 当脚本运行时间超过这一限制后,Redis 将开始接受其他命令但不会执行(以确保脚本的原子性,因为此时脚本并没有被终止),而是会返回“BUSY”错误。Redis 提供了一个 script kill 的命令来中止脚本的执行。新开一个客户端:script kill

  如果当前执行的 Lua 脚本对 Redis 的数据进行了修改(SET、DEL 等),那么通过 script kill 命令是不能终止脚本运行的。因为要保证脚本运行的原子性,如果脚本执行了一部分终止,那就违背了脚本原子性的要求。最终要保证脚本要么都执行,要么都不执行。遇到这种情况,只能通过 shutdown nosave 命令来强行终止 redis。shutdown nosave 和 shutdown 的区别在于 shutdown nosave 不会进行持久化操作,意味着发生在上一次快照后的数据库修改都会丢失。

  如果我们有一些特殊的需求,可以用 Lua 来实现。

posted @ 2018-12-21 09:59  吴振照  阅读(1332)  评论(0编辑  收藏  举报