Redis学习之旅~原理篇

2019-06-22  本文已影响0人  无一幸免

内容依旧来自<redis深度历险>

核心原理

线程IO模型

单线程非阻塞IO

while (true) {
  eventList = select(readFds, writeFds, timeout);
  for (event in eventList) {
       handleEvent(event);
  }
}

通信协议

背景

最小单元类型,每个单元结束时候以\r\n结束

上面列举了这么多个类型,可以看出redis的传输协议里面有大量冗余的回车换行符。虽然它浪费了部分空间,但是胜在简洁。这里我需要思考的就是,性能并不总是一切,简单性、易理解和易实现也是要权衡的问题。

redis持久化

实际操作代码如下:

cd /etc
vim redis.conf

修改如下配置
appendonly yes
# The name of the append only file (default: "appendonly.aof")
appendfilename "appendonly.aof"
往下面看,有三种刷盘方式,我们选择每秒刷一次
# appendfsync always
appendfsync everysec # 一秒调用一次
# appendfsync no


...
很后面有一行,这个是redis文件的配置
dir /var/lib/redis

运行几个命令
set a 1 
incr a 
set b 2
如此...

接着去到/var/lib/redis文件夹,可以看到appendonly.aof文件已经生成,使用less命令进行查看,就会有如下命令

*2
$6
SELECT
$1
0
*3
$3
SET
$1
c
$1
2
*3
$3
SET
$1
v
$1
1
*3
$3
SET
$1
a
$1
1

接下来尝试另外一个命令,bgrewriteaof对日志进行瘦身

dbsize
6
//日志显示的文件大小
[root@VM_75_157_centos redis]# ll
total 20
-rw-r--r-- 1 root  root  347 Jun 20 22:37 appendonly.aof

然后执行bgrewriteaof命令:
127.0.0.1:6379> bgrewriteaof
Background append only file rewriting started
redis开启了子进程进行瘦身
[root@VM_75_157_centos redis]# ll
total 20
-rw-r--r-- 1 root  root  267 Jun 20 22:38 appendonly.aof
文件大小从347降低到了267

管道

public class PiplineTest {
    private static int count = 10000;
 
    public static void main(String[] args){
        useNormal();
        usePipeline();
    }
 
    public static void usePipeline(){
        ShardedJedis jedis = getShardedJedis();
        ShardedJedisPipeline pipeline = jedis.pipelined();
        long begin = System.currentTimeMillis();
        for(int i = 0;i<count;i++){
            pipeline.set("key_"+i,"value_"+i);
        }
        pipeline.sync();
        jedis.close();
        System.out.println("usePipeline total time:" + (System.currentTimeMillis() - begin));
    }
 
    public static void useNormal(){
        ShardedJedis jedis = getShardedJedis();
        long begin = System.currentTimeMillis();
        for(int i = 0;i<count;i++){
            jedis.set("key_"+i,"value_"+i);
        }
        jedis.close();
        System.out.println("useNormal total time:" + (System.currentTimeMillis() - begin));
    }
 
    public static ShardedJedis getShardedJedis(){
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        poolConfig.setMaxTotal(2);
        poolConfig.setMaxIdle(1);
        poolConfig.setMaxWaitMillis(2000);
        poolConfig.setTestOnBorrow(false);
        poolConfig.setTestOnReturn(false);
        JedisShardInfo info1 = new JedisShardInfo("127.0.0.1",6379);
        JedisShardInfo info2 = new JedisShardInfo("127.0.0.1",6379);
        ShardedJedisPool pool = new ShardedJedisPool(poolConfig, Arrays.asList(info1,info2));
        return pool.getResource();
    }
}

消耗时间

useNormal total time:772
usePipeline total time:112

管道的本质:网络交互的简略流程如下

redis事物

普通数据库的事务大致如下:

begin();
try{
    //业务逻辑 
    ....
    commit();
} catch(Exception e) {
    rollback();
}

redis的事务有如下的指令来支持,主要有multi事务开始,exec事务执行,discard事务丢弃。

127.0.0.1:6379> multi
OK
127.0.0.1:6379> incr a
QUEUED
127.0.0.1:6379> incr a
QUEUED
127.0.0.1:6379> exec
1) (integer) 9
2) (integer) 10

如果中途有命令是错误的呢?
[root@VM_75_157_centos ~]# redis-cli
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incre a
(error) ERR unknown command 'incre'
127.0.0.1:6379> incr a
QUEUED
127.0.0.1:6379> incr a
QUEUED
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.
这时就会告诉用户,事务被丢弃了
a的值并没有改变。但是,这并没有确保是所有的指令都没有执行,redis的事务不支持原子性

redis事务的执行流程就是所有的执行在exec指令之前,都不会执行,而是缓存在服务器的事务队列当中。服务器一旦接收到exec指令,才开始批量执行队列的指令。之前说过redis是单线程,所以可以保证队列里面的指令可以得到顺序执行,不会被其他指令抢占。保证了一批指令的批量执行。

(nil)
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set test test 
QUEUED
127.0.0.1:6379> incr test
QUEUED
127.0.0.1:6379> set test2 test2
QUEUED
127.0.0.1:6379> exec
1) OK
2) (error) ERR value is not an integer or out of range
3) OK
127.0.0.1:6379> get test
"test"
127.0.0.1:6379> get test2
"test2"

上面的事务,在 第二个指令执行的时候失败了。如果有使用mysql的经验,我们可能认为,后续的get命令,得到的会是null值。的确,mysql可以对事务进行回滚。但是,redis后续的指令都被执行了。redis事务不支持回滚的一个原因就是redis是先操作指令,然后再写日志。而mysql是先写日志,再进行操作。所以,发生错误的时候,mysql有可以回滚的日志,而redis没有。通过上述的操作,我们可以知道redis的事务不具备原子性,而是仅仅满足了事务隔离性种的串行化。

对事务的操作,我们是可以进行一定的优化的,使用的方式就是前面提过的管道。之前的这几个命令,一个命令就消耗了一个网络来回,我们可以使用管道进行优化。

watch指令,这个是redis提供的一种乐观锁的实现。如果有用过关系型数据库,乐观锁的实现的话,就是在表里面增加一个version版本号。在对某一行进行修改的时候,先select这一行,获得当前的版本号,然后执行更新的时候可以是

update table set a = ? where id = ? and version = 当前线程select的版本号

乐观锁可以处理Java程序的多线程并发修改。redis的watch也是同样的道理,在事务开启之前,先用watch盯住某个key,然后进行事务操作,如果key在事务执行之前,有被修改过,事务就执行失败。

127.0.0.1:6379> set books java
OK
127.0.0.1:6379> watch books
OK
127.0.0.1:6379> set books redis
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set  books golang
QUEUED
127.0.0.1:6379> exec
(nil)

我们事先watch了books变量,但是,在事务之前,books被改变了,所以,后面执行事务的时候,就失败。redis乐观锁的指令顺序是watch->multi->exec。

分布式锁

redis key的过期策略

定时扫描

扫描策略的时间配置
cd /etc
vim redis.conf
把文件拉到最后,会有一行
hz 10

修改这个值就可以改变定时过期扫描的频率,redis支持1~500,但是超过100的话,就不是一个good idea。

jedis.expire(key, Math.random(86400) + time);

从节点过期策略

上一篇下一篇

猜你喜欢

热点阅读