Redis_学习笔记(2024.04)

学习视频来自于:


1.NoSQL数据库简介

1.1 技术发展

技术的分类

  • 解决功能性的问题:Java、Jsp、RDBMS、Tomcat、HTML、Linux、JDBC、SVN
  • 解决扩展性的问题:Struts、Spring、SpringMVC、Hibernate、Mybatis
  • 解决性能的问题:NoSQL、Java线程、Hadoop、Nginx、MQ、ElasticSearch

Web1.0时代

Web1.0的时代,数据访问量很有限,用一夫当关的高性能的单点服务器可以解决大部分问题。

                        

Web2.0时代

随着Web2.0的时代的到来,用户访问量大幅度提升,同时产生了大量的用户数据。加上后来的智能移动设备的普及,所有的互联网平台都面临了巨大的性能挑战。

解决Web2.0问题

  • 解决CPU及内存压力

  • 解决IO压力

1.2 NoSQL数据库

NoSQL数据库概述

NoSQL(Not Only SQL),意即“不仅仅是SQL”,泛指非关系型的数据库。

NoSQL 不依赖业务逻辑方式存储,而以简单的key-value模式存储,因此大大的增加了数据库的扩展能力。

  • 不遵循SQL标准。
  • 不支持ACID(事务的四个特性)。
  • 远超于SQL的性能。

NoSQL适用场景

  • 对数据高并发的读写
  • 海量数据的读写
  • 对数据高可扩展性的

NoSQL不适用场景

  • 需要事务支持
  • 基于sql的结构化查询存储,处理复杂的关系,需要即席查询。
  • 用不着sql的和用了sql也不行的情况,请考虑用NoSql

常见的NoSQL数据库

Memcache

Redis

MongoDB

1.3 行式存储数据库(大数据时代)

行式数据库

列式数据库


 

Hbase

HBase是Hadoop项目中的数据库。
它用于需要对大量的数据进行随机、实时的读写操作的场景中。
HBase的目标就是处理数据量非常庞大的表,可以用普通的计算机处理超过10亿行数据,还可处理有数百万列元素的数据表

Cassandra[kəˈsændrə]

Apache Cassandra是一款免费的开源NoSQL数据库,其设计目的在于管理由大量商用服务器构建起来的庞大集群上的海量数据集(数据量通常达到PB级别)。在众多显著特性当中,Cassandra最为卓越的长处是对写入及读取操作进行规模调整,而且其不强调主集群的设计思路能够以相对直观的方式简化各集群的创建与扩展流程

图关系型数据库

主要应用:社会关系,公共交通网络,地图及网络拓谱

2.Redis概述安装

2.0 概述

  • Redis是一个开源的key-value存储系统。
  • 和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)和hash(哈希类型)。
  • 这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。
  • 在此基础上,Redis支持各种不同方式的排序。
  • 与memcached一样,为了保证效率,数据都是缓存在内存中。
  • 区别的是Redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件。并且在此基础上实现了master-slave(主从)同步

2.1 应用场景

配合关系型数据库做高速缓存

  • 高频次,热门访问的数据,降低数据库IO
  • 分布式架构,做session共享

多样的数据结构存储持久化数据

2.2 Redis安装

Redis官方网站Redis中文官方网站
http://redis.iohttp://redis.cn/
安装版本
  • 6.2.1 for Linux(redis-6.2.1.tar.gz)
  • 不用考虑在windows环境下对Redis的支持

?????????

2.3 Redis介绍相关知识

Redis中默认16个数据库,类似数组下标从0开始,初始默认使用0号库

使用命令 select <dbid>来切换数据库。如: select 8

统一密码管理,所有库同样密码

操作指令:

dbsize 查看当前数据库的key的数量
flushdb 清空当前库
flushall 通杀全部库

Redis是单线程+多路IO复用技术

多路复用是指使用一个线程来检查多个文件描述符(Socket)的就绪状态,比如调用select和poll函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞直到超时。

得到就绪状态后进行真正的操作可以在同一个线程里执行,也可以启动线程执行(比如使用线程池)。

实例讲解

1,2,3都让黄牛帮忙卖票,然后去做自己的事情

串行 vs 多线程+锁(memcached) vs 单线程+多路IO复用(Redis)

与Memcache三点不同: 支持多数据类型,支持持久化,单线程+多路IO复用

3.常用五大数据类型

指的是key-value中,value的类型

3.1 Redis键(key)

进入客户端redis-cli可使用一下命令

keys *					查看当前库所有key (匹配:keys *1)
exists key				判断某个key是否存在
type key				查看你的key是什么类型
del key       			删除指定的key数据
unlink key   			根据value选择非阻塞删除
						仅将keys从keyspace元数据中删除,真正的删除会在后续异步操作。
expire key 10   		10秒钟:为给定的key设置过期时间
ttl key 				查看还有多少秒过期,-1表示永不过期,-2表示已过期

select					命令切换数据库
dbsize					查看当前数据库的key的数量
flushdb					清空当前库
flushall				通杀全部库

keys *:查看当前库所有key (匹配:keys *1)

exists key:判断某个key是否存在

type key:查看你的key是什么类型

del key:删除指定的key数据

unlink key:根据value选择非阻塞删除,仅将keys从keyspace元数据中删除,真正的删除会在后续异步操作

expire key 10:10秒钟:为给定的key设置过期时间

ttl key :查看还有多少秒过期,-1表示永不过期,-2表示已过期

select:命令切换数据库

dbsize:查看当前数据库的key的数量

flushdb:清空当前库

flushall:通杀全部库

3.2 Redis字符串(String)

简介

  • String是Redis最基本的类型,你可以理解成与Memcached一模一样的类型,一个key对应一个value。
  • String类型是二进制安全的。意味着Redis的string可以包含任何数据。比如jpg图片或者序列化的对象。
  • String类型是Redis最基本的数据类型,一个Redis中字符串value最多可以是512M

常用命令

  • set <key><value>:添加键值对

