Redis基本概念和作用
Posted 瑾!
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis基本概念和作用相关的知识,希望对你有一定的参考价值。
Redis(Remote Dictionary Server ),即远程字典服务,是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。从2010年3月15日起,Redis的开发工作由VMware主持。从2013年5月开始,Redis的开发由Pivotal赞助。
REmote DIctionary Server(Redis) 是一个由 Salvatore Sanfilippo 写的 key-value 存储系统,是跨平台的非关系型数据库。
Redis 是一个开源的使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可基于内存、分布式、可选持久性的键值对(Key-Value)存储数据库,并提供多种语言的 API。
Redis 通常被称为数据结构服务器,因为值(value)可以是字符串(String)、哈希(Hash)、列表(list)、集合(sets)和有序集合(sorted sets)等类型。
Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库,缓存和消息中间件。它支持多种类型的数据结构,如字符串(strings)、哈希列(hashes)、列表(lists)、集合(sets)、有序集合(sorted sets)与范围查询,bitmaps、hyperloglogs和地理空间(geospatial)索引半径查询。Redis内置了复制(replication)、LUA脚本(Luascripting)、LRU驱动事件(LRU eviction)、事务(transactions)和不同级别的磁盘持久化(persistence),并通过Redis哨兵(Sentinel)和自动分区(Cluster)提高可用性(highavailability)
Redis 是当前互联网世界最流行的 NoSQL(Not Only SQL)数据库。NoSQL在互联网系统中的作用很大,因为它可以在很大程度上提高互联网系统的性能。
Redis 具备一定持久层的功能,也可以作为一种缓存工具。对于NoSQL数据库而言,作为持久层,它存储的数据是半结构化的没这就意味着计算机在读入内存中有更少的规则,读入速度更快。
对于那些结构化、多范式规则的数据库系统而言,它更具有性能优势。作为缓存,它可以支持大数据存入内存中,只要命中率高,它就能快速响应,因为在内存中的数据读/写比数据库读/写磁盘的速度快即使到上百倍。
目前NoSQL 有很多争议,有人认为它可以取代数据库,我却不这么认为。
因为数据库系统有更好的规范性和数据完整性,功能更强大,作为持久层更为完善,安全性更高。而NoSQL结构松散、不完整、攻能有限,目前尚不具备取代数据库的实力,但是作为缓存工具,它的高性能、高相应等功能,使它成为一个很重要的工具。
Redis 的优势是什么?
- 相应快速 Redis 相应非常快,每秒可以执行大约110000个写入操作,或者81000个读取操作,其速度远超数据库。如果存入一些常用的数据,就能有效的提高系统的性能
- 支持5中数据结构 它们是字符串、哈希结构、列表、集合、有序集合。比如对于字符串可以存入一些java基础数据类型,哈希可以存储对象,列表可以存储List对象等。这使得在应用中很容易根据自己的需求进行选择存储的数据类型,方便开发。对于Redis而言,虽然只有5中数据类型,但是有两大好处:一方面可以满足存储各种数据结构体的需求;另一方面数据类型少,使得规则少,需要的判断和逻辑就少,这样读/写的速度就更快
- 操作都是原子的 所有Redis的操作都是原子的,从而确保党两个客户同时访问Redis服务器时,得到的是更新后的值(最新值)。在需要高并发的场合可以考虑使用Redis的事务,处理一些需要锁的业务。
- MultiUtility工具 Redis可以在如缓存、消息传递队列中使用(Redis支持“发布+订阅”的消息模式),在应用程序如Web应用程序会话,网站页面点击数等任何短暂的数据中使用
Redis的数据结构有哪些
1.string 字符串 与其他编程语言或者其他键值存储提供的字符串非常相似,键(key)---值(value)(字符串格式),字符串拥有一些操作命令,如 get、set、del还有一些比如自增或者自减操作等等。Redis是使用c语言开发,但c中并没有字符串类型,只能使用指针或者符数组的形式表示一个字符串,所以Redis设计了一个简单动态字符串(SDS[Simple Dynamic String])作为低实现:
定义SDS对象,此对象中包含三个属性:
- Len buf 中已经占有的长度(表示此字符串的实际长度)
- Free buf中未使用的缓冲区长度
- Buf【】实际保存字符串数据的地方
所以取字符串的长度的时间复杂度为o(1),另,buf【】中依然采用了c语言的以\\0结尾可以直接使用C语言的部分标准C字符串库函数。
空间分配原则:党len小于|mb(1024*1024)时增加字符串分配空间大小为原来的2倍,党len大于等于1mb时每次分配额外多分配1m的空间
由此可以得出一下特征:
- Redis为字符分配空间的次数时小于等于字符串的长度N,而c语言中的分配原则必为N 降低了分配次数提高了追加速度,代价就是多占用一些内存空间,并且这些空间不会自动释放
- 二进制安全的
- 高效的计算字符串的长度
- 高效的追加字符串操作
2.列表 (List) redis对键表的结构支持使得它在键值存储的世界中独树一帜,一个列表结构可以有序的存储多个字符串,拥有例如lpush lpop rpush rpop等等操作命令。在3.2版本之前,列表时使用ziplist和linkedlist实现的 在这些老版本中,当列表对象同时满足一下两个条件时,列表对象使用ziplist编码:
- 列表对象保存的所有字符串元素长度都小于64字节
- 列表对象保存的元素数量小于512个
当有任意条件不满足时将尽享一次转码,使用linkedlist
3.哈希(hash) redis的散列可以存储多个键值对之间的映射,单列存储的值既可以是字符串又可以时数字值,并且用户同样可以对散列存储的数字值执行自增操作或者自减操作。散列可以看作时一个文档或者关系型数据库里的一行。Has底层的数据实现有两种:一是ziplist 另一种时hashtable
4.集合(set) redis的集合和列表都可以存储多个字符串,他们之间的不同在于,列表可以存储多个相同的字符串,而集合则通过使用散列表(hsahtable)来保证自己存储的每一个字符串都是各不相同的(这些散列表只有键,但没有与键相关联的值),redis中的集合是无序的。还可能存在另一种集合那就是intset,它是用于存储正式的有序集合,里面存放同一类型的整数。 注意intset只支持升级不支持奖及操作。
5.有序集合(zset) 有序集合和散列 一样,都用于存储键值对,有序集合的键被称为成员(member),每个成员都是各不相同的。有序几集合的值则被称为分值(score),分值必须是浮点数。有序集合是redis里面唯一一个既可以根据成员访问元素(这一点跟散列一样),又可以根据分值以及分值的排序舒徐访问的元素结构
Redis的应用场景
- 缓存会话(单点登录)
- 分布式锁,比如:使用setnx
- 各种排行榜或计数器
- 商品列表或者用户基础数据列表等
- 使用list作为消息队列
- 秒杀,库存扣减等
五种类型的应用场景
- 字符串(string) redis对于KV的操作效率很高,可以直接用作计数器。例如,统计在线人数等,另外string类型是二进制存储安全的,所以可以使用它来存储图片、视频等
- 哈希(hash) 存放键值对,一般可以用来存放某个对象的基本属性信息,例如,用户信息,商品信息等,
- 列表(list) 可以用于实现消息队列,也可以使用它提供range命令,做分页查询等功能。
- 集合(set) 证书的有序列表可以直接使用集合。可以用作某些去重功能,比如 奖池抽奖
- 有序集合(zset) 可以使用范围查询,排行榜功能或者topN功能
锁的基本概念到 Redis 分布式锁实现
package Thread;
import java.util.concurrent.TimeUnit;
public class Ticket {
/**
* 初始库存量
* */
Integer ticketNum = 8;
public void reduce(int num){
//判断库存是否够用
if((ticketNum - num) >= 0){
try {
TimeUnit.MILLISECONDS.sleep( 200);
} catch (InterruptedException e){
e.printStackTrace();
}
ticketNum -= num;
System.out.println(Thread.currentThread().getName() + "成功卖出"
+ num + "张,剩余" + ticketNum + "张票");
} else {
System.err.println(Thread.currentThread().getName() + "没有卖出"
+ num + "张,剩余" + ticketNum + "张票");
}
}
public static void main(String[] args) throws InterruptedException{
Ticket ticket = new Ticket();
//开启10个线程进行抢票,按理说应该有两个人抢不到票
for( int i= 0;i< 10;i++){
new Thread(() -> ticket.reduce( 1), "用户" + (i + 1)).start();
}
Thread.sleep( 1000L);
}
}
public synchronized void reduce( int num){
//判断库存是否够用
if((ticketNum - num) >= 0){
try {
TimeUnit.MILLISECONDS.sleep( 200);
} catch (InterruptedException e){
e.printStackTrace();
}
ticketNum -= num;
System.out.println(Thread.currentThread().getName() + "成功卖出"
+ num + "张,剩余" + ticketNum + "张票");
} else {
System.err.println(Thread.currentThread().getName() + "没有卖出"
+ num + "张,剩余" + ticketNum + "张票");
}
}
2.1 缩短锁的持有时间
public void reduceByLock( int num){
boolean flag = false;
synchronized (ticketNum){
if((ticketNum - num) >= 0){
ticketNum -= num;
flag = true;
}
}
if(flag){
System.out.println(Thread.currentThread().getName() + "成功卖出"
+ num + "张,剩余" + ticketNum + "张票");
}
else {
System.err.println(Thread.currentThread().getName() + "没有卖出"
+ num + "张,剩余" + ticketNum + "张票");
}
if(ticketNum == 0){
System.out.println( "耗时" + (System.currentTimeMillis() - startTime) + "毫秒");
}
}
public synchronized void reduce( int num){
//判断库存是否够用
if((ticketNum - num) >= 0){
try {
TimeUnit.MILLISECONDS.sleep( 200);
} catch (InterruptedException e){
e.printStackTrace();
}
ticketNum -= num;
if(ticketNum == 0){
System.out.println( "耗时" + (System.currentTimeMillis() - startTime) + "毫秒");
}
System.out.println(Thread.currentThread().getName() + "成功卖出"
+ num + "张,剩余" + ticketNum + "张票");
} else {
System.err.println(Thread.currentThread().getName() + "没有卖出"
+ num + "张,剩余" + ticketNum + "张票");
}
}
2.2 减少锁的粒度
package Thread;
import java.util.concurrent.CountDownLatch;
public class Movie {
private final CountDownLatch latch = new CountDownLatch( 1);
//魔童哪吒
private Integer babyTickets = 20;
//蜘蛛侠
private Integer spiderTickets = 100;
public synchronized void showBabyTickets() throws InterruptedException{
System.out.println( "魔童哪吒的剩余票数为:" + babyTickets);
//购买
latch.await();
}
public synchronized void showSpiderTickets() throws InterruptedException{
System.out.println( "蜘蛛侠的剩余票数为:" + spiderTickets);
//购买
}
public static void main(String[] args) {
Movie movie = new Movie();
new Thread(() -> {
try {
movie.showBabyTickets();
} catch (InterruptedException e){
e.printStackTrace();
}
}, "用户A").start();
new Thread(() -> {
try {
movie.showSpiderTickets();
} catch (InterruptedException e){
e.printStackTrace();
}
}, "用户B").start();
}
}
魔童哪吒的剩余票数为:20
public void showBabyTickets() throws InterruptedException{
synchronized (babyTickets) {
System.out.println( "魔童哪吒的剩余票数为:" + babyTickets);
//购买
latch.await();
}
}
public void showSpiderTickets() throws InterruptedException{
synchronized (spiderTickets) {
System.out.println( "蜘蛛侠的剩余票数为:" + spiderTickets);
//购买
}
}
魔童哪吒的剩余票数为:20
蜘蛛侠的剩余票数为:100
2.3 锁分离
-
公平锁:ReentrantLock -
非公平锁:Synchronized、ReentrantLock、cas -
悲观锁:Synchronized -
乐观锁:cas -
独享锁:Synchronized、ReentrantLock -
共享锁:Semaphore
4.1 分布式锁需要注意哪些点
1)互斥性
2)防死锁
3)持锁人解锁
4)可重入
4.2 Redis 分布式锁流程
-
设置锁的有效时间 目的是防死锁 (非常关键)
-
分配客户端的唯一标识,目的是保证持锁人解锁 (非常重要)
4.3 加锁和解锁
4.3.1 加锁
public String set( String key, String value, String nxxx, String expx, long time) {
this.checkIsInMultiOrPipeline();
this.client. set(key, value, nxxx, expx, time);
return this.client.getStatusCodeReply();
}
4.3.2 解锁
-
检查是否自己持有锁 (判断唯一标识) ; -
删除锁。
if Redis. call( "get", key==argv[ 1]) then
return Redis. call( "del", key)
else return 0 end
public class RedisDistributedLock implements Lock {
//上下文,保存当前锁的持有人id
private ThreadLocal<String> lockContext = new ThreadLocal<String>();
//默认锁的超时时间
private long time = 100;
//可重入性
private Thread ownerThread;
public RedisDistributedLock() {
}
public void lock() {
while (!tryLock()){
try {
Thread.sleep( 100);
} catch (InterruptedException e){
e.printStackTrace();
}
}
}
public boolean tryLock() {
return tryLock(time,TimeUnit.MILLISECONDS);
}
public boolean tryLock(long time, TimeUnit unit){
String id = UUID.randomUUID().toString(); //每一个锁的持有人都分配一个唯一的id
Thread t = Thread.currentThread();
Jedis jedis = new Jedis( "127.0.0.1", 6379);
//只有锁不存在的时候加锁并设置锁的有效时间
if( "OK". equals(jedis. set( "lock",id, "NX", "PX", unit.toMillis(time)))){
//持有锁的人的id
lockContext. set(id); ①
//记录当前的线程
setOwnerThread(t); ②
return true;
} else if(ownerThread == t){
//因为锁是可重入的,所以需要判断当前线程已经持有锁的情况
return true;
} else {
return false;
}
}
private void setOwnerThread(Thread t){
this.ownerThread = t;
}
public void unlock() {
String script = null;
try{
Jedis jedis = new Jedis( "127.0.0.1", 6379);
script = inputStream2String(getClass().getResourceAsStream( "/Redis.Lua"));
if(lockContext. get()== null){
//没有人持有锁
return;
}
//删除锁 ③
jedis.eval(script, Arrays.asList( "lock"), Arrays.asList(lockContext. get()));
lockContext. remove();
} catch (Exception e){
e.printStackTrace();
}
}
/**
* 将InputStream转化成String
* @param is
* @return
* @throws IOException
*/
public String inputStream2String(InputStream is) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int i = -1;
while ((i = is.read()) != -1) {
baos.write(i);
}
return baos.toString();
}
public void lockInterruptibly() throws InterruptedException {
}
public Condition newCondition() {
return null;
}
}
-
用一个上下文全局变量来记录持有锁的人的 uuid,解锁的时候需要将该 uuid 作为参数传入 Lua 脚本中,来判断是否可以解锁。 -
要记录当前线程,来实现分布式锁的重入性,如果是当前线程持有锁的话,也属于加锁成功。 -
用 eval 函数来执行 Lua 脚本,保证解锁时的原子性。
-
数据库如果挂掉会导致业务系统不可用。 -
无法设置过期时间,会造成死锁。
6.2 基于 zookeeper 的分布式锁
-
从性能角度:Redis > zookeeper > 数据库 -
从可靠性 (安全) 性角度:zookeeper > Redis > 数据库
-
互斥性 -
防死锁 -
加锁人解锁 -
可重入性
以上是关于Redis基本概念和作用的主要内容,如果未能解决你的问题,请参考以下文章