redis指北

redis

本文整合自:
Redis使用手册
RedisTemplate介绍和用法
redis实现分布式锁
springboot基础(64)利用redisTemplate实现Redis锁

1、引言

随着业务的增长和产品的完善,急速增长的数据给Oracle数据库带来了很大的压力,而随着我们对产品服务质量要求的提高,传统的数据查询方式已无法满足我们需求。为此我们需要寻找另外一种模式来提高数据查询效率。NoSQL内存数据库是最近兴起的新型数据库,它的特点就是把数据放在内存中操作,数据处理速度相对于磁盘提高了好几个量级,因此,通过把经常访问的数据转移到内存数据库中,不但可以缓解Oracle的访问压力,而且可以极大提高数据的访问速度,提高用户体验。

2、概述

Redis是一个开源的,先进的key-value持久化产品。它通常被称为数据结构服务器,它的值可以是字符串(String)、哈希(Map)、列表(List)、集合(Sets)和有序集合(Sorted sets)等类型。可以在这些类型上面做一些原子操作,如:字符串追加、增加Hash里面的值、添加元素到列表、计算集合的交集,并集和差集;或者区有序集合中排名最高的成员。为了取得好的性能,Redis是一个内存型数据库。不限于此,Redis也可以把数据持久化到磁盘中,或者把数据操作指令追加了一个日志文件,把它用于持久化。也可以用Redis容易的搭建master-slave架构用于数据复制。其它让它像缓存的特性包括,简单的check-and-set机制,pub/sub和配置设置。Redis可以用大部分程序语言来操作:CC++C#JavaNode.jsphpruby等等。Redis是用ANSIC写的,可以运行在多数POSIX系统,如:Linux*BSDOS XSoloris等。官方版本不支持Windows下构建,可以选择一些修改过的版本,照样可以使用Redis

3、redis介绍

3.1 五种数据类型

3.1.1 String类型

String是最基本的类型,而且string 类型是二进制安全的。意思是 redis string 可以包含任何数据。比如 jpg 图片或者序列化的对象。从内部实现来看其实 string 可以看作 byte 数组,最大上限是 1G 字节。

string类型数据操作指令简介

set key value # 设置key对应string类型数据的值,返回1表示成功,0失败

setnx key value # 如果key不存在,设置key对应string类型的值。如果key已经存在,返回0

get key # 获取key对应的string值,如果key不存在,返回nil

getset key value # 先获取key的值,再设置key的值。如果key不存在,则返回nil

mget key1 key2 …… keyN # 一次获取多个key的值,如果对应key不存在,则对应返回nil

mset key1 value1 …… keyN valueN # 一次设置多个key的值,成功返回1表示所有的值都设置了,失败返回0表示没有任何值被设置

msetnx key1 value1 …… keyN valueN # 一次设置多个key的值,但是不会覆盖已经存在的key

incr key # 对key的值做++操作,并返回新的值。如果key的值不是int类型,会返回错误。incr一个不存在的key,则设置key的值为1.

decr key # 对key的值做--操作。decr一个不存在的key,则设置key的值为-1

incrby key integer # 对key加上指定值,key不存在的时候会设置key,并认为原来的value是0

decrby key integer # 对key减去指定值。
3.1.2 hash类型

hash是一个string类型的fieldvalue的映射表。添加,删除操作都是O(1)(平均)。 hash特别适合用于存储对象。相对于将对象的每个字段存成单个string类型。将一个对象存储在hash类型中会占用更少的内存,并且可以更方便的存取整个对象。省内存的原因是新建一个hash对象时开始是用 zipmap(又称为 small hash)来存储的。这个 zipmap 其实并不是hashtable,但是zipmap相比正常的hash实现可以节省不少hash本身需要的一些元数据存储开销。尽管zipmap的添加,删除,查找都是 O(n),但是由于一般对象的field 数量都不太多。所以使用zipmap也是很快的,也就是说添加删除平均还是O(1)。如果field 或者 value的大小超出一定限制后,redis会在内部自动将zipmap替换成正常的hash实现.这个限制可以在配置文件中指定。

hash类型数据操作指令简介

hset key field value # 设置hash field为指定值,如果key不存在,则创建

hget key field # 获取指定的hash field

hmget key field1 …… fieldN # 获取全部指定的hash field

hmset key field1 value1 …… fieldN valueN # 同时设置hash的多个field

hincrby key field integer # 将指定的hashfield加上指定的值。成功,返回hashfield变更后的值

hexistkey field # 检测指定field是否存在。

hdelkey field # 删除指定的hash field

hlen key # 返回指定hash的field数量。

hkeys key # 返回hash的所有field

hvals key # 返回hash的所有value

hgetall # 返回hash的所有field和value
3.1.3 List类型

list是一个链表结构,可以理解成一个每个子元素都是string类型的双向链表。主要功能是push、pop、获取一个范围内的所有值等。操作中的key理解为链表的名字。

List类型数据操作指令简介

lpush key string # 在key对应的list的头部添加字符串元素,返回1表示成功,0表示key存在且不是list类型

rpush key string # 在key对应list的尾部添加字符串元素

llen key # 返回key对应list的长度,如果key不存在则返回0,如果key对应类型不是list返回错误。

lrange key start end # 返回指定区间内元素,下标从0开始,负值表示从后面计算,-1表示倒数第一个元素,key不存在返回空列表

ltrim key start end # 截取list指定区间内元素,成功返回1,key不存在返回错误

lset key index value # 设置list中指定下标的元素值,成功返回1,key或者下标不存在返回错误

lrem key count value # 从list的头部(count正数)或尾部(count负数)删除一定数量(count)匹配value的元素,返回删除的元素数量。count为0时候,删除全部。

lpop key # 从list的头部删除并返回删除元素。如果key对应list不存在或者是空,会返回nil;如果key对应值不是list返回错误

rpop key # 从list的尾部删除并返回删除元素