*NX:当数据库中key不存在时,可以将key-value添加数据库
*XX:当数据库中key存在时,可以将key-value添加数据库,与NX参数互斥
*EX:key的超时秒数
*PX:key的超时毫秒数,与EX互斥

  • get <key>:查询对应键值
  • append <key><value>:将给定的<value> 追加到原值的末尾
  • strlen:<key>获得值的长度
  • setnx <key><value>:只有在 key 不存在时,设置 key 的值
  • incr <key>:将 key 中储存的数字值增1;只能对数字值操作,如果为空,新增值为1,具有原子性
  • decr <key>:将 key 中储存的数字值减1;只能对数字值操作,如果为空,新增值为-1
  • incrby / decrby <key><步长>:将 key 中储存的数字值增减<步长>,自定义步长。
  • mset <key1><value1> <key2><value2> ..... :同时设置一个或多个 key-value对
  • mget <key1> <key2> <key3> .....:同时获取一个或多个 value
  • msetnx <key1><value1> <key2><value2> ..... :同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在。具有原子性,有一个失败则都失败
  • getrange <key><起始位置><结束位置>:获得值的范围,类似java中的substring,前包,后包
  • setrange <key><起始位置><value>:用 <value> 覆写<key>所储存的字符串值,从<起始位置>开始(索引从0开始)。

  • setex <key><过期时间><value>:设置键值的同时,设置过期时间,单位秒。
  • getset <key><value>:以新换旧,设置了新值同时获得旧值。

数据结构

String的数据结构为简单动态字符串(Simple Dynamic String,缩写SDS)。

是可以修改的字符串,内部结构实现上类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配.

如图中所示,内部为当前字符串实际分配的空间capacity一般要高于实际字符串长度len。当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时一次只会多扩1M的空间。需要注意的是字符串最大长度为512M。

3.3 Redis列表(List)

简介

  • 单键多值

  • Redis 列表是简单的字符串列表,按照插入顺序排序。

  • 你可以添加一个元素到列表的头部(左边)或者尾部(右边)。

  • 它的底层实际是个双向链表,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差

常用命令

lpush/rpush <key><value1> <value2><value3> .... :从左边/右边插入一个或多个值。

lpop/rpop <key>:从左边/右边吐出一个值。值在键在,值光键亡。

rpoplpush <key1> <key2>:从<key1>列表右边吐出一个值,插到<key2>列表左边。

lrange <key> <start> <stop>:按照索引下标获得元素(从左到右)

lrange <key> 0-1: 0左边第一个,-1右边第一个,(0-1表示获取所有)

lindex <key> <index>:按照索引下标获得元素(从左到右)

llen <key>:获得列表长度

linsert <key> before/after "<value>" "<newvalue>":在<value>的 后面/前面 插入<newvalue>插入值

lrem <key> <n> "<value>":从左边删除n个value(从左到右)

lset <key> <index> <value>:将列表key下标为index的值替换成value

数据结构

List的数据结构为快速链表quickList。

首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是ziplist,也即是压缩列表。它将所有的元素紧挨着一起存储,分配的是一块连续的内存。

当数据量比较多的时候才会改成quicklist,因为普通的链表需要的附加指针空间太大,会比较浪费空间。比如这个列表里存的只是int类型的数据,结构上还需要两个额外的指针prev和next。

Redis将链表和ziplist结合起来组成了quicklist。也就是将多个ziplist使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。

3.4 Redis集合(Set)

简介

Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的
当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。
Redis的Set是string类型的无序集合。它底层其实是一个value为null的hash表,所以添加,删除,查找的复杂度都是O(1)。
一个算法,随着数据的增加,执行时间的长短,如果是O(1),数据增加,查找数据的时间不变

常用命令

sadd <key> <value1> <value2> ..... :将一个或多个 member 元素加入到集合 key 中,已经存在的 member 元素将被忽略
smembers <key>:取出该集合的所有值。
sismember <key><value>:判断集合<key>是否为含有该<value>值,有1,没有0
scard<key>:返回该集合的元素个数。
srem <key><value1><value2> ....: 删除集合中的某个元素。
spop <key>:随机从该集合中吐出一个值,会从集合中删除。
srandmember <key><n>:随机从该集合中取出n个值。不会从集合中删除 。
smove <source-key><destination-key><value>:把集合中一个值从一个集合移动到另一个集合
sinter <key1><key2>:返回两个集合的交集元素。
sunion <key1><key2>:返回两个集合的并集元素。
sdiff <key1><key2>:两个集合的差集元素(在key1中不包含key2中的)

数据结构

Set数据结构是dict字典,字典是用哈希表实现的。
Java中HashSet的内部实现使用的是HashMap,只不过所有的value都指向同一个对象。
Redis的set结构也是一样,它的内部也使用hash结构,所有的value都指向同一个内部值。

3.4 Redis哈希(Hash)

简介

Redis hash 是一个键值对集合。

Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象,类似Java里面的Map<String,Object>

用户ID为查找的key,存储的value用户对象包含姓名,年龄,生日等信息

  • 如果用普通的key/value结构来存储,主要有以下2种存储方式:

  • 通过 key(用户ID) + field(属性标签) 就可以操作对应属性数据了,既不需要重复存储数据,也不会带来序列化和并发修改控制的问题

常用命令

hset <key><field><value>:给<key>集合中的 <field>键赋值<value>

hget <key1><field>:从<key1>集合<field>取出 <value>

hmset <key1> <field1><value1> <field2><value2>... :批量设置hash的值

hexists <key1><field>:查看哈希表 key 中,给定域 field 是否存在。

hkeys <key>:列出该hash集合的所有field

hvals <key>:列出该hash集合的所有value

hincrby <key><field><increment>为哈希表 key 中的域 field 的值加上增量<increment>

hsetnx <key><field><value>:将哈希表 key 中的域 field 的值设置为 value ,当且仅当域 field 不存在 .

数据结构

Hash类型对应的数据结构是两种:ziplist(压缩列表),hashtable(哈希表)。

当field-value长度较短且个数较少时,使用ziplist,否则使用hashtable。

3.5 Redis有序集合Zset(sorted set)

简介

Redis有序集合zset与普通集合set非常相似,是一个没有重复元素的字符串集合。
不同之处是有序集合的每个成员都关联了一个评分(score),这个评分(score)被用来按照从最低分到最高分的方式排序集合中的成员。
集合的成员是唯一的,但是评分可以是重复了 。
因为元素是有序的, 所以你也可以很快的根据评分(score)或者次序(position)来获取一个范围的元素。
访问有序集合的中间元素也是非常快的,因此你能够使用有序集合作为一个没有重复成员的智能列表。

