Redis学习(一)Redis基础

Redis介绍

什么是Redis?

  • Redis是用c语言开发的一个开源的高性能键值对(key-value)内存数据库,他是一种NoSql数据库。
  • 他是【单进程单线程】的内存数据库,所以说不存在线程安全问题。
  • 他可以支持并发10w/QPS,所以说性能非常优秀。之所以单进程单线程性能还这么好,就是因为底层采用了【IO多路复用(NIO思想)】
  • 相比Memcache这种专业缓存技术,它有更优秀的读写能力,及丰富的数据类型。
  • 他提供了五中数据类型来存储(值):
  • 字符串类型(String)
  • 散列类型(hash)
  • 列表类型(list)
  • 集合类型(set)
  • 有序集合类型(sortedset、zset)

Redis官网

  • 官网地址:http://redis.io/
  • 中文官网地址:http://www.redis.cn/
  • 下载地址:http://download.redis.io/releases/

什么是NoSQL?

  • NoSQL,即Not-Only SQL(不仅仅是SQL),泛指菲关系型数据库。
  • 什么是关系型数据库?数据结构是一种有行有列的数据库。
  • NoSQL是为了解决高并发、高可用、高可扩展、大数据存储问题而产生的数据库解决方案。
  • NOSQL可以作为关系型数据库的良好补充,但是不能替代关系型数据库。

Redis发展历史

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

Redis安装环境

#1.安装c语言需要的GCC 环境
yum install -y gcc-c++
yum install -y wget
#2.下载解压redis的源码包
wget http://download.redis.io/releases/redis-5.0.4.tar.gz
tar -zxf redis-5.0.4.tar.gz
#3.编译Redis源码,进入redis-5.0.4
cd redis-5.0.4
make
#4.安装Redis,通过PREFIX指定安装目录
make install PREFIX=/usr/local/redis

Redis启动

前台启动
  • 直接运行/bin/redis-server
#启动
./redis-server
#关闭
ctrl+c
  • 前台启动客户端窗口关闭则Redis程序结束,不推荐前台启动
后台启动
  • 拷贝redis.config配置文件到Redis安装目录的bin目录
  • 修改redis.config
vim redis.config
# 将`daemonize`由`no`改为`yes`
daemonize yes
# 默认绑定的是回环地址,默认不能被其他机器访问
# bind 127.0.0.1
# 是否开启保护模式,由yes改为no
protected-mode no
  • 启动服务
#启动服务
./redis-service redis.conf
#关闭服务
./redis-cli shutdown
其他命令说明
  • redis-server :启动 redis 服务
  • redis-cli :进入 redis 命令客户端
  • redis-benchmark : 性能测试的工具
  • redis-check-aof : aof 文件进行检查的工具
  • redis-check-dump : rdb 文件进行检查的工具
  • redis-sentinel : 启动哨兵监控服务

Redis的数据类型

官方命令大全:http://www.redis.cn/commands.html

Redis存储数据是通过key-value格式存储数据的,其中value可以定义五中类型:String、Hash、List、Set、Zset

在Redis中的命令语句,命令是忽略大小的,而key是不忽略大小的。

String类型

常用命令:

SET key value				--赋值
GET key						--取值
GETSET key value			--取值并赋值

--数值增减 当value为整数数据时,才能使用以下命令操作数值的增减
INCR key					--递增数字
INCRBY key increment		--增加指定的整数

DECR key					--递减数值
DECRBY key decrement		--减少指定的数值

setnx key value				--当不存在时赋值

--其他命令
APPEND key value			--向键值的末尾追加,返回值为追加后的字符串总长度
STRLEN key					--返回键值的长度,如果不存在返回0

MSET key1 value1 key2 value2 ...	--同时设置多个值
MGET key1 key2 ...					--同时获取多个值
Hash类型

hash类型也叫做散列类型,他提供了字段和字段值的映射。字段值只能是字符串类型,不支持散列类型和集合类型。

常用命令

HSET key field value				--设置一个key的一个字段的值
HMSET key1 field1 value1 field2 value2 --设置一个key的多个字段的值
HSETNX key field value 				--设置一个key一个字段的值,如果该字段值值存在,则不执行任何操作

HGET key field 						--获取一个字段的值
HMGET key field1 field2 field3 ...	--获取一个key的多个字段的值
HGETALL key 						--获取key 所有字段的值

HDEL key field1 field2 ...		  	--删除key的一个或多个字段的值

HINCRBY key field increment 		--增加数字

HEXISTS key field					--判断key的这个字段是否存在

HKEYS key 							--获取key的字段名
HVALS key							--获取key的字段值

HLEN key							--获取key的字段数量

HGETALL key							--获取key的所有信息,包含key和value
String类型和Hash类型的区别

hash类型适合存储那些对象数据,特别是对象属性经常发生【增删改】的数据。String类型也可以存储对象类型,将java对象转成json字符串进行存储,这种存储适合【查询】操作。

List类型

