目录
1 持久化选项
redis提供了两种方式进行持久化:
1 快照:某一时刻将所有数据写入硬盘。
2 只追加文件:在执行写命令时,将被执行的命令复制到硬盘。
与持久化相关的选项。
1.1 快照持久化
根据redis.conf
配置项,创建快照,几种创建快照方法:
- bgsave命令:创建子进程进行快照创建。
- save命令:直接主进程执行快照创建,完成之前阻塞其它命令。
- 配置文件选项:如save 60 1,从上次创建开始,60s有一次改动就进行快照创建。
- shutdown命令:当收到该命令,会自动执行save命令。
- sync命令:如从服务器复制主服务器,主服务器将执行bgsave命令。
在只使用快照存储数据,如果系统发生故障,将导致最后一次创建快照到系统崩溃时刻的所有数据变化情况。
1.2 AOF持久化
将被执行的写命令写到aof文件的末尾。需要配置选项:appendonly yes
。
同步频率:
always:每次写都要同步写入硬盘,严重降低redis速度;
everysec:每秒写入一次,常用;
no:操作系统决定何时写入。
aof文件体积较大,可以通过配置对应的压缩选项,进行重写。
2 复制
2.1 复制选项配置
redis采用了与关系数据库相同的方法实现复制特性,由主服务器进行写操作,从服务器进行读请求。
可以通过命令 slaveof host port让服务器连接主服务器,执行复制。
2.2 Redis复制启动过程
slave和master交互过程如下:
注意:redis在进行同步时,会清空自己的所有数据。
redis不支持主主同步。
2.3 主从链
redis支持主从链模式类似下图,也就是一个从服务器也可以有它的从服务器。
从而避免都从主服务器进行复制。
3 处理系统故障
如果发送系统故障,需要采取有效措施,应对各种问题
3.1 验证快照和AOF文件
redis提供了对应的命令redis-check-aof和redis-check-rdb验证aof文件和快照文件,是否有效。
可以对aof使用–fix进行修复,但是快照目前无法修复。
3.2 更换故障主服务器
假设A、B两天机器运行Redis服务,A为B的主服务器,如果A因为各种故障导致无法提供服务。可以使用另一台服务器C作为主服务器。
更换步骤如下:主要是copy对应的备份文件,这里以AOF文件为例(记得更改配置(appendonly yes
):
1. B服务器,创建演示数据:
2 将Windows文件appendonly.aof传输到linux下
查看linux文件存储地址:
将对应的aof文件复制到目标文件夹下。
3 然后重启linux文件服务,查看数据是否恢复:
4 测试同步:
可见已经完成主服务器更换。
4 Redis事务
事务是为了保障数据的正确性。
Redis主要是使用
multi和exec
命令实现。
4.1 事务演示例子:游戏物品出售
数据结构分析:
- 定义hash存储用户信息。
- 定义用户包裹集合存储用户游戏物品。
- 定义market存储用户需要出售的物品和价格。
4.2 Java代码演示
物品放到市场销售:
/**
* 将指定物品添加到market
* @param itemid
* @param sellerid
* @param price
*/
public void listItem(String itemid, String sellerid, double price) {
String inventory = "inventory:" + sellerid;
String item = itemid + "." + sellerid;
long endTime = System.currentTimeMillis() + 5000;
while (System.currentTimeMillis() < endTime) {
redisTemplate.watch(inventory);
BoundSetOperations<String, Object> userSet = redisTemplate.boundSetOps(inventory);
if (!userSet.isMember(itemid)) {
redisTemplate.unwatch();
return;
}
//开启事务执行
redisTemplate.multi();
BoundZSetOperations<String, Object> market = redisTemplate.boundZSetOps("market:");
market.add(item, price);
userSet.remove(itemid);
redisTemplate.exec();
}
}
- 这里首先使用了watch命令监视用户包裹集合,以防止出售过程用户更改,提供失败重试机会。
- 之后将开启事务,将出售物品放入市场,并移除用户包裹的对应物品。
- 执行事务。
购买商品:
/**
* 购买物品
* @param buyerid
* @param sellerid
* @param itemid
* @param price
*/
public void purchaseItem(String buyerid, String sellerid, String itemid, double price) {
String buyer = "users:" + buyerid;
String seller = "user:" + sellerid;
String item = itemid + "." + sellerid;
String inventory = "inventory:" + buyerid;
long endTime = System.currentTimeMillis() + 10000;
while (System.currentTimeMillis() < endTime) {
redisTemplate.watch("market:");
BoundZSetOperations<String, Object> market = redisTemplate.boundZSetOps("market");
Double score = market.score(item);
BoundHashOperations<String, Object, Object> buyerInfo = redisTemplate.boundHashOps(buyer);
BoundHashOperations<String, Object, Object> sellerInfo = redisTemplate.boundHashOps(seller);
Object funds = buyerInfo.get("funds");
if (score != price || price > (Double) funds) {
redisTemplate.unwatch();
return;
}
//开启、执行事务
redisTemplate.multi();
sellerInfo.increment("funds", price);
buyerInfo.increment("funds", -price);
redisTemplate.opsForSet().add(inventory, itemid);
market.remove(item);
redisTemplate.exec();
}
}
- 这里首先watch市场,防止出售过程被改动。
- 判断买家是否有足够金额购买。
- 如果满足则开启事务
- 添加对应修改命令,执行事务。
Redis没有提供数据库那种加锁机制,主要是防止加锁客户端等待时间过长。但是提供了失败重试的乐观锁机制。
4.5 非事务性流水线
事务性流水线可以加快执行速度,但是multi和exec
命令会消耗资源。不过Redis提供了不使用事务命令来执行流水线命令的机制-pipeline。
这里以Java为例,演示非事务性流水线:
/**
* 使用pipeline
*/
@Test
void pipeLineTest() {
List<Object> objects = redisTemplate.executePipelined(new SessionCallback<String>() {
@Override
public String execute(RedisOperations operations) throws DataAccessException {
redisTemplate.opsForList().leftPush("pipe", "k1");
redisTemplate.opsForList().rightPush("pipe", "k2");
redisTemplate.opsForList().leftPop("pipe");
redisTemplate.opsForList().rightPop("pipe");
return null;
}
});
objects.stream().forEach(System.out::println);
}
主要是使用executePipelined
打包执行命令,一次发送给redis服务器执行。
返回值为列表对应每条命令执行结果,上述代码测试执行结果:
下面示例是对同样非事务性流水线和普通执行的性能测试demo:
/**
* 使用pipeline
*/
@Test
void pipeLineTest() {
redisTemplate.executePipelined(new SessionCallback<String>() {
@Override
public String execute(RedisOperations operations) throws DataAccessException {
redisTemplate.opsForList().leftPush("pipe", "k1");
redisTemplate.opsForList().rightPush("pipe", "k2");
redisTemplate.opsForList().leftPop("pipe");
redisTemplate.opsForList().rightPop("pipe");
return null;
}
});
}
@Test
void notusePipeTest() {
redisTemplate.opsForList().leftPush("pipe", "k1");
redisTemplate.opsForList().rightPush("pipe", "k2");
redisTemplate.opsForList().leftPop("pipe");
redisTemplate.opsForList().rightPop("pipe");
}
@Test
void performanceTest() {
long pipelineEndTime = System.currentTimeMillis() + 60000;
int count = 0;
while (System.currentTimeMillis() < pipelineEndTime) {
pipeLineTest();
count++;
}
System.out.println(count);
pipelineEndTime = System.currentTimeMillis() + 60000;
count = 0;
while (System.currentTimeMillis() < pipelineEndTime) {
notusePipeTest();
count++;
}
System.out.println(count);
}
下面是执行10s和执行一分钟的结果比较:
时间 | 普通命令执行 | 流水线命令执行 |
---|---|---|
10s | 69次 | 210次 |
1分钟 | 403次 | 1548次 |
可以看出通过流水线执行命令,可以将原来通信时间缩短到原来的1/4到1/3,从而提升Redis的性能。
我们可以通过redis-benchmark
命令来查看redis的执行性能,如下演示:
对于上述可以看出redis对于基本的命令tps可以达到3-5万/s,执行非常快。
参考文献
[1] 《Redis实战》.