常用命令

zadd <key> <score1><value1> <score2><value2>…:将一个或多个 member 元素及其 score 值加入到有序集 key 当中。

zrange <key><start><stop> [WITHSCORES]:返回有序集 key 中,下标在<start><stop>之间的元素,带WITHSCORES,可以让分数一起和值返回到结果集。

zrangebyscore key min max [withscores] [limit offset count]:返回有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。有序集成员按 score 值递增(从小到大)次序排列。

zrevrangebyscore key max min [withscores] [limit offset count]:同上,改为从大到小排列。

zincrby <key><increment><value>:为元素的score加上增量increment

zrem <key><value>:删除该集合下,指定值的元素

zcount <key><min><max>:统计该集合,分数区间内的元素个数

zrank <key><value>:返回该值在集合中的排名,从0开始。

数据结构

  • SortedSet(zset)是Redis提供的一个非常特别的数据结构

一方面它等价于Java的数据结构Map<String, Double>,可以给每一个元素value赋予一个权重score

一方面它又类似于TreeSet,内部的元素会按照权重score进行排序,可以得到每个元素的名次,还可以通过score的范围来获取元素的列表。

  • zset底层使用了两个数据结构

hash作用就是关联元素value和权重score,保障元素value的唯一性,可以通过元素value找到相应的score值。
跳跃表,跳跃表的目的在于给元素value排序,根据score的范围获取元素列表。

5.Redis的发布和订阅

5.1 什么是发布和订阅

  • Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者 (pub) 发送消息,订阅者 (sub) 接收消息。
  • Redis 客户端可以订阅任意数量的频道

5.2 Redis的发布和订阅

客户端可以订阅频道如下图

当给这个频道发布消息后,消息就会发送给订阅的客户端

5.3 发布订阅命令行实现

打开一个客户端订阅频道一(channel1)

#打开客户端
/usr/local/bin/redis-cli
#订阅频道
SUBSCRIBE channel1

打开另一个客户端,给channel1发布消息hello

#打开客户端
/usr/local/bin/redis-cli
#给channel1发布消息hello
PUBLISH channel1 hello

打开第一个客户端可以看到发送的消息

6.Redis新数据类型

6.1 Bitmaps

简介

现代计算机用二进制(位) 作为信息的基础单位, 1个字节等于8位

例如“abc”字符串是由3个字节组成, 但实际在计算机存储时将其用二进制表示, “abc”分别对应的ASCII码分别是97、 98、 99, 对应的二进制分别是01100001、 01100010和01100011,如下图

合理地使用操作位能够有效地提高内存使用率和开发效率

Redis提供了Bitmaps这个“数据类型”可以实现对位的操作

  1. Bitmaps本身不是一种数据类型, 实际上它就是字符串(key-value) , 但是它可以对字符串的位进行操作
  2. Bitmaps单独提供了一套命令, 所以在Redis中使用Bitmaps和使用字符串的方法不太相同。 可以把Bitmaps想象成一个以位为单位的数组, 数组的每个单元只能存储0和1, 数组的下标在Bitmaps中叫做偏移量

命令 - setbit

  • 格式

setbit<key><offset><value>:设置Bitmaps中某个偏移量的值(0或1)

注:*offset:偏移量从0开始

  • 实例

- 题目:每个独立用户是否访问过网站存放在Bitmaps中, 将访问的用户记做1, 没有访问的用户记做0, 用偏移量作为用户的id

- 解释:设置键的第offset个位的值(从0算起) , 假设现在有20个用户,userid=1, 6, 11, 15, 19的用户对网站进行了访问, 那么当前Bitmaps初始化结果如图

- 实例:unique:users:20201106代表2020-11-06这天的独立访问用户的Bitmaps

- 注意:很多应用的用户id以一个指定数字(例如10000) 开头, 直接将用户id和Bitmaps的偏移量对应势必会造成一定的浪费, 通常的做法是每次做setbit操作时将用户id减去这个指定数字。在第一次初始化Bitmaps时, 假如偏移量非常大, 那么整个初始化过程执行会比较慢, 可能会造成Redis的阻塞

命令 - getbit

  • 格式:

    getbit<key><offset>:获取Bitmaps中某个偏移量的值

  • 实例

    获取id=8的用户是否在2020-11-06这天访问过, 返回0说明没有访问过

注:因为100根本不存在,所以也是返回0

命令 - bitcount

  • 格式:

    bitcount<key>[start end]:统计字符串从start字节到end字节比特值为1的数量

  • 实例1:

    计算2022-11-06这天的独立访问用户数量

  • 实例2:

start和end代表起始和结束字节数, 下面操作计算用户id在第1个字节到第3个字节之间的独立访问用户数, 对应的用户id是11, 15, 19。

  • 实例3:

情景:K1 【01000001 01000000 00000000 00100001】,对应【0,1,2,3】

①bitcount K1 1 2: 统计下标1、2字节组中bit=1的个数,即01000000 00000000中1的个数

②bitcount K1 1 3:统计下标1、2、3字节组中bit=1的个数,即01000000 00000000 00100001中1的个数

③bitcount K1 0 -2 :统计下标0到2(下标倒数第2),字节组中bit=1的个数,即01000001 01000000 00000000中1的个数

  • 注意:

- redis的setbit设置或清除的是bit位置,而bitcount计算的是byte位置

- start 和 end 参数的设置,都可以使用负数值:比如 -1 表示最后一个位,而 -2 表示倒数第二个位

命令 - bitop

  • 格式:

bitop and(or/not/xor) <destkey> [key…]

  • 作用:

bitop是一个复合操作, 它可以做多个Bitmaps的and(交集) 、 or(并集) 、 not(非) 、 xor(异或) 操作并将结果保存在destkey中

  • 实例1
#2020-11-04 日访问网站的userid=1,2,5,9。
setbit unique:users:20201104 1 1
setbit unique:users:20201104 2 1
setbit unique:users:20201104 5 1
setbit unique:users:20201104 9 1


