Redis系列二之事务及消息通知

时间:2021-05-08 17:25:00

一、事务

  Redis中的事务是一组命令的集合。一个事务中的命令要么都执行,要么都不执行。

  1、事务简介

  事务的原理是先将一个事务的命令发送给Redis,然后再让Redis依次执行这些命令。下面看一个示例:

  Redis系列二之事务及消息通知

  首先,使用multi命令告诉Redis:下面我给你的命令属于同一个事务,你先不要执行,而是暂时存起来。

  然后,我们发送两个set命令来实现赋值,可以看到redis没有执行这些命令,而是返回queued表示这两条命令已经进入等待执行的事务队列中。

  当所有要在同一事务中执行的命令都发给Redis后,用exec命令告诉Redis将等待执行的事务队列中所有的命令按照发送顺序依次执行。

  2、错误处理

  当一个事务中某个命令执行出错时,Redis会怎样处理呢?这里有二种情况:

  情况一:语法错误,指错命令不存在或者命令参数的个数不对

  Redis系列二之事务及消息通知

  可以看到,跟在multi命令后执行了三个命令,第二个和第三个命令有语法错误,执行exec后redis就会执行返回错误。

  情况二:运行错误,指在命令执行时出现的错误。

  Redis系列二之事务及消息通知

  这种错误在实际执行前redis是无法发现的,所以在事务里这样的命令是会被redis接受和执行的,如果事务里一条命令出现了运行错误,事务里其他的命令依然会继续执行(包括出错命令之后的命令)。

  注意:Redis事务没有关系型数据库提供的回滚功能。

  3、Watch命令

  watch命令可以监控一个或多个键,一旦其中一个键被修改或删除,之后的事务就不会执行,监控一直持续到exec命令(但是不能保证其他客户不修改这一键值)。

  Redis系列二之事务及消息通知

 

  上例中执行watch命令后,事务执行前修改了key值,所以事务中命令set username xujian没有执行,exec命令返回空结果。

   注意:执行exec命令后立即取消对所有键的监控,如果不想执行事务中的命令也可以使用unwatch命令来保证下一个事务的执行不会受到影响。

二、生存时间

  在Redis中,可以使用expire命令设置一个键的生存时间,到时间后Redis会自动删除它。

  expire命令的使用方法为expire key seconds,其中seconds参数表示键的生存时间,单位是秒。如果想知道一个键还有多久的时间会被删除,可以使用TTL命令,返回值是键的剩余时间(单位是秒)。如果想取消键的生存时间设置,即将恢复永久的,可以使用persist命令。

  Redis系列二之事务及消息通知

三、排序

  1、有序集合

  有序集合常见的使用场景是大数据排序,如游戏的玩家排行榜。

  2、sort命令

  sort命令可以对列表类型、集合类型和有序集合类型键进行排序,并且可以完成与关系数据库中的连接查询相类似的任务。

  对List进行排序:

  Redis系列二之事务及消息通知

  对有序集合类型排序时会忽略元素的分数,只针对元素自身的值进行排序:

  Redis系列二之事务及消息通知

3、by参数

  by参数的语法为“by 参考键”,其中参考键可以是字符串类型键或者是散列类型键的某个字段。如果提供了by参数,sort命令将不再依据元素自身的值进行排序,而是对每个元素使用元素的值替换参考键中的第一个“*”并获取其值,然后依据该值对元素排序:

  Redis系列二之事务及消息通知

4、store参数

  默认情况下,sort会直接返回排序结果,如果希望保存排序结果,可以使用store参数。

  注意:sort命令的时间复杂度是O(n+mlogm),其中n表示要排序的列表(集合或有序集合)中的元素个数,m表示要返回的元素的个数。当n较大的时候sort命令的性能相对较低,并且redis在排序前会建立一个长度为n的容器来存储待排序的元素。在开发中使用sort要注意以下几点:

  1、尽可能减少待排序键中元素的数量(使n尽可能小)

  2、使用limit参数只获取需要的数据(使m尽可能小)

  3、如果要排序的数据量较大,尽可能使用store参数将结果缓存

四、消息通知

  一般来说,消息队列有两种场景,一种是生产者消费者模式,一种是发布者订阅者模式。利用redis这两种场景的消息队列都能实现。

  1、生产者消费者模式

  生产者生产消息放到队列中,多个消费者同时监听队列,谁先抢到消息谁就会从队列中取走消息,即对于每个消息最多只能被一个消费者拥有。

  具体的方法就是创建一个任务队列,生产者主动lpush消息,而消费者去rpop数据。但是这样存在一个问题,就是消费者需要主动去请求数据,周期性的请求会造成资源的浪费。如果可以实现一旦有新消息加入队列就通知消费者就好了,这时借助brpop命令就可以实现这样的需求。brpop和rpop命令相似,唯一区别就是当列表中没有元素时,brpop命令会一直阻塞住连接,直到有新元素加入。