blpop key1 …… keyN timeout从左到右扫描,返回对第一个非空list进行lpop操作并返回,比如blpop lsit1 list2 list3 0,如果list1不存在,list2,list3都是非空,则对list2做lpop并返回从list2中删除的元素。如果所有的list都是空或者不存在,则会阻塞timeout秒,timeout为0表示一直阻塞。当阻塞时,如果有client对key1 …… keyN中的任意key进行push操作,则第一在这个key上被阻塞的client会立即返回。如果超时发生,则返回nil。

brpop # 同blpop,一个是从头部删除,一个是从尾部删除
3.1.4 Set类型

set是无序集合,最大可以包含(2的 32 次方-1)个元素。set 的是通过 hash table 实现的,所以添加,删除,查找的复杂度都是 O(1)。hash table 会随着添加或者删除自动的调整大小。需要注意的是调整 hash table 大小时候需要同步(获取写锁)会阻塞其他读写操作。可能不久后就会改用跳表(skip list)来实现。跳表已经在 sorted sets 中使用了。关于 set 集合类型除了基本的添加删除操作,其它有用的操作还包含集合的取并集(union),交集(intersection),差集(difference)。通过这些操作可以很容易的实现 SNS 中的好友推荐和 blog 的 tag 功能。

set类型数据操作指令简介

sadd key member # 添加一个string元素到key对应set集合中,成功返回1,如果元素已经在集合中则返回0,key对应的set不存在则返回错误。

srem key member # 从key对应set中移除指定元素,成功返回1,如果member在集合中不存在或者key不存在返回0,如果key对应的不是set类型的值返回错误。

spop key # 删除并返回key对应set中随机的一个元素,如果set是空或者key不存在返回 nil。

srandmember key # 同spop,随机取set中的一个元素,但是不删除元素。

smove srckey dstkey member # 从srckey对应set中移除member并添加到dstkey对应set中,整个操作是原子的。成功返回1,如果member在srckey中不存在返回0,如果key不是set 类型返回错误。

scard key # 返回set的元素个数,如果set是空或者key不存在返回0。

sismember key member # 判断member是否在set中,存在返回1,0表示不存在或者key不存在。

sinter key1 key2 …… keyN # 返回所有给定key的交集。

sinterstore dstkey key1 …… keyN # 返回所有给定key的交集,并保存交集存到dstkey下。

sunion key1 key2 …… keyN # 返回所有给定key的并集

sunionstore dstkey key1 …… keyN # 返回所有给定key的并集,并保存并集到dstkey下

sdiff key1 key2 …… keyN # 返回所有给定key的差集

sdiffstore dstkey key1 …… keyN # 返回所有给定key的差集,并保存差集到dstkey下

smembers key # 返回key对应set的所有元素,结果是无序的
3.1.5 Sorted Set

sorted set是有序集合,它在set的基础上增加了一个顺序属性,这一属性在添加修改元素的时候可以指定,每次指定后,会自动重新按新的值调整顺序。可以理解了有两列的 mysql表,一列存value,一列存顺序。操作中key理解为sorted set的名字。

Sorted Set类型数据操作指令简介

add key score member # 添加元素到集合,元素在集合中存在则更新对应score

zrem key member # 删除指定元素,1表示成功,如果元素不存在则返回0

zincrby key incr member # 增加对应member的score值,然后移动元素并保持skip list保持有序。返回更新后的score值

zrank key member # 返回指定元素在集合中的排名(下标),集合中元素是按score从小到大排序的

zrevrank key member # 同上,但是集合中元素是按score从大到小排序的

zrange key start end # 类似lrange操作从集合中去指定区间的元素。返回的是有序结果

zrevrange key start end # 同上,返回结果是按score逆序的

zrangebyscore key min max # 返回集合中score在给定区间的元素

zcountkey min max # 返回集合中score在给定区间的数量

zcard key # 返回集合中元素个数

zscore key element # 返回给定元素对应的score

zremrangebyrank key min max # 删除集合中排名在给定区间的元素

zremrangebyscore key min max # 删除集合中score在给定区间的元素

3.2 Redis主从复制

3.2.1 主从复制介绍

Redis支持将数据同步到多台从库中,这种特性对提高读取性能非常有益。master可以有多个slave。除了多个slave连到相同的master外,slave也可以连接其它slave形成图状结构。主从复制不会阻塞master。也就是说当一个或多个slavemaster进行初次同步数据时,master可以继续处理客户端发来的请求。相反slave在初次同步数据时则会阻塞不能处理客户端的请求。

主从复制可以用来提高系统的可伸缩性,我们可以用多个slave 专门用于客户端的读请求,比如sort操作可以使用slave来处理也可以用来做简单的数据冗余。可以在 master 禁用数据持久化,只需要注释掉 master 配置文件中的所有 save 配置,然后只在 slave 上配置数据持久化。

3.2.2 主从复制过程

当设置好 slave 服务器后,slave 会建立和 master 的连接,然后发送 sync命令。无论是第一次同步建立的连接还是连接断开后的重新连接,master 都会启动一个后台进程,将数据库快照保存到文件中,同时 master 主进程会开始收集新的写命令并缓存起来。后台进程完成写文件后,master 就发送文件给 slaveslave 将文件保存到磁盘上,然后加载到内存恢复数据库快照到 slave 上。接着 master 就会把缓存的命令转发给 slave。而且后续 master 收到的写命令都会通过开始建立的连接发送给slave。从masterslave的同步数据的命令和从客户端发送的命令使用相同的协议格式。当 master 和 slave 的连接断开时 slave 可以自动重新建立连接。如果 master 同时收到多个 slave 发来的同步连接命令,只会启动一个进程来写数据库镜像,然后发送给所有 slave。

配置 slave服务器很简单,只需要在配置文件中加入如下配置

slaveof  192.168.1.1 6379     #指定 master的 ip 和端口。

3.3 Redis持久化