#2020-11-03 日访问网站的userid=0,1,4,9。
setbit unique:users:20201103 0 1
setbit unique:users:20201103 1 1
setbit unique:users:20201103 4 1
setbit unique:users:20201103 9 1

计算出两天都访问过网站的用户数量

bitop and unique:users:and:20201104_03 unique:users:20201103 unique:users:20201104

计算出任意一天都访问过网站的用户数量(例如月活跃就是类似这种) , 可以使用or求并集

Bitmaps与set对比

假设网站有1亿用户, 每天独立访问的用户有5千万, 如果每天用集合类型和Bitmaps分别存储活跃用户可以得到表

数据类型每个用户id占用空间需要存储的用户量全部内存量
集合类型64位5000000064位*50000000 = 400MB
Bitmaps1位1000000001位*100000000 = 12.5MB

很明显, 这种情况下使用Bitmaps能节省很多的内存空间, 尤其是随着时间推移节省的内存还是非常可观的

数据类型一天一个月一年
集合类型400MB12GB144GB
Bitmaps12.5MB375MB4.5GB

但Bitmaps并不是万金油, 假如该网站每天的独立访问用户很少, 例如只有10万(大量的僵尸用户) , 那么两者的对比如下表所示

数据类型每个userid占用空间需要存储的用户量全部内存量
集合类型64位10000064位*100000 = 800KB
Bitmaps1位1000000001位*100000000 = 12.5MB

很显然, 这时候使用Bitmaps就不太合适了, 因为基本上大部分位都是0,虽然只有10w用户,但是10w用户中可能 有人的id是1000w,因此必须要分配1000w位

6.2 HyperLogLog

简介

7.Redis_Jedis_测试

7.1 Jedis所需要的jar包

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

7.2 连接Redis注意事项

  • 禁用Linux的防火墙:Linux(CentOS7)里执行命令

systemctl stop firewalld.service 
systemctl disable firewalld.service  
  • redis.conf中注释掉bind 127.0.0.1 ,然后 protected-mode no

7.3 Jedis常用操作

创建动态的工程

1.创建maven项目

2.引入依赖

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

3.创建测试程序

创建com/duanyf/jedis/JedisDemo1.java,进行测试

public class JedisDemo1 {

  public static void main(String[] args) {
    //创建Jedis对象
    Jedis jedis=new Jedis("192.168.2.135",6379);

    //测试
    String value = jedis.ping();
    System.out.println(value);//PONG
  }
}

7.4 测试相关数据类型

Jedis-API: Key

  @Test
  public void demo1(){
    //创建Jedis对象
    Jedis jedis=new Jedis("192.168.2.135",6379);

    //添加数据
    jedis.set("name","lucy");

    //获取
    String name = jedis.get("name");
    System.out.println("name:"+name);

    //打印所有key
    Set<String> keys = jedis.keys("*");
    for (String key : keys) {
      System.out.println(key);
    }

    //判断key值是否存在
    System.out.println(jedis.exists("name"));

    //查看还有多少秒过期
    System.out.println(jedis.ttl("name"));
  }

Jedis-API: String

  @Test
  public void demo2(){
    //创建Jedis对象
    Jedis jedis=new Jedis("192.168.2.135",6379);

    //设置多个key-value
    jedis.mset("str1","v1","str2","v2","str3","v3");

    //获取多个key的value
    System.out.println(jedis.mget("str1","str2","str3"));
  }

Jedis-API: List

  @Test
  public void demo3(){
    //创建Jedis对象
    Jedis jedis=new Jedis("192.168.2.135",6379);

    //加入数据
    jedis.lpush("key1","v1","v2","v3");

    //获取数据
    List<String> list = jedis.lrange("key1",0,-1);
    for (String element : list) {
      System.out.println(element);
    }
  }

Jedis-API: set

  @Test
  public void demo4(){
    //创建Jedis对象
    Jedis jedis=new Jedis("192.168.2.135",6379);

    //加入数据
    jedis.sadd("orders", "order01");
    jedis.sadd("orders", "order02");
    jedis.sadd("orders", "order03");
    jedis.sadd("orders", "order04");

    //获取数据
    Set<String> smembers = jedis.smembers("orders");
    for (String order : smembers) {
      System.out.println(order);
    }

    //删除元素
    jedis.srem("orders", "order02");

    //判断元素是否还存在
    System.out.println(jedis.sismember("orders", "order02"));
  }

Jedis-API: hash

  @Test
  public void demo5(){
    //创建Jedis对象
    Jedis jedis=new Jedis("192.168.2.135",6379);

    //加入数据
    jedis.hset("hash1","userName","lisi");

    Map<String,String> map = new HashMap<String,String>();
    map.put("telphone","13810169999");
    map.put("address","atguigu");
    map.put("email","abc@163.com");
    jedis.hmset("hash2",map);

    //获取数据
    System.out.println(jedis.hget("hash1","userName"));

    List<String> result = jedis.hmget("hash2", "telphone","email");
    for (String element : result) {
      System.out.println(element);
    }
  }

Jedis-API: zset

  @Test
  public void demo6(){
    //创建Jedis对象
    Jedis jedis=new Jedis("192.168.2.135",6379);

    //加入数据
    jedis.zadd("zset01", 100d, "z3");
    jedis.zadd("zset01", 90d, "l4");
    jedis.zadd("zset01", 80d, "w5");
    jedis.zadd("zset01", 70d, "z6");

    //获取数据
    Set<String> zrange = jedis.zrange("zset01", 0, -1);
    for (String e : zrange) {
      System.out.println(e);
    }
  }

8.Redis_Jedis_实例

完成一个手机验证码功能
要求:
1、输入手机号,点击发送后随机生成6位数字码,2分钟有效
2、输入验证码,点击验证,返回成功或失败
3、每个手机号每天只能输入3次

思路

  • 随机生成6位数字码:使用Random类
  • 2分钟有效:将验证码放入redis,设置过期时间120s
  • 验证:从redis中取出数字码和输入内容的进行比较
  • 每天只能输入3次:
    1. incr每次发送+1
    2. 大于3时,提醒不能发送了

代码实现

package com.duanyf.jedis;

import java.util.Random;
import redis.clients.jedis.Jedis;

/**
 * @Description:
 * @Author: duanyf
 * @DateTime: 2022/9/24 0024 0:01
 */