Redis的列表类型(list类型)可以存储一个有序的字符串列表,常用的操作是向列表两端添加元素,或者获得列表的一个片段。

List类型内部是使用双向链表实现的,所以向列表两端添加元素的时间复杂度为O(1),获取越接近两端的元素就越快。这就意味着即使是一个有几千万个元素的列表,获取头部或者尾部的10条记录也是很快的。

常用命令

LPUSH key value1 value2 value3 ... 	--从左边放入一条或多条数据
RPUSH key value1 value2 value2 ...  --从右边防疫一条或多条数据

LRANGE key start stop				-- 获取列表中某一片段,将返回‘start’、'stop'之间的所有元素(包含两端),索引从‘0’开始,索引可以是负数,‘-1’代表最后变得一个元素。

LPOP key							--从列左端弹出元素
RPOP key							--从列右端弹出元素

LLEN key							--获取列表中元素的个数

LREM key count value				--删除列表中指定个数的值
LREM命令会删除列表中前count个值为value的元素,返回实际删除的元素个数。根据count值不同,该命令的执行方式会有所不同。
- 当count>0时,LREM会从列表左边开始删除
- 当count<0时,LREM会从列表后边开始删除
- 当count=0时,LREM会删除所有值为value的元素

LINDEX key index					--获得指定索引元素的值

LSET key index value				--设置指定索引元素的值

LTRIM key start stop				--只保留列表指定范围的片段,指定范围和LRANGE一样

LINSERT key BEFORE|AFTER pivot value 
向列表中插入数据,该命令首先会在列表中从左到右查找值为pivot的元素,然后根据第二个参数是BRFORE还是AFTER来决定将value插入到该元素的前面还是后面

RPOPLPUSH source destination		--将元素从一个列表转移到另一个列表中

可以用来存储商品信息的评论列表
Set类型

set类型即为集合类型,其中的数据是不重复且没有顺序的

集合类型的常用操作是向集合中加入或者删除元素、判断某个元素是否存在等,由于集合类型的Redis内部是使用值为空的散列表实现,所有这些操作的时间复杂度都为O(1)

Redis还提供了多个集合之间的交集、并集、差集的运算

常用命令

SADD key member1 member2...			--添加key的一个或者多个元素,返回值为添加的个数
SREM key member1 member2...			--删除key的一个或者多个元素,返回值为删除的个数

SMENMERS key						--获得集合中的所有元素

SISMEMBER key member				--判断key中是否存在元素

SDIFF A B							--集合的差集运算:属于A并且不属于B的元素构成的集合

SINTER A B							--集合的交集运算:属于A且属于B的元素构成的集合

SUNION A B							--集合的并集运算:属于A且属于B的元素构成的集合

SCARD key							--获得集合中元素的个数

SPOP key							--从集合中弹出一个元素,由于集合是无序的,所有SPOP命令会从集合中随机选择一个元素弹出
Zset(sortedset)类型

在set集合类型的基础上,有序集合类型为集合中的每个元素都关联一个分数。这使得我们不仅可以完成插入、删除和判断元素是否存在集合中,还能够获得分数最高或最低的前N个元素,获取指定分数范围内的元素等与分数有关的操作。

常用命令

ZADD key score1 member1  score2 member2....	--增加元素
向有序集合中加入一个元素和该元素的分数,如果该元素已经存在则会用新的分数替换原有的分数。返回值是新加入到集合中的元素的个数,不包含之前已经存在的元素。

ZRANGE/ZREVRANGE
获得排名在某个范围的元素列表
	-ZRANGE:按照元素分数从小到大的顺序返回索引从start到stop之间的所有元素(包含两端的元素)
	-ZREVRANGE:按照元素分数从大到小的顺序返回索引从start到stop之间的所有元素(包含两端的元素)
ZRANGE key start stop 0 2				--0-2之间的
ZREVRANGE key start stop 0 2			--0-2之间的
如果需要获得元素的分数,可以在命令尾部机上WITHSCORES参数
ZRANGE key start stop 0 2 WITHSCORES	--0到2之间的
ZREVRANGE key start stop 0 2 WITHSCORES	--0-2之间的

ZSCORE key member						--获取元素的分数

ZREM key member1 member2 ...			--删除key集合中的一个或多个成员,不存在的成员将被忽略,当key存在但不是有序集合时,返回一个错误

ZRANGEBYSCORE key min max [WITHSCORES]	--获得指定分数范围的元素min max 表示范围

ZINCRBY key increment member 			--增加某个元素的分数,返回值为修改后的分数

ZCARD key								--获得集合中元素的数量

ZCOUNT key min max						--获得指定范围内的元素个数

ZREMRANGEBYRANK key start stop			--按照排名范围删除元素

ZREMRANGEBYSCORE key min max			--按照分数范围删除元素

ZRANK key member						--获取元素的排名从小到大
ZREVRANK key member						--获取元素的排名从大到小


Zset可以应用到商品销售的排行榜
通用命令
keys pattern 							--返回给定的pattern的所有的key

DEL key									--删除key