通常Redis将数据存储在内存中或虚拟内存中,但它提供了数据持久化功能可以把内存中的数据持久化到磁盘。持久化有什么好处呢?比如可以保证断电后数据不会丢失,升级服务器也会变得更加方便。Redis提供了两种数据持久化的方式。

3.3.1 RDB Snapshotting方式持久化(默认方式)

这种方式就是将内存中数据以快照的方式写入到二进制文件中,默认的文件名为 dump.rdb。客户端也可以使用save或者bgsave命令通知redis做一次快照持久化。save操作是在主线程中保存快照的,由于redis是用一个主线程来处理所有客户端的请求,这种方式会阻塞所有客户端请求。所以不推荐使用。另一点需要注意的是,每次快照持久化都是将内存数据完整写入到磁盘一次,并不是增量的只同步增量数据。如果数据量大的话,写操作会比较多,必然会引起大量的磁盘IO操作,可能会严重影响性能。

这种方式的缺点也是显而易见的,由于快照方式是在一定间隔时间做一次的,所以如果 redis 意外当机的话,就会丢失最后一次快照后的所有数据修改。

3.3.2 APF方式持久化

这种方式 redis 会将每一个收到的写命令都通过 write 函数追加到文件中(默认 appendonly.aof)。当redis重启时会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。当然由于操作系统会在内核中缓存write做的修改,所以可能不是立即写到磁盘上。这样的持久化还是有可能会丢失部分修改。不过我们可以通过配置文件告诉 redis我们想要通过fsync函数强制操作系统写入到磁盘的时机。有三种方式如下(默认是:每秒fsync一次)

appendonly yes //启动日志追加持久化方式

appendfsync always //每次收到写命令就立即强制写入磁盘,最慢的,但是保证完全持久化,不推荐使用

appendfsync everysec //每秒钟强制写入磁盘一次,在性能和持久化方面做了很好的折中,推荐使用

appendfsync no //完全依赖操作系统,性能最好,持久化没保证

日志追加方式同时带来了另一个问题。持久化文件会变的越来越大。例如我们调用 incr test 命令 100 次,文件中必须保存全部 100 条命令,其实有 99 条都是多余的。因为要恢复数据库状态其实文件中保存一条 set test 100 就够了。为了压缩这种持久化方式的日志文件。 redis 提供了 bgrewriteaof 命令。收到此命令 redis 将使用与快照类似的方式将内存中的数据以命令的方式保存到临时文件中,最后替换原来的持久化日志文件。

3.4 Redis虚拟内存

3.4.1 虚拟内存介绍

首先说明下redis的虚拟内存与操作系统虚拟内存不是一码事,但是思路和目的都是相同的。就是暂时把不经常访问的数据从内存交换到磁盘中,从而腾出宝贵的内存空间。对于 redis这样的内存数据库,内存总是不够用的。除了可以将数据分割到多个redis 服务器以外。另外的能够提高数据库容量的办法就是使用虚拟内存技术把那些不经常访问的数据交换到磁盘上。如果我们存储的数据总是有少部分数据被经常访问,大部分数据很少被访问,对于网站来说确实总是只有少量用户经常活跃。当少量数据被经常访问时,使用虚拟内存不但能提高单台redis数据库服务器的容量,而且也不会对性能造成太多影响。

redis没有使用操作系统提供的虚拟内存机制而是自己在用户态实现了自己的虚拟内存机制。主要的理由有以下两点,操作系统的虚拟内存是以4k/页为最小单位进行交换的。而redis的大多数对象都远小于4k,所以一个操作系统页上可能有多个redis对象。另外redis的集合对象类型如 list,set可能存在于多个操作系统页上。最终可能造成只有10%的key被经常访问,但是所有操作系统页都会被操作系统认为是活跃的,这样只有内存真正耗尽时操作系统才会进行页的交换。相比操作系统的交换方式。redis可以将被交换到磁盘的对象进行压缩,保存到磁盘的对象可以去除指针和对象元数据信息。一般压缩后的对象会比内存中的对象小 10倍。这样redis的虚拟内存会比操作系统的虚拟内存少做很多IO操作。

3.4.2 虚拟内存相关设置
vm-enabled yes #开启虚拟内存功能
vm-swap-file /tmp/redis.swap #交换出来value保存的文件路径/tmp/redis.swap
vm-max-memory 268435456 #redis使用的最大内存上限(256MB),超过上限后redis开始交换value到磁盘swap文件中。建议设置为系统空闲内存的60%-80%
vm-page-size 32 #每个redis页的大小32个字节
vm-pages 134217728 #最多在文件中使用多少个页,交换文件的大小=(vm-page-size*vm-pages)4GB
vm-max-threads 8 #用于执行value对象换入换出的工作线程数量。0表示不使用工作线程

3.5 其他参考资料

所有特性请参考:http://redis.io/documentation

所有命令请参考:http://redis.io/commands#sorted_set

4、环境安装

4.1 下载、解压、安装

$ wget http://download.redis.io/releases/redis-2.8.13.tar.gz
$ tar xzf redis-2.8.13.tar.gz
$ cd redis-2.8.13
$ make
4.2 启动Redis实例
$ src/redis-server

4.3 通过内置客户端进行测试

$ src/redis-cli

redis> set foo bar
OK

redis>get foo
"bar"

5、配置文件

  1. Redis默认不是以守护进程的方式运行,可以通过该配置项修改,使用yes启用守护进程
daemonize yes
  1. Redis以守护进程方式运行时,Redis默认会把pid写入/var/run/redis.pid文件,可以通过pidfile指定
pidfile /usr/local/redis/var/redis.pid
  1. 指定Redis监听端口,默认端口为6379,作者在自己的一篇博文中解释了为什么选用6379作为默认端口,因为6379在手机按键上MERZ对应的号码,而MERZ取自意大利歌女Alessia Merz的名字。