public class PhoneCode {

  public static void main(String[] args) {
    String phone="123343256";
    //模拟验证码的发送
    verifyCode(phone);

    //getRedisCode(phone,"428476");
  }

  //1.生成6位数字验证码
  public static String getCode() {
    Random random = new Random();
    String code = "";
    for (int i = 0; i < 6; i++) {
      int rand = random.nextInt(10);
      code += rand;
    }
    return code;
  }

  //2.让每个手机每天只能发送三次,验证码放到redis中,设置过期时间
  public static void verifyCode(String phone){
    //创建Jedis对象
    Jedis jedis=new Jedis("192.168.2.135",6379);

    //拼接key
    //手机发送次数
    String phone_sendCount_key="VerifyCode"+phone+":count";
    //验证码key
    String codeKey="VerifyCode"+phone+":code";

    //每个手机每天只能发送三次
    String count=jedis.get(phone_sendCount_key);
    if (count==null){
      //没有发送次数,第一次发送,设置发送次数为1
      //这个发送次数需要持续一天
      jedis.setex(phone_sendCount_key,24*60*60,"1");
    }else if (Integer.parseInt(count)<=2){
      //第一次 为1进入if 第二次还是为1进入此elseif变成2出去 第三次为2还是进入此elseif
      //发送次数+1
      jedis.incr(phone_sendCount_key);
    }else {
      System.out.println("今天发送次数已经超过三次");
      jedis.close();
      return;
    }

    //随机生成验证码放到redis中
    String vcode = getCode();
    jedis.setex(codeKey,120,vcode);
    jedis.close();
  }

  //3.验证码校验
  public static void getRedisCode(String phone,String code){
    //从redis中获取验证码
    Jedis jedis=new Jedis("192.168.2.135",6379);
    String codeKey="VerifyCode"+phone+":code";
    String redisCode = jedis.get(codeKey);
    //判断
    if (redisCode.equals(code)){
      System.out.println("成功");
    }else {
      System.out.println("失败");
    }
    jedis.close();
  }
}

9.Redis与Spring Boot整合

1.创建Springboot项目

2.配置maven(同上)

3.引入依赖

<!-- redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- spring2.X集成redis所需common-pool2-->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.6.0</version>
</dependency>

4.修改pom.xml

5.application.properties配置redis配置

#Redis服务器地址
spring.redis.host=192.168.3.135
#Redis服务器连接端口
spring.redis.port=6379
#Redis数据库索引(默认为0)
spring.redis.database= 0
#连接超时时间(毫秒)
spring.redis.timeout=1800000
#连接池最大连接数(使用负值表示没有限制)
spring.redis.lettuce.pool.max-active=20
#最大阻塞等待时间(负数表示没限制)
spring.redis.lettuce.pool.max-wait=-1
#连接池中的最大空闲连接
spring.redis.lettuce.pool.max-idle=5
#连接池中的最小空闲连接
spring.redis.lettuce.pool.min-idle=0

6.添加redis配置类com/duanyf/redis_springboot/config/RedisConfig.java

@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        template.setConnectionFactory(factory);
//key序列化方式
        template.setKeySerializer(redisSerializer);
//value序列化
        template.setValueSerializer(jackson2JsonRedisSerializer);
//value hashmap序列化
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        return template;
    }

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//解决查询缓存转换异常的问题
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解决乱码的问题),过期时间600秒
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(600))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
                .disableCachingNullValues();
        RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
        return cacheManager;
    }
}

7.创建com/duanyf/redis_springboot/controller/RedisTestController.java

@RestController
@RequestMapping("/redisTest")
public class RedisTestController {

  @Autowired
  private RedisTemplate redisTemplate;

  @GetMapping
  public String testRedis(){
    //设置值到redis中
    redisTemplate.opsForValue().set("name","lucy");
    //从redis中获取值
    String name = (String) redisTemplate.opsForValue().get("name");
    return name;
  }
}

10.Redis事务锁机制_秒杀

10.1 Redis的事务定义

Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
Redis事务的主要作用就是串联多个命令防止别的命令插队。

10.2 Multi、Exec、discard

从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec后,Redis会将之前的命令队列中的命令依次执行,组队的过程中可以通过discard来放弃组队。

实例

组队成功,提交成功

组队成功,放弃组队

10.3 事务的错误处理

组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消。

如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。

10.4 事务冲突的问题

10.4.1 例子

模拟一个场景:有很多人能使用你的账户支付,同时去参加双十一抢购,账户中有1w元

  • 一个请求想给金额减8000
  • 一个请求想给金额减5000
  • 一个请求想给金额减1000

10.4.2 悲观锁

悲观锁(Pessimistic Lock), 顾名思义,就是很悲观

每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。

传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁

实例

10.4.3 乐观锁

乐观锁(Optimistic Lock), 顾名思义,就是很乐观

每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。

乐观锁适用于多读的应用类型,这样可以提高吞吐量。

Redis就是利用这种check-and-set机制实现事务的。

实例

10.4.4 WATCH key [key …]

在执行multi之前,先执行watch key1 [key2],可以监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。(乐观锁)

开启终端一

开启终端二

10.4.4 unwatch

  • 取消 WATCH 命令对所有 key 的监视。
  • 如果在执行 WATCH 命令之后,EXEC 命令或DISCARD 命令先被执行了的话,那么就不需要再执行UNWATCH 了。

10.5 Redis事务三特性

单独的隔离操作

事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

没有隔离级别的概念

队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行

不保证原子性

事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚

11.Redis事务秒杀案例

11.1 案例分析

11.2 环境配置

1.创建一个web项目

2.配置tomcat

3.导入依赖

<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.4.2</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.codehaus.janino/janino -->
<dependency>
    <groupId>org.codehaus.janino</groupId>
    <artifactId>janino</artifactId>
    <version>2.7.8</version>
</dependency>


<!-- https://mvnrepository.com/artifact/org.codehaus.janino/commons-compiler -->
<dependency>
    <groupId>org.codehaus.janino</groupId>
    <artifactId>commons-compiler</artifactId>
    <version>2.7.8</version>
</dependency>


<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.25</version>
</dependency>