BRPOP key timeout

  brpop命令接收两个参数,第一个参数key为键值,第二个参数timeout为超时时间。BRPOP命令取数据时候,如果暂时不存在数据,该命令会一直阻塞直到达到超时时间。如果timeout设置为0,那么就会无限等待下去。

  Redis系列二之事务及消息通知

  2、发布者订阅者模式

  发布者生产消息放到队列里,多个监听队列的订阅者都会受到同一份消息。

  生产者使用下面命令来发布消息:

PUBLISH CHANNEL MESSAGE

  订阅者通过下面的命令来订阅消息,执行subscribe命令后,客户端进入订阅状态,处于此状态的客户端不能使用4个属于“发布/订阅”模型的命令之外的命令。另外,可以使用subscribe channel1.1 channel1.2 ... 同时订阅多个频道。

SUBSCRIBE CHANNEL

  Redis系列二之事务及消息通知

  3、Java实现的redis的消息队列

  在jedis中,有对应的方法进行订阅和发布,为了传输对象,需要将对象进行序列化,并封装成字符串进行处理。

  下面我们要实现三个类,一个对应publish,一个对应subscribe,一个对应要传递的对象实体类:

  实体类:

import java.io.Serializable;
/**
* 实体类
* 封装消息
*
@author Administrator
*
*/
public class Message implements Serializable
{
private static final long serialVersionUID = 1L;
private String title;
private String content;
public String getTitle()
{
return title;
}
public void setTitle(String title)
{
this.title = title;
}
public String getContent()
{
return content;
}
public void setContent(String content)
{
this.content = content;
}
}

  Publish类:

import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import redis.clients.jedis.Jedis;
/**
* 发布者,用于发布消息
*
@author Administrator
*
*/
public class TestPub
{
public static void main(String[] args)
{
Jedis jedis
= new Jedis("127.0.0.1");
try
{
Message message
= new Message();
message.setTitle(
"体育新闻");
message.setContent(
"著名NBA球星科比退役了!");
ByteArrayOutputStream baos
= new ByteArrayOutputStream();
ObjectOutputStream oos
= new ObjectOutputStream(baos);
oos.writeObject(message);
String msg1
= baos.toString("ISO-8859-1");

jedis.publish(
"foo", msg1);
}
catch (Exception e)
{
e.printStackTrace();
}
jedis.close();
}
}

  Subscribe类:

import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPubSub;
/**
* 订阅者,用于接收消息
*
@author Administrator
*
*/
public class TestSub
{
public static void main(String[] args)
{
Jedis jedis
= new Jedis("127.0.0.1");
JedisPubSub jedisPubSub
= new JedisPubSub()
{
@Override
public void onUnsubscribe(String channel, int subscribedChannels)
{
}

@Override
public void onSubscribe(String channel, int subscribedChannels)
{
}

@Override
public void onPUnsubscribe(String pattern, int subscribedChannels)
{
}

@Override
public void onPSubscribe(String pattern, int subscribedChannels)
{
}

@Override
public void onPMessage(String pattern, String channel, String message)
{
}

@Override
public void onMessage(String channel, String message)
{
try
{
ByteArrayInputStream bis
= new ByteArrayInputStream(message.getBytes("ISO-8859-1"));
            // 此处指定字符集将字符串编码成字节数组,此处的字符集需要与发布时的字符集保持一致 ObjectInputStream ois = new ObjectInputStream(bis);
Message message2
= (Message) ois.readObject();
System.out.println(message2.getTitle()
+"\n"+message2.getContent());
}
catch (Exception e)
{
e.printStackTrace();
}
finally
{

}
}
};
jedis.subscribe(jedisPubSub,
"foo");
jedis.close();
}
}

  先执行订阅操作,然后发布者发布消息,执行结果为:

  Redis系列二之事务及消息通知

五、管道

  客户端和Redis使用TCP协议连接,不管是客户端向Redis发送命令还是Redis向客户端返回命令的执行结果,都需要经过网络传输。在执行过个命令时,每条命令都需要等待上一条命令执行完才能执行,即使命令不需要上一条命令的执行结果。

  Redis的底层通信协议对管道提供了支持,通过管道可以一次性发送多条命令并在执行完后一次性将结果返回,当一组命令中每条命令都不依赖之前命令的执行结果时就可以将这组命令一起通过管道发出。管道通过减少客户端与Redis的通信次数来实现降低往返时延累计值的目的。

六、存储优化

  1、精简键名和键值

  2、内部编码优化

七、参考资料

  1、Redis入门指南