exists									--确认key是否存在

Redis在实际使用过程中更多的用作缓存,缓存数据一般都是要设置过期时间的
expire
EXPIRE key seconds						--设置key的生存时间(秒) key在多少秒后会自动删除
TTl key									--查看key剩余的生存时间
PERSIST key								--清楚key的生存时间
PEXPIRE key milliseconds				--设置key的生存时间为 (毫秒)

RENAME oldkey	newkey					--修改oldkey的名字为newkey

TYPE key 								--显示指定key的数据类型

Redis消息模式

队列模式

使用list类型的lpush和rpop实现消息队列

  • 消息接收方如果不知道队列中有消息,会一致发送rpop命令,这样每一次都建立一次连接,不太好
  • 可以使用brpop命令,如果从队列中取不出来数据,会一致阻塞,在一定范围内没有取出则返回null
发布订阅模式
#订阅消息
subscribe message1
#发布消息
publish message1 “发布消息”

Redis事务

  • Redis的事务是通过MULTI、EXEC、DISCARD、WATCH、UNWATCH 五个命令完成的
  • Redis的单个命令都是原子性的,所以这里需要确保事务性的对象是命令集合
  • Redis将命令集合序列化并确保初一统一事务的命令集合连续且不被打断的执行
  • Redis不支持回滚操作
事务命令

MULTI

  1. 用于标记事务模块的开始
  2. Redis会将后续的命令逐个放入队列中,然后使用EXEC命令原子化的执行这个命令序列

EXEC

在一个事务中执行所有先放入队列的命令,然后恢复正常的连接状态

DISSCARD

清除所有先前在一个事务中放入的命令队列,然后恢复正常的连接状态

WATCH

当某个[事务按条件执行]时,就要使用这个命令给定的[key(键)]设置为受监控状态,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断

UNWATCH

清除所有先前为一个事务监控的key

Redis的事务执行
  1. Multi开启事务
  2. 向队列中逐个加入需要执行的命令
  3. EXEC会先检查队列中每个命令的正确性,如果命令格式是错误的,将停止事务,加入的命令也不会执行。
  4. 如果是因为数据结构引起的错误,错误的这条命令将执行失败,而其他正确的命令将会执行成功,所以说Redis的事务是不支持回滚的

Redis事务应用——Redis乐观锁

利用redis乐观锁可以实现秒杀,Redis乐观锁是Redis事务的经典应用。

秒杀场景描述:

  • 秒杀活动对稀缺或者特价的商品进行定时,定量售卖,吸引成大量的消费者进行抢购,但又只有少部分 消费者可以下单成功。因此,秒杀活动将在较短时间内产生比平时大数十倍,上百倍的页面访问流量和下单请求流量。
  • 由于秒杀只有少部分请求能够成功,而大量的请求是并发产生的,所以如何确定哪个请求成功了,就是由redis乐观锁来实现。具体思路如下: 监控锁定量,如果该值被修改成功则表示该请求被通过,反之表示该请求未通过。
  • 从监控到修改到执行都需要在redis里操作,这样就需要用到Redis事务。

乐观锁基于CAS(Compare And Swap)思想(比较并替换),是不具有互斥性,不会产生锁等待而消耗资源,但是需要反复的重试,但也是因为重试的机制,能比较快的响应。因此我们可以利用redis来实现乐观锁。

思路如下:

  • 利用redis的watch功能,监控这个redisKey的状态值
  • 获取redisKey的值
  • 创建Redis事务
  • 给这个key的值+1
  • 然后去执行这个事务,如果key的值被修改过则回滚,key不加1

Java模拟并发下的秒杀

@Test
    public void redisLock() {
        String redisKey = "second";
        ExecutorService executorService = Executors.newFixedThreadPool(20);
        try {
            Jedis jedis = new Jedis("127.0.0.1", 6379);
            // 初始值
            jedis.set(redisKey, "0");
            jedis.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        for (int i = 0; i < 100; i++) {
             executorService.execute(() -> {
                Jedis jedis1 = new Jedis("127.0.0.1", 6379);
                try {
                    jedis1.watch(redisKey);
                    String redisValue = jedis1.get(redisKey);
                    int valInteger = Integer.valueOf(redisValue);
                    String userInfo = UUID.randomUUID().toString();
                    // 没有秒完
                    if (valInteger < 20) {
                        Transaction tx = jedis1.multi();
                        tx.incr(redisKey);
                        List list = tx.exec();
                        // 秒成功 失败返回空list而不是空
                        if (list != null && list.size() > 0) {
                            System.out.println("用户:" + userInfo + ",秒杀成功!当前成功人数:" + (valInteger + 1));
                        }
                        // 版本变化,被别人抢了。
                        else {
                            System.out.println("用户:" + userInfo + ",秒杀失败");
                        }
                    }
                    // 秒完了
                    else {
                        System.out.println("已经有20人秒杀成功,秒杀结束");
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    jedis1.close();
                }
            });
        }
        executorService.shutdown();
    }