<!-- https://mvnrepository.com/artifact/ch.qos.logback/logback-classic -->
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.4-groovyless</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-core</artifactId>
    <version>1.2.4-groovyless</version>
    <scope>compile</scope>
</dependency>

<!-- https://mvnrepository.com/artifact/ch.qos.logback/logback-access -->
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-access</artifactId>
    <version>1.2.3</version>
</dependency>

<!-- https://mvnrepository.com/artifact/log4j/log4j -->
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

<!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>3.1.0</version>
    <scope>provided</scope>
</dependency>

出现找不到类 clean一下

4.配置web.xml

<servlet>
    <description></description>
    <display-name>doseckill</display-name>
    <servlet-name>doseckill</servlet-name>
    <servlet-class>SecKillServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>doseckill</servlet-name>
    <url-pattern>/doseckill</url-pattern>
</servlet-mapping>

11.3 并发秒杀模拟

模拟测试工具ab安装

联网

sudo yum install httpd-tools

无网络

cd  /run/media/root/CentOS 7 x86_64/Packages
#顺序安装
apr-1.4.8-3.el7.x86_64.rpm
apr-util-1.5.2-6.el7.x86_64.rpm
httpd-tools-2.4.6-67.el7.centos.x86_64.rpm

ab参数讲解

#请求数量
-n requests     Number of requests to perform
#并发数量
-c concurrency  Number of multiple requests to make at a time
#提交参数
-p postfile     File containing data to POST. Remember also to set -T
#参数类型
-T content-type Content-type header to use for POST/PUT data, eg.
                    'application/x-www-form-urlencoded'
                    Default is 'text/plain'

实例测试

1.创建postfile,postfile中写有参数

vim postfile

2.设置reids

/usr/local/bin/redis-cli
del sk:0101:user
set sk:0101:qt 10

3.使用ab

#“~/”表示当前路径
#ip填的不是虚拟机 而是本机ip
ab -n 1000 -c 100 -p ~/postfile -T application/x-www-form-urlencoded http://10.211.161.108:8080/seckill/doseckill

4.查看问题

idea中

redis中,库存为负数

11.4 连接池解决连接超时问题

为什么会连接超时

当向redis发送多个请求,redis处理不过来时,可能会出现连接超时问题

连接池

作用:

    节省每次连接redis服务带来的消耗,把连接好的实例反复利用,通过参数管理连接的行为。

参数:

  • MaxTotal:控制一个pool可分配多少个jedis实例,通过pool.getResource()来获取;如果赋值为-1,则表示不限制;如果pool已经分配了MaxTotal个jedis实例,则此时pool的状态为exhausted。
  • maxIdle:控制一个pool最多有多少个状态为idle(空闲)的jedis实例
  • MaxWaitMillis:表示当borrow一个jedis实例时,最大的等待毫秒数,如果超过等待时间,则直接抛JedisConnectionException
  • testOnBorrow:获得一个jedis实例的时候是否检查连接可用性(ping());如果为true,则得到的jedis实例均是可用的

11.5 超卖问题及解决

超卖问题

利用乐观锁淘汰用户,解决超卖问题

11.6 库存遗留问题

产生原因:

使用乐观锁,当一个请求购买了一个茶品后,版本号更新,剩余请求版本号与其不匹配无法购买
有的请求可以成功是在当前请求结束后,库存更新了,版本号自然更新,所以此请求和版本号匹配可以购买
有的请求失败是因为当一个请求未完成时,它也请求,此时版本号没有更新,此请求版本号与其不匹配无法购买

Lua

介绍

  • Lua 是一个小巧的脚本语言,Lua脚本可以很容易的被C/C++ 代码调用,也可以反过来调用C/C++的函数。
  • Lua并没有提供强大的库,一个完整的Lua解释器不过200k,所以Lua不适合作为开发独立应用程序的语言,而是作为嵌入式脚本语言。
  • 很多应用程序、游戏使用LUA作为自己的嵌入式脚本语言,以此来实现可配置性、可扩展性。这其中包括魔兽争霸地图、魔兽世界、博德之门、愤怒的小鸟等众多游戏插件或外挂。

LUA脚本在Redis中的优势

  • 将复杂的或者多步的redis操作写为一个脚本,一次提交给redis执行,减少反复连接redis的次数,提升性能。
  • LUA脚本是类似redis事务,有一定的原子性,不会被其他命令插队,可以完成一些redis事务性的操作。
  • 注意redis的lua脚本功能,只有在Redis 2.6以上的版本才可以使用。
  • 利用lua脚本淘汰用户,解决超卖问题。
  • redis 2.6版本以后,通过lua脚本解决争抢问题,实际上是redis 利用其单线程的特性,用任务队列的方式解决多任务并发问题。

11.8 代码编写

index.jsp页面

<%@ page language="java" contentType="text/html; charset=UTF-8"
         pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <title>Insert title here</title>
</head>
<body>
<h1>iPhone 13 Pro !!!  1元秒杀!!!
</h1>


<form id="msform" action="${pageContext.request.contextPath}/doseckill" enctype="application/x-www-form-urlencoded">
  <input type="hidden" id="prodid" name="prodid" value="0101">
  <input type="button"  id="miaosha_btn" name="seckill_btn" value="秒杀点我"/>
</form>

</body>
<script  type="text/javascript" src="${pageContext.request.contextPath}/script/jquery/jquery-3.1.0.js"></script>
<script  type="text/javascript">
  $(function(){
    $("#miaosha_btn").click(function(){
      var url=$("#msform").attr("action");
      $.post(url,$("#msform").serialize(),function(data){
        if(data=="false"){
          alert("抢光了" );
          $("#miaosha_btn").attr("disabled",true);
        }
      } );
    })
  })
</script>
</html>

单人秒杀

  • SecKill_redis

import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.commons.pool2.impl.GenericObjectPoolConfig;

import ch.qos.logback.core.rolling.helper.IntegerTokenConverter;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.ShardedJedisPool;
import redis.clients.jedis.Transaction;