port 6379
  1. 绑定的主机地址
bind 127.0.0.1
  1. 当客户端闲置多长时间后关闭连接,如果指定为0,表示关闭该功能
timeout
  1. 对客户端发送ACK信息,linux中单位为秒
tcp-keepalive 0
  1. 指定日志记录级别,Redis总共支持四个级别:debugverbosenoticewarning,默认为notice
loglevel notice
  1. 日志记录位置,默认为标准输出
logfile /usr/local/redis/var/redis.log
  1. 设置数据库的数量,默认数据库为0,可以使用SELECT 命令在连接上指定数据库id
databases 16
  1. 指定在多长时间内,有多少次更新操作,就将数据同步到数据文件,可以多个条件配合

Save分别表示900秒(15分钟)内有1个更改,300秒(5分钟)内有10个更改以及60秒内有10000个更改。

Redis默认配置文件中提供了三个条件:

save 900 1
save 300 10
save 60 10000
  1. 持久化失败以后,redis是否停止
stop-writes-on-bgsave-error yes
  1. 指定存储至本地数据库时是否压缩数据,默认为yes,Redis采用LZF压缩,如果为了节省CPU时间,可以关闭该选项,但会导致数据库文件变的巨大
rdbcompression yes
rdbchecksum yes
  1. 指定本地数据库文件名,默认值为dump.rdb
dbfilename dump.rdb
  1. 指定本地数据库存放目录
dir /usr/local/redis/var
  1. 设置当本机为slave服务时,设置master服务的IP地址及端口,在Redis启动时,它会自动从master进行数据同步
slaveof <host> <port>
  1. 当master服务设置了密码保护时,slave服务连接master的密码
masterauth <password>
  1. 设置Redis连接密码,如果配置了连接密码,客户端在连接Redis时需要通过AUTH 命令提供密码,默认关闭
requirepass foobared
  1. 设置同一时间最大客户端连接数,在 Redis2.4中,最大连接数是被直接硬编码在代码里面的,而在2.6版本中这个值变成可配置的。maxclients 的默认值是 10000,你也可以在 redis.conf 中对这个值进行修改。当然,这个值只是 Redis 一厢情愿的值,Redis 还会照顾到系统本身对进程使用的文件描述符数量的限制。在启动时 Redis 会检查系统的 soft limit,以查看打开文件描述符的个数上限。如果系统设置的数字,小于咱们希望的最大连接数加32,那么这个 maxclients 的设置将不起作用,Redis 会按系统要求的来设置这个值。(加32是因为 Redis 内部会使用最多32个文件描述符,所以连接能使用的相当于所有能用的描述符号减32)。当上面说的这种情况发生时(maxclients 设置后不起作用的情况),Redis 的启动过程中将会有相应的日志记录。比如下面命令希望设置最大客户端数量为10000,所以 Redis 需要 10000+32 个文件描述符,而系统的最大文件描述符号设置为10144,所以 Redis 只能将maxclients 设置为 10144 – 32 = 10112。
maxclients 10000
  1. 指定Redis最大内存限制,Redis在启动时会把数据加载到内存中,达到最大内存后,Redis会先尝试清除已到期或即将到期的Key,当此方法处理后,仍然到达最大内存设置,将无法再进行写入操作,但仍然可以进行读取操作。Redis新的vm机制,会把Key存放内存,Value会存放在swap区
maxmemory
slave-serve-stale-data yes
slave-read-only yes
repl-disable-tcp-nodelay no
slave-priority 100
  1. 指定是否在每次更新操作后进行日志记录,Redis在默认情况下是异步的把数据写入磁盘,如果不开启,可能会在断电时导致一段时间内的数据丢失。因为Redis本身同步数据文件是按上面slave条件来同步的,所以有的数据会在一段时间内只存在于内存中。默认为no
appendonly no
  1. 指定更新日志文件名,默认为appendonly.aof
appendfilename appendonly.aof
  1. 指定更新日志条件,共有3个可选值:

no:表示等操作系统进行数据缓存同步到磁盘(快)

always:表示每次更新操作后手动调用fsync()将数据写到磁盘(慢,安全)

everysec:表示每秒同步一次(折衷,默认值)

appendfsync everysec
  1. 指定是否启用虚拟内存机制,默认值为no,简单的介绍一下,VM机制将数据分页存放,由Redis将访问量较少的页即冷数据swap到磁盘上,访问多的页面由磁盘自动换出到内存中(在后面的文章我会仔细分析Redis的VM机制)
vm-enabled no
  1. 虚拟内存文件路径,默认值为/tmp/redis.swap,不可多个Redis实例共享
vm-swap-file /tmp/redis.swap
  1. 将所有大于vm-max-memory的数据存入虚拟内存,无论vm-max-memory设置多小,所有索引数据都是内存存储的(Redis的索引数据就是keys),也就是说,当vm-max-memory设置为0的时候,其实是所有value都存在于磁盘。
vm-max-memory 0
  1. Redis swap文件分成了很多的page,一个对象可以保存在多个page上面,但一个page上不能被多个对象共享,vm-page-size是要根据存储的 数据大小来设定的,作者建议如果存储很多小对象,page大小最好设置为32或者64bytes;如果存储很大大对象,则可以使用更大的page,如果不确定,就使用默认值
vm-page-size 32
  1. 设置swap文件中的page数量,由于页表(一种表示页面空闲或使用的bitmap)是在放在内存中的,,在磁盘上每8个pages将消耗1byte的内存。
vm-pages 134217728
  1. 设置访问swap文件的线程数,最好不要超过机器的核数,如果设置为0,那么所有对swap文件的操作都是串行的,可能会造成比较长时间的延迟。默认值为4
vm-max-threads 4
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
lua-time-limit 5000
slowlog-log-slower-than 10000
slowlog-max-len 128
  1. 指定在超过一定的数量或者最大的元素超过某一临界值时,采用一种特殊的哈希算法
hash-max-ziplist-entries 512

hash-max-ziplist-value 64

list-max-ziplist-entries 512

list-max-ziplist-value 64

set-max-intset-entries 512

zset-max-ziplist-entries 128

zset-max-ziplist-value 64
  1. 指定是否激活重置哈希,默认为开启(后面在介绍Redis的哈希算法时具体介绍)
activerehashing yes

client-output-buffer-limit normal 0 00

client-output-buffer-limit slave256mb 64mb 60

client-output-buffer-limit pubsub32mb 8mb 60

hz 10
————————————————

                            版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
                        
原文链接:https://blog.csdn.net/suifeng3051/article/details/38657613

6、使用

6.1 Jedis

6.1.1 pom.xml导入依赖

在java中使用Redis,可以使用Jedis库

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>版本号</version>
</dependency>
6.1.2 使用
import redis.clients.jedis.Jedis;

public class RedisExample {
    public static void main(String[] args) {
        //连接到redis服务器
        Jedis jedis = new Jedis("<url>", <端口>);
        
        //设置键值对
        jedis.set("key", "value");
        
        //获取键对应的值
        String value = jedis.get("key");
        System.out.println("获取键‘key’对应的值:" + value);
        
        //关闭Jedis连接
        jedis.close();
    }
}

6.2 spring-data-redis

6.2.1 功能介绍

jedis客户端在编程实施方面存在如下不足

  1. connection管理缺乏自动化,connection-pool的设计缺少必要的容器支持。
  2. 数据操作需要关注“序列化”/“反序列化”,因为jedis的客户端API接受的数据类型为stringbyte,对结构化数据(json,xml,pojo等)操作需要额外的支持。
  3. 事务操作纯粹为硬编码。
  4. pub/sub功能,缺乏必要的设计模式支持,对于开发者而言需要关注的太多。

spring-data-redis针对jedis提供了如下功能:

  1. 连接池自动管理,提供了一个高度封装的”Redis Template"类

  2. 针对jedis客户端中大量api进行了归类封装,将同一类型操作封装为operation接口

    ValueOperations:简单K-V操作
    SetOperations:set类型数据操作
    ZSetOperations:zset类型数据操作
    HashOperations:针对map类型的数据操作
    ListOperations:针对list类型的数据操作
    
  3. 提供了对key的“bound”(绑定)便捷化操作API,可以通过bound封装指定的key,然后进行一系列的操作而无须“显式”的再次指定Key,即

    BoundKeyOperations:
    BoundValueOperations
    BoundSetOperations
    BoundListOperations
    BoundSetOperations
    BoundHashOperations
    
  4. 将事务操作封装,由容器控制

  5. 针对数据的“序列化/反序列化”,提供了多种可选择策略(RedisSerializer

    • JdkSerializationRedisSerializer:POJO对象的存取场景,使用JDK本身序列化机制,将pojo类通过ObjectInputStream/ObjectOutputStream进行序列化操作,最终redis-server中将存储字节序列。是目前最常用的序列化策略。
    • StringRedisSerializer:Key或者value为字符串的场景,根据指定的charset对数据的字节序列编码成string,是“new String(bytes, charset)”和“string.getBytes(charset)”的直接封装。是最轻量级和高效的策略。
    • JacksonJsonRedisSerializerjackson-json工具提供了javabeanjson之间的转换能力,可以将pojo实例序列化成json格式存储在redis中,也可以将json格式的数据转换成pojo实例。因为jackson工具在序列化和反序列化时,需要明确指定Class类型,因此此策略封装起来稍微复杂。【需要jackson-mapper-asl工具支持】
    • OxmSerializer:提供了将javabeanxml之间的转换能力,目前可用的三方支持包括jaxbapache-xmlbeansredis存储的数据将是xml工具。不过使用此策略,编程将会有些难度,而且效率最低;不建议使用。【需要spring-oxm模块的支持】

    针对“序列化和发序列化”中JdkSerializationRedisSerializerStringRedisSerializer是最基础的策略,原则上,我们可以将数据存储为任何格式以便应用程序存取和解析(其中应用包括apphadoop等其他工具),不过在设计时仍然不推荐直接使用用“JacksonJsonRedisSerializer”和“OxmSerializer”,因为无论是json还是xml,他们本身仍然是String。如果你的数据需要被第三方工具解析,那么数据应该使用StringRedisSerializer而不是JdkSerializationRedisSerializer。如果你的数据格式必须为json或者xml那么在编程级别,在redisTemplate配置中仍然使用StringRedisSerializer,在存储之前或者读取之后,使用“SerializationUtils”工具转换转换成json或者xml

  6. 基于设计模式,和JMS开发思路,将pub/sub的API设计进行了封装,使开发更加便捷。

  7. spring-data-redis中,并没有对sharding提供良好的封装,如果你的架构是基于sharding,那么你需要自己去实现,这也是sdrjedis相比,唯一缺少的特性。

6.2.2 使用
6.2.2.1 pom.xml导入依赖
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
6.2.2.2 String操作

set void set(K key, V value);

添加获取数据

redisTemplate.opsForValue().set("numTest_1","123");//设置key和value
String getRedisStr = redisTemplate.opsForValue().get("numTest_1");//根据key读value
System.out.println("getRedisStr---------"+getRedisStr);//123

set void set(K key, V value, long timeout, TimeUnit unit);
设置数据有效期

redisTemplate.opsForValue().set("numTest_2","456",10, TimeUnit.SECONDS);
String getRedisStrTime = redisTemplate.opsForValue().get("numTest_2");//设置的是10秒失效,十秒之内查询有结果,十秒之后返回为null
System.out.println("getRedisStrTime---------"+getRedisStrTime);//456
//        TimeUnit.DAYS          //天
//        TimeUnit.HOURS         //小时
//        TimeUnit.MINUTES       //分钟
//        TimeUnit.SECONDS       //秒
//        TimeUnit.MILLISECONDS  //毫秒

set void set(K key, V value, long offset);
根据key设置value指定下标字符

redisTemplate.opsForValue().set("keyTest_1","hello world");
//覆写(overwrite)给定 key 所储存的字符串值,从偏移量 offset 开始  6:偏移下标
redisTemplate.opsForValue().set("keyTest_1","redis", 6);
String getRedisOffsetStr = redisTemplate.opsForValue().get("keyTest_1");
System.out.println("getRedisOffsetStr---------"+getRedisOffsetStr);//hello redis

get V get(Object key);
Get操作

template.opsForValue().set("key","hello world");
System.out.println("---------"+template.opsForValue().get("key"));//hello world

getAndSet V getAndSet(K key, V value);
设置键的字符串值并返回其旧值

redisTemplate.opsForValue().set("SetTest","test");
String getRedisAndSet = redisTemplate.opsForValue().getAndSet("SetTest", "test2");
System.out.println("getRedisAndSet---------"+getRedisAndSet);//test

append Integer append(K key, String value);
根据key拼接value的值,如果被拼接的key没值则为空字符串

redisTemplate.opsForValue().append("test","Hello");	
String getRedisAppend_1 = redisTemplate.opsForValue().get("test");
System.out.println("getRedisAppend_1---------"+getRedisAppend_1);//Hello

redisTemplate.opsForValue().append("test","world");
String getRedisAppend_2 = redisTemplate.opsForValue().get("test"); 
System.out.println("getRedisAppend_2---------"+getRedisAppend_2);//Helloworld

size Long size(K key);
根据key获取value的长度

redisTemplate.opsForValue().set("key","hello world");
Long getRedisStrSize = redisTemplate.opsForValue().size("key");
System.out.println("getRedisStrSize---------"+getRedisStrSize);//11
6.2.2.3 List操作

Long size(K key);
返回存储在键中的列表的长度。如果键不存在,则将其解释为空列表,并返回0。当key存储的值不是列表时返回错误。

System.out.println(template.opsForList().size("list"));//0

Long leftPush(K key, V value);
(从左边插入)将所有指定的值插入存储在键的列表的头部。如果键不存在,则在执行推送操作之前将其创建为空列表。

template.opsForList().leftPush("list","java");
template.opsForList().leftPush("list","python");
template.opsForList().leftPush("list","c++");
//返回的结果为推送操作后的列表长度,推送三次,分次为123

Long leftPushAll(K key, V... values);
(从左边插入)批量把一个数组插入到列表中

String[] strs = new String[]{"1","2","3"};
template.opsForList().leftPushAll("list",strs);
System.out.println(template.opsForList().range("list",0,-1));//[3, 2, 1]

Long rightPush(K key, V value);
(从右边插入)将所有指定的值插入存储在键的列表的头部。如果键不存在,则在执行推送操作之前将其创建为空列表。(从右边插入)

template.opsForList().rightPush("listRight","java");
template.opsForList().rightPush("listRight","python");
template.opsForList().rightPush("listRight","c++");
//返回的结果为推送操作后的列表长度,推送三次,分次为123

Long rightPushAll(K key, V... values);

(从右边插入)批量把一个数组插入到列表中

String[] strs = new String[]{"1","2","3"};
template.opsForList().rightPushAll("list",strs);
System.out.println(template.opsForList().range("list",0,-1));//[1, 2, 3]

void set(K key, long index, V value);
在列表中index的位置设置value值

System.out.println(template.opsForList().range("listRight",0,-1));
template.opsForList().set("listRight",1,"setValue");
System.out.println(template.opsForList().range("listRight",0,-1));
//[java, python, oc, c++]
//[java, setValue, oc, c++]

Long remove(K key, long count, Object value);
从存储在键中的列表中删除等于值的元素的第一个计数事件。
计数参数以下列方式影响操作:
count> 0:删除等于从头到尾移动的值的元素。
count <0:删除等于从尾到头移动的值的元素。
count = 0:删除等于value的所有元素。

System.out.println(template.opsForList().range("listRight",0,-1));
template.opsForList().remove("listRight",1,"setValue");//将删除列表中存储的列表中第一次出现的“setValue”。
System.out.println(template.opsForList().range("listRight",0,-1));
[java, setValue, oc, c++]
[java, oc, c++]

V index(K key, long index);
根据下标获取列表中的值,下标是从0开始的,-1为获取全部

System.out.println(template.opsForList().range("listRight",0,-1));
System.out.println(template.opsForList().index("listRight",2));
//[java, oc, c++]
//c++

V leftPop(K key);
弹出最左边的元素,弹出之后该值在列表中将不复存在

System.out.println(template.opsForList().range("list",0,-1));
System.out.println(template.opsForList().leftPop("list"));
System.out.println(template.opsForList().range("list",0,-1));
//[c++, python, oc, java, c#]
//c++
//[python, oc, java, c#]

V rightPop(K key);
弹出最右边的元素,弹出之后该值在列表中将不复存在

System.out.println(template.opsForList().range("list",0,-1));
System.out.println(template.opsForList().rightPop("list"));
System.out.println(template.opsForList().range("list",0,-1));
//[python, oc, java, c#]
//c#
//[python, oc, java]
6.2.2.4 Hash操作

Long delete(H key, Object... hashKeys);
删除给定的哈希hashKeys

System.out.println(template.opsForHash().delete("redisHash","name"));
System.out.println(template.opsForHash().entries("redisHash"));
//1
//{class=6, age=28.1}

Boolean hasKey(H key, Object hashKey);
判断哈希hashKey是否存在

System.out.println(template.opsForHash().hasKey("redisHash","666"));
System.out.println(template.opsForHash().hasKey("redisHash","777"));
//true
//false

HV get(H key, Object hashKey);
从键中的哈希获取给定hashKey的值

System.out.println(template.opsForHash().get("redisHash","age"));
//26

Set keys(H key);
获取key所对应的散列表的key

System.out.println(template.opsForHash().keys("redisHash"));
//redisHash所对应的散列表为{class=1, name=666, age=27}
//[name, class, age]

Long size(H key);
获取key所对应的散列表的大小个数

System.out.println(template.opsForHash().size("redisHash"));
//redisHash所对应的散列表为{class=1, name=666, age=27}
//3

void putAll(H key, Map<? extends HK, ? extends HV> m);
使用m中提供的多个散列字段设置到key对应的散列表中

Map<String,Object> testMap = new HashMap();
testMap.put("name","666");
testMap.put("age",27);
testMap.put("class","1");
template.opsForHash().putAll("redisHash1",testMap);
System.out.println(template.opsForHash().entries("redisHash1"));
//{class=1, name=jack, age=27}

void put(H key, HK hashKey, HV value);
设置散列hashKey的值

template.opsForHash().put("redisHash","name","666");
template.opsForHash().put("redisHash","age",26);
template.opsForHash().put("redisHash","class","6");
System.out.println(template.opsForHash().entries("redisHash"));
//{age=26, class=6, name=666}

List values(H key);
获取整个哈希存储的值根据密钥

System.out.println(template.opsForHash().values("redisHash"));
//[tom, 26, 6]

Map<HK, HV> entries(H key);
获取整个哈希存储根据密钥

System.out.println(template.opsForHash().entries("redisHash"));
{age=26, class=6, name=tom}

Cursor<Map.Entry<HK, HV>> scan(H key, ScanOptions options);
使用Cursor在key的hash中迭代,相当于迭代器。

Cursor<Map.Entry<Object, Object>> curosr = template.opsForHash().scan("redisHash",ScanOptions.ScanOptions.NONE);
while(curosr.hasNext()){
    Map.Entry<Object, Object> entry = curosr.next();
    System.out.println(entry.getKey()+":"+entry.getValue());
}
//age:27
//class:6
//name:666
6.2.2.5 Set操作

Long add(K key, V... values);
无序集合中添加元素,返回添加个数
也可以直接在add里面添加多个值 如:template.opsForSet().add("setTest","aaa","bbb")

String[] strs= new String[]{"str1","str2"};
System.out.println(template.opsForSet().add("setTest", strs));
//2

Long remove(K key, Object... values);
移除集合中一个或多个成员

String[] strs = new String[]{"str1","str2"};
System.out.println(template.opsForSet().add("setTest", strs));
System.out.println(template.opsForSet().remove("setTest",strs));
//2
//0

V pop(K key);
移除并返回集合中的一个随机元素

System.out.println(template.opsForSet().pop("setTest"));
System.out.println(template.opsForSet().members("setTest"));
//bbb
//[aaa, ccc]

Boolean move(K key, V value, K destKey);
将 member 元素从 source 集合移动到 destination 集合

template.opsForSet().move("setTest","aaa","setTest2");
System.out.println(template.opsForSet().members("setTest"));
System.out.println(template.opsForSet().members("setTest2"));
//[ccc]
//[aaa]

Long size(K key);
无序集合的大小长度

System.out.println(template.opsForSet().size("setTest"));
//1

Set members(K key);
返回集合中的所有成员

System.out.println(template.opsForSet().members("setTest"));
//[ddd, bbb, aaa, ccc]

Cursor scan(K key, ScanOptions options);
遍历set

Cursor<Object> curosr = template.opsForSet().scan("setTest", ScanOptions.NONE);
  while(curosr.hasNext()){
     System.out.println(curosr.next());
  }
//ddd
//bbb
//aaa
//ccc
6.2.2.6 ZSet操作

Boolean add(K key, V value, double score);
新增一个有序集合,存在的话为false,不存在的话为true

System.out.println(template.opsForZSet().add("zset1","zset-1",1.0));
//true

Long add(K key, Set<TypedTuple> tuples);
新增一个有序集合

ZSetOperations.TypedTuple<Object> objectTypedTuple1 = new DefaultTypedTuple<>("zset-5",9.6);
ZSetOperations.TypedTuple<Object> objectTypedTuple2 = new DefaultTypedTuple<>("zset-6",9.9);
Set<ZSetOperations.TypedTuple<Object>> tuples = new HashSet<ZSetOperations.TypedTuple<Object>>();
tuples.add(objectTypedTuple1);
tuples.add(objectTypedTuple2);
System.out.println(template.opsForZSet().add("zset1",tuples));
System.out.println(template.opsForZSet().range("zset1",0,-1));
//[zset-1, zset-2, zset-3, zset-4, zset-5, zset-6]

Long remove(K key, Object... values);
从有序集合中移除一个或者多个元素

System.out.println(template.opsForZSet().range("zset1",0,-1));
System.out.println(template.opsForZSet().remove("zset1","zset-6"));
System.out.println(template.opsForZSet().range("zset1",0,-1));
//[zset-1, zset-2, zset-3, zset-4, zset-5, zset-6]
//1
//[zset-1, zset-2, zset-3, zset-4, zset-5]

Long rank(K key, Object o);
返回有序集中指定成员的排名,其中有序集成员按分数值递增(从小到大)顺序排列

System.out.println(template.opsForZSet().range("zset1",0,-1));
System.out.println(template.opsForZSet().rank("zset1","zset-2"));
//[zset-2, zset-1, zset-3, zset-4, zset-5]
//0   表明排名第一

Set range(K key, long start, long end);
通过索引区间返回有序集合成指定区间内的成员,其中有序集成员按分数值递增(从小到大)顺序排列

System.out.println(template.opsForZSet().range("zset1",0,-1));
//[zset-2, zset-1, zset-3, zset-4, zset-5]

Long count(K key, double min, double max);
通过分数返回有序集合指定区间内的成员个数

System.out.println(template.opsForZSet().rangeByScore("zset1",0,5));
System.out.println(template.opsForZSet().count("zset1",0,5));
//[zset-2, zset-1, zset-3]
//3

Long size(K key);
获取有序集合的成员数,内部调用的就是zCard方法

System.out.println(template.opsForZSet().size("zset1"));
//6

Double score(K key, Object o);
获取指定成员的score值

System.out.println(template.opsForZSet().score("zset1","zset-1"));
//2.2

Long removeRange(K key, long start, long end);
移除指定索引位置的成员,其中有序集成员按分数值递增(从小到大)顺序排列

System.out.println(template.opsForZSet().range("zset2",0,-1));
System.out.println(template.opsForZSet().removeRange("zset2",1,2));
System.out.println(template.opsForZSet().range("zset2",0,-1));
//[zset-1, zset-2, zset-3, zset-4]
//2
//[zset-1, zset-4]

Cursor<TypedTuple> scan(K key, ScanOptions options);
遍历zset

Cursor<ZSetOperations.TypedTuple<Object>> cursor = template.opsForZSet().scan("zzset1", ScanOptions.NONE);
    while (cursor.hasNext()){
       ZSetOperations.TypedTuple<Object> item = cursor.next();
       System.out.println(item.getValue() + ":" + item.getScore());
    }
//zset-1:1.0
//zset-2:2.0
//zset-3:3.0
//zset-4:6.0

6.3 使用redis实现同步锁

6.3.1 简单实现

步骤:

  1. 使用Redis的SETNX命令进行锁定
  2. 在处理完后使用RedisDELETE命令释放锁

多个进程通过使用setnx命令(也就是setIfAbsent方法)向redis中插入同一条key-value键值对。插入成功的成功成功加锁,返回true;没有插入成功就没有成功加锁,返回false。等到业务代码执行完毕,加锁成功的进程再删除这条key-value键值对就是解锁。

//通过使用setnx命令(也就是setIfAbsent方法)向redis中插入一条key-value键值对的方式拿到锁
Boolean  result=stringRedisTemplate.opsForValue().setIfAbsent("job","programmer");
//result不为空,说明这个客户端拿到锁
//result为空,证明这个客户端没有拿到锁
 
if(result==false)//没有拿到锁
{
   return “error”;
}
 
//从redis中取出stock这个key的value,这时拿到的是字符串,将其转化为整数
int  stock=Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
 
if(stock>0)//如果此时库存大于0,就扣减库存,stock-1
{
     
   stringTemplate.opsForValue("stock",stock-1+"”);
   System.out.println("扣减库存成功");
}
else
{
       System.out.println("库存不足,扣减失败");
}
stringRedisTemplate.delete("job");//释放锁,就是删除刚刚插入的key-value键值对
 
6.3.2 防死锁

上面的方式有可能会造成死锁,比如成功拿到锁的客户端扣减库存的逻辑抛异常了,那就不会执行到释放锁的逻辑,那么该锁是一直没有释放,其他请求时一直无法再成功加锁的,会成为死锁的,其他请求无法再扣减该商品

所以需要给这个插入的键值对设置自动过期时间(这里设置了过期时间为10s,足够执行业务代码)

Boolean  result = stringRedisTemplate.opsForValue().setIfAbsent("job","programmer");
stringRedisTemplate.expire("job", 10, TimeUnit.SECONDS);//设置过期时间为10s

或者

Boolean result = stringRedisTemplate.opsForValue().setIfAbsent("job","programmer",10,TimesUnit.SECONDS);
6.3.3 并发运行问题

如果定义线程运行准备删除key 时,第二个线程刚好执行删除key,那么有可能第一个线程删除了第二个线程放进去的锁。比如:

线程1加锁成功,并且设置过期时间为5秒,然后线程开始执行业务逻辑

如果线程1执行时间超过5秒,还没执行完业务逻辑锁的到期时间已经到了

此时线程2成功加上锁了,也设置过期时间,然后一直在执行自己的业务逻辑

此时线程1执行完了业务逻辑,于是去释放锁,此时释放的是线程2加的锁,而不是线程1自己加的锁

解决方法:多个客户端插入的key-value只有key相同,value不同,设置成key=欲扣减库存的商品id,value=线程id

比如说:

线程1设置的是10000011:00012

线程2设置的是10000011:00013

注意setnx命令只要插入的这个key-value键值对key已经存在就会返回false,而不需要key-value完全一样

然后在线程释放锁的时候,先比较一下要删除的key-value的value是不是和自己的线程id一样,如果不一样是不可以删除这个key-value键值对的

public int buy(int id) {
        String key="lock-"+id;
        String uuid= "LOCKED-"+UUID.randomUUID().toString();
        //获取锁,设置有效期,防止程序异常没有释放锁导致死锁
        try {

            //设置的缓存最好是有限时间,防止意外宕机时,缓存一直存在,导致服务一直不能使用
            Boolean b = redisTemplate.opsForValue().setIfAbsent(key, uuid, Duration.ofSeconds(60));
            if (null != b && b.booleanValue()) {
                //获取到锁
                //执行业务

            } else {
                //未获取到锁
                //响应失败
            }
        } finally {
            if(uuid.equals(redisTemplate.opsForValue().get(key))){//最大可能确保不删错
                //释放锁
                redisTemplate.delete(key);
            }

        }
        return 0;
    }

6.3.4 使用Redisson实现分布锁

引入相关依赖包:

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.6.5</version>
</dependency>
@Autowired  
private  Redisson  redisson;
 
RLock lock=redisson.getLock(“job”);
 
 
 
lock.tryLock(30,TimesUnit.SECONDS);
 
 
int  stock=Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
 
if(stock>0)//如果此时库存大于0,就扣减库存,stock-1
{
     
   stringTemplate.opsForValue("stock",stock-1+"”);
   System.out.println("扣减库存成功");
}
else
{
       System.out.println("库存不足,扣减失败");
}
lock.unlock();
  • 27
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值