public class SecKill_redis {
	/**
	 * 秒杀过程
	 * @param uid 用户id
	 * @param prodid 产品id
	 * @return
	 * @throws IOException
	 */
	public static boolean doSecKill(String uid,String prodid) throws IOException {
		//1 uid和prodid非空判断
		if(uid == null || prodid == null) {
			return false;
		}

		//2 连接redis
		Jedis jedis = new Jedis("192.168.2.140",6379);
		//通过连接池得到jedis对象
		//JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
		//Jedis jedis = jedisPoolInstance.getResource();

		//3 拼接key
		//3.1 库存key
		String kcKey = "sk:"+prodid+":qt";
		//3.2 秒杀成功用户key
		String userKey = "sk:"+prodid+":user";

		//监视库存
		//jedis.watch(kcKey);

		//4.获取库存,如果库存null,秒杀还没有开始
		String kc = jedis.get(kcKey);
		if(kc == null) {
			System.out.println("秒杀还没有开始,请等待");
			jedis.close();
			return false;
		}

		// 5.判断用户是否重复秒杀操作
		if(jedis.sismember(userKey, uid)) {
			System.out.println("已经秒杀成功了,不能重复秒杀");
			jedis.close();
			return false;
		}

		//6.判断如果商品数量,库存数量小于1,秒杀结束
		if(Integer.parseInt(kc)<=0) {
			System.out.println("秒杀已经结束了");
			jedis.close();
			return false;
		}

		//7.秒杀过程
		//使用事务
		//Transaction multi = jedis.multi();
		//
		组队操作
		//multi.decr(kcKey);
		//multi.sadd(userKey,uid);
		//
		执行
		//List<Object> results = multi.exec();

		//if(results == null || results.size()==0) {
		//	System.out.println("秒杀失败了....");
		//	jedis.close();
		//	return false;
		//}

		//7.1 库存-1
		jedis.decr(kcKey);
		//7.2 把秒杀成功用户添加清单里面
		jedis.sadd(userKey,uid);

		System.out.println("秒杀成功了..");
		jedis.close();
		return true;
	}
}
  • SecKillServlet
import java.io.IOException;
import java.util.Random;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 秒杀案例
 */
public class SecKillServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;

    public SecKillServlet() {
        super();
    }

	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

		String userid = new Random().nextInt(50000) +"" ;
		String prodid =request.getParameter("prodid");
		
		boolean isSuccess=SecKill_redis.doSecKill(userid,prodid);
		//boolean isSuccess= SecKill_redisByScript.doSecKill(userid,prodid);
		response.getWriter().print(isSuccess);
	}

}
  • redis中加入数据

连接池设置

  • JedisPoolUtil

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

public class JedisPoolUtil {
	private static volatile JedisPool jedisPool = null;

	private JedisPoolUtil() {
	}

	public static JedisPool getJedisPoolInstance() {
		if (null == jedisPool) {
			synchronized (JedisPoolUtil.class) {
				if (null == jedisPool) {
					JedisPoolConfig poolConfig = new JedisPoolConfig();
					poolConfig.setMaxTotal(200);
					poolConfig.setMaxIdle(32);
					poolConfig.setMaxWaitMillis(100*1000);
					poolConfig.setBlockWhenExhausted(true);
					poolConfig.setTestOnBorrow(true);  // ping  PONG
				 
					jedisPool = new JedisPool(poolConfig, "192.168.2.143", 6379, 60000 );
				}
			}
		}
		return jedisPool;
	}

	public static void release(JedisPool jedisPool, Jedis jedis) {
		if (null != jedis) {
			jedisPool.returnResource(jedis);
		}
	}

}

  • SecKill_redis

解决超卖问题

SecKill_redis

import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.commons.pool2.impl.GenericObjectPoolConfig;

import ch.qos.logback.core.rolling.helper.IntegerTokenConverter;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.ShardedJedisPool;
import redis.clients.jedis.Transaction;

public class SecKill_redis {
	/**
	 * 秒杀过程
	 * @param uid 用户id
	 * @param prodid 产品id
	 * @return
	 * @throws IOException
	 */
	public static boolean doSecKill(String uid,String prodid) throws IOException {
		//1 uid和prodid非空判断
		if(uid == null || prodid == null) {
			return false;
		}

		//2 连接redis
		//Jedis jedis = new Jedis("192.168.2.141",6379);
		//通过连接池得到jedis对象
		JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
		Jedis jedis = jedisPoolInstance.getResource();

		//3 拼接key
		//3.1 库存key
		String kcKey = "sk:"+prodid+":qt";
		//3.2 秒杀成功用户key
		String userKey = "sk:"+prodid+":user";

		//监视库存
		jedis.watch(kcKey);

		//4.获取库存,如果库存null,秒杀还没有开始
		String kc = jedis.get(kcKey);
		if(kc == null) {
			System.out.println("秒杀还没有开始,请等待");
			jedis.close();
			return false;
		}

		// 5.判断用户是否重复秒杀操作
		if(jedis.sismember(userKey, uid)) {
			System.out.println("已经秒杀成功了,不能重复秒杀");
			jedis.close();
			return false;
		}

		//6.判断如果商品数量,库存数量小于1,秒杀结束
		if(Integer.parseInt(kc)<=0) {
			System.out.println("秒杀已经结束了");
			jedis.close();
			return false;
		}

		//7.秒杀过程
		//使用事务
		Transaction multi = jedis.multi();

		//组队操作
		//7.1 库存-1
		multi.decr(kcKey);
		//7.2 把秒杀成功用户添加清单里面
		multi.sadd(userKey,uid);

		//执行
		List<Object> results = multi.exec();

		if(results == null || results.size()==0) {
			System.out.println("秒杀失败了....");
			jedis.close();
			return false;
		}

		System.out.println("秒杀成功了..");
		jedis.close();
		return true;
	}
}

使用Lua脚本解决库存遗留问题

  • Lua脚本

local userid=KEYS[1]; 
local prodid=KEYS[2];
local qtkey="sk:"..prodid..":qt";
local usersKey="sk:"..prodid.":usr'; 
local userExists=redis.call("sismember",usersKey,userid);
if tonumber(userExists)==1 then     
  return 2;
end
local num= redis.call("get" ,qtkey);
if tonumber(num)<=0 then 
  return 0; 
else 
  redis.call("decr",qtkey);
  redis.call("sadd",usersKey,userid);
end
return 1;

2表示已经秒杀过

0表示秒杀失败

1表示秒杀成功

  • JedisPoolUtil 同上

  • SecKill_redisByScript

import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.slf4j.LoggerFactory;

import ch.qos.logback.core.joran.conditional.ElseAction;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.ShardedJedisPool;
import redis.clients.jedis.Transaction;

public class SecKill_redisByScript {
	
	private static final  org.slf4j.Logger logger =LoggerFactory.getLogger(SecKill_redisByScript.class) ;

	public static void main(String[] args) {
		JedisPool jedispool =  JedisPoolUtil.getJedisPoolInstance();
 
		Jedis jedis=jedispool.getResource();
		System.out.println(jedis.ping());
		
		Set<HostAndPort> set=new HashSet<HostAndPort>();

	//	doSecKill("201","sk:0101");
	}
	
	static String secKillScript ="local userid=KEYS[1];\r\n" + 
			"local prodid=KEYS[2];\r\n" + 
			"local qtkey='sk:'..prodid..\":qt\";\r\n" + 
			"local usersKey='sk:'..prodid..\":usr\";\r\n" + 
			"local userExists=redis.call(\"sismember\",usersKey,userid);\r\n" + 
			"if tonumber(userExists)==1 then \r\n" + 
			"   return 2;\r\n" + 
			"end\r\n" + 
			"local num= redis.call(\"get\" ,qtkey);\r\n" + 
			"if tonumber(num)<=0 then \r\n" + 
			"   return 0;\r\n" + 
			"else \r\n" + 
			"   redis.call(\"decr\",qtkey);\r\n" + 
			"   redis.call(\"sadd\",usersKey,userid);\r\n" + 
			"end\r\n" + 
			"return 1" ;
			 
	static String secKillScript2 = 
			"local userExists=redis.call(\"sismember\",\"{sk}:0101:usr\",userid);\r\n" +
			" return 1";

	public static boolean doSecKill(String uid,String prodid) throws IOException {

		JedisPool jedispool =  JedisPoolUtil.getJedisPoolInstance();
		Jedis jedis=jedispool.getResource();

		 //String sha1=  .secKillScript;
		String sha1=  jedis.scriptLoad(secKillScript);
		Object result= jedis.evalsha(sha1, 2, uid,prodid);

		  String reString=String.valueOf(result);
		if ("0".equals( reString )  ) {
			System.err.println("已抢空!!");
		}else if("1".equals( reString )  )  {
			System.out.println("抢购成功!!!!");
		}else if("2".equals( reString )  )  {
			System.err.println("该用户已抢过!!");
		}else{
			System.err.println("抢购异常!!");
		}
		jedis.close();
		return true;
	}
}
  • SecKillServlet
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import redis.clients.jedis.Jedis;

/**
 * 秒杀案例
 */
public class SecKillServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;

    public SecKillServlet() {
        super();
    }

	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

		String userid = new Random().nextInt(50000) +"" ;
		String prodid =request.getParameter("prodid");

		//boolean isSuccess=SecKill_redis.doSecKill(userid,prodid);
		boolean isSuccess= SecKill_redisByScript.doSecKill(userid,prodid);
		response.getWriter().print(isSuccess);
	}

}

12.Redis持久化之RDB

官网介绍:http://www.redis.io

12.1 RDB是什么

在指定的时间间隔内将内存中的数据集快照写入磁盘,
也就是行话讲的Snapshot快照
它恢复时是将快照文件直接读到内存里

12.2 Fork

Fork的作用是复制一个与当前进程一样的进程。

新进程的所有数据(变量、环境变量、程序计数器等) 数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程

在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用

出于效率考虑,Linux中引入了“写时复制技术”

一般情况父进程和子进程会共用同一段物理内存,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。

12.3 备份是如何执行的

  • Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。
  • 整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能
  • 如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。
  • RDB的缺点是最后一次持久化后的数据可能丢失。

12.4 RDB持久化流程

12.5 dump.rdb文件

在redis.conf中配置文件名称,默认为dump.rdb

vim /etc/redis.conf

12.6 如何触发RDB快照;保持策略

12.6.1 配置文件中save默认的快照配置

12.6.2 命令save VS bgsave


save :save时只管保存,其它不管,全部阻塞。手动保存。不建议。
bgsave:Redis会在后台异步进行快照操作, 快照同时还可以响应客户端请求。
可以通过lastsave 命令获取最后一次成功执行快照的时间

12.6.3 flushall命令

执行flushall命令,也会产生dump.rdb文件,但里面是空的,无意义

12.6.5 save

格式:save 秒钟 写操作次数
RDB是整个内存的压缩过的Snapshot
RDB的数据结构,可以配置复合的快照触发条件,默认是1分钟内改了1万次,或5分钟内改了10次,或15分钟内改了1次。
禁用:不设置save指令,或者给save传入空字符串

12.6.6 stop-writes-on-bgsave-error

当Redis无法写入磁盘的话,直接关掉Redis的写操作。推荐yes

12.6.7 rdbcompression 压缩文件

  • 对于存储到磁盘中的快照,可以设置是否进行压缩存储。
  • 如果是的话,redis会采用LZF算法进行压缩。
  • 如果你不想消耗CPU来进行压缩的话,可以设置为关闭此功能。
  • 推荐yes

12.6.8 rdbchecksum 检查完整性

  • 在存储快照后,还可以让redis使用CRC64算法来进行数据校验,
  • 这样做会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能
  • 推荐yes.

12.6.9 RDB的备份

  • 先通过config get dir 查询rdb文件的目录
  • 将*.rdb的文件拷贝到别的地方
  • rdb的恢复
  1. 关闭Redis
  2. 先把备份的文件拷贝到工作目录下 cp dump2.rdb dump.rdb
  3. 启动Redis, 备份数据会直接加载

12.7 优缺点

优势

  • 适合大规模的数据恢复
  • 对数据完整性和一致性要求不高更适合使用
  • 节省磁盘空间
  • 恢复速度快

劣势

  • Fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑
  • 虽然Redis在fork时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能。
  • 在备份周期在一定间隔时间做一次备份,所以如果没到备份周期结束Redis意外down掉的话,就会丢失最后一次快照后的所有修改

12.7 停止RDB

动态停止RDB:redis-cli config set save ""
save后给空值,表示禁用保存策略

12.8 总结

  • 27
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值