redis不提供严格的锁机制,即不保证数据的完全正确性,所以需要用户自己去严格检查。为了保证数据的安全,需要将数据及时的存储到硬盘上,redis提供两种持久化的方式:快照-批量写入;追加-单条追加。另外为了保证数据库的安全,防止数据丢失和为了负载均衡,redis也提供了分布式情况下的复制策略,可以对多个服务器数据进行同步。另外,根据不同的场景,我们需要合理的判断形势,在性能和安全上达到一个平衡,即既能够快速的处理数据,又能够有效地避免数据的丢失。


  • 4.1 数据持久化
  • 快照snapshotting
    快照即对数据进行批量备份,之前的备份数据将被删除。创建快照的方式有以下几种:
  • 通过bgsave创建快照,这种方式下,redis会通过调用fork来创建一个子线程,子线程负责将快照写入硬盘,而父线程还是继续执行接下来的步骤。
  • 客户端直接向redis发送save命令来执行快照,这时,redis在完成当前快照之前,将不再响应其他命令,可以避免新的数据写入,不存在丢失情况。但是这种情况并不常用,我的思考:因为redis就是用来高并发的,如果需要等待,还不如不用;另外,save命令使用在以下场景,即bgsave命令时,没有足够的内存(这种情况需要避免),或者并不需要太高效率,完全可以等待持久化操作。
  • 用户通过save 60 10000命令来控制bgsave的时间,这段话代表,60秒之内有10000个写入时,将触发bgsave命令。redis在接收到关闭服务器请求时或者接收到TERM信号时,将会触发save命令,阻塞所有客户端,并在save命令执行完毕后,关闭服务器。从而保证数据保存正常。
  • 另外在都服务情况下,当主服务器向从服务器进行数据复制时,即SYNC命令。主服务器需要对自身数据进行快照,即执行bgsave命令。(如果是刚刚执行,则不再执行,如何判断?应该是有个时间字段判断~)
    缺陷:
    通过快照的逻辑发现,快照是用来保存数据到磁盘上,如果没有及时快照,比如在下一次快照之前,系统奔溃,那么内存中还没有被快照的数据将永久丢失,所有,快照持久化只适用于那些即便丢失部分数据,也并不会造成问题的情况,比如视频数据。
    场景
    1 一般而言对于bgsave命令,根据不同的场景,时间任务设定也不尽相同,因此为了更加贴近实际,需要开发场景和生产场景尽量相同
    2 另外为了防止数据丢失,需要保存日志,一边在之后根据日志来恢复数据。
    3 由于bgsave命令需要创建子线程,对于20G的大数据处理,将会存在间断性的停顿,但是对于save,速度将大大加快。为了降低停顿对用户体验的影响,我们可以选择低峰时间段进行快照,比如凌晨3点。
  • 追加append-only file
    快照能够将大量数据进行写入,这样数据存储将能够很好地组织,也便于查询,但是容易导致大量数据丢失。而AOF追加,通过只在AOF文件后进行追加,从而及时的添加当前的信息。

文件同步:write和flush命令,write会将把数据存储到缓冲区,然后等待操作系统将数据写入硬盘,flush则会请求操作系统将数据尽快写入硬盘。
AOF有三种选择,每个命令执行一次,每间隔一秒执行一次;让操作系统自己判断执行时间。为了兼顾安全性和写入性能,一般使用每间隔一秒执行一次追加。
* 重写/压缩AOF
前面,我们已经知道了,AOF操作通过追加,可以将文件的丢失控制在一秒以内,从而大大防止数据的丢失,但为什么不直接使用这种方式,还使用快照呢?原因在于,每一次AOF都会产生命令记录AOF,这样就会导致AOF文件越来越大,最终可能沾满硬盘空间,所以,为了避免这种情况,AOF提供了重写功能,即压缩数据,类似于java垃圾回收中的复制整理,通过将数据进行重新整理,去掉冗余的数据,将大大降低磁盘的空间。
AOF重写命令也有两个,BGREWRITEAOF和AUTO..,这两个命令的逻辑同bgsave/save类似,也是通过是否开子线程来进行数据重写操作。

  • 4.2 复制
    为了满足分布式服务器结构,数据与数据之间的互相备份,redis提供了复制功能。通过复制功能,就可以大大降低单个redis的负载,从而实现负载均衡。
  • redis的复制功能需要配置主从服务器,具体参数参考相关文档
  • redis在进行复制时,将擦除从服务器的所有相关数据;另外由于主服务器进行复制时将创建子线程,将大大占用内存,所以,为了防止效率低下,在内存初期配置时,最好预留一部分内存,即主服务器只是用60-70%的内存。
  • 主服务链:有时候我们可能会这样设计一个服务群,一个主服务器,和100个从服务器,但是这样是否很好?如果100台服务器同时请求复制,那么主服务器的压力将很大很大,所以,为了进一步降低服务器的压力,我们使用了服务器链(有如树结构),如下图,每一层的服务器都相当于下一层服务器的主服务器,每一个主服务器的连接点都不会太多,这样,顶端的服务器压力将会分配给下面的所有服务器。
  • 磁盘检查
    为了验证主服务器是否已经将数据发送到从服务器,需要向主服务器发送一个唯一的虚构值,这个虚构值也将发送到从服务器,通过检查从服务器是否含有虚构值就可以判断,是否发送成功。
  • 4.3 处理系统故障(略)
  • 4.4 Redis事物
    redsi事物并不同于mysql中的事物,即它并不会保证事物的原子性和自动回滚,需要用户自己设置判断。具体信息如下:

单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增加任何维持原子性的机制,所以 Redis 事务的执行并不是原子性的。
事务可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。
例子:如下的例子为模拟游戏商城中,卖家发布自己的商品,和卖家购买商品的逻辑。

  • 4.5 非事物型流水线
    通过上面的redis事物,我们知道,redis的事物并不是真正意义上的事物,而是由两部分组成,一部分为通过pipeline流水线,批量执行命令,这样大大增加写效率;另一个通过使用multi和exec命令,保证当前的事物不被其他客户端干扰,从而一定程度上避免数据冲突。我们发现,如果我们并不需要数据安全,但是,觉得单个命令执行过慢,那么,是否就可以使用事事务的第一个逻辑,进行批量写入,即只使用流水线功能,但是不屏蔽其他客户端。例子如下:
  • 总结
    数据库总是在读入和写入之间达到平衡,写入快(如追加),则查找慢(数据不规律);写入慢(进行有规律的写入),则查找就快。之所以导致这个原因,是由于硬盘的逻辑结构和物理逻结构并不相同,我们存储时,都是根据物理结构,但是查找使用逻辑结构,所以效率降低。
    另外,数据库存储总是在安全和性能之间平衡,为了安全就必须对数据进行严格检查加锁,但是为了效率,又必须进行加锁同步,而不是异步执行。所以,需要找到一个折中,如果数据安全不是太严格,就可以适当降低安全限制,使用乐观锁。但此时,用户需要自己创建检查。
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Pipeline;
import redis.clients.jedis.Transaction;
import redis.clients.jedis.Tuple;

import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * @author: ZouTai
 * @date: 2018/7/3
 * @description: 第4章:数据安全与性能保障
 * @create: 2018-07-03 11:25
 * 不同于关系型数据库的事务,redis数据库事务并不是使用悲观锁,
 * 在执行事务时,1、不会检查相关变量,2、也不会自动执行重试
 * 所以需要开发者自己手动检查变量,手动重试(redis类似于悲观锁,线程不加锁,但是需要检查)
 */
public class Chapter04 {
    public static void main(String[] args) {
        new Chapter04().run();
    }

    private void run() {
        Jedis conn = new Jedis("localhost");
        conn.select(15);

        testListItem(conn,false);
        testPurchaseItem(conn);
        testBenchMark(conn);
    }

    private void testBenchMark(Jedis conn) {
        benchmarkUpdateToken(conn, 5);
        benchmarkUpdateToken2(conn, 5);
    }

    /**
     * 不使用反射
     * @param conn
     * @param duration
     */
    private void benchmarkUpdateToken2(Jedis conn, int duration) {
        int count = 0;
        long start = System.currentTimeMillis();
        long end = System.currentTimeMillis() + duration * 1000;
        while (start < end) {
            updateToken(conn, "token", "user", "item");
        }

        long delta = System.currentTimeMillis() - start;
        System.out.println(
                "updateToken" + ' ' +
                        count + ' ' +
                        (delta / 1000) + ' ' +
                        (count / (delta / 1000)));


        count = 0;
        start = System.currentTimeMillis();
        end = System.currentTimeMillis() + duration * 1000;
        while (start < end) {
            updateTokenPipeline(conn, "token", "user", "item");
        }

        delta = System.currentTimeMillis() - start;
        System.out.println(
                "updateToken" + ' ' +
                        count + ' ' +
                        (delta / 1000) + ' ' +
                        (count / (delta / 1000)));
    }

    private void benchmarkUpdateToken(Jedis conn, int duration) {
        try {
            // 使用java反射功能,减少函数冗余(可以对比发现)
            @SuppressWarnings("rawtypes")
            Class[] args = new Class[]{
                Jedis.class, String.class, String.class, String.class
            }; // 定义4个函数参数
            Method[] methods = new Method[]{
                this.getClass().getDeclaredMethod("updateToken", args),
                this.getClass().getDeclaredMethod("updateTokenPipeline", args),
            };
            for (Method method : methods) {
                int count = 0;
                long start = System.currentTimeMillis();
                long end = System.currentTimeMillis() + duration * 1000;
                while (start < end) {
                    method.invoke(this, conn, "token", "user", "item");
                }

                long delta = System.currentTimeMillis() - start;
                System.out.println(
                    method.getName() + ' ' +
                    count + ' ' +
                    (delta / 1000) + ' ' +
                    (count / (delta / 1000)));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 使用非事务模式,但是集成提交任务
     * @param conn
     * @param token
     * @param user
     * @param item
     */
    private void updateTokenPipeline(Jedis conn, String token, String user, String item) {
        long timestamp = System.currentTimeMillis() / 1000;
        Pipeline pipe = conn.pipelined();
        pipe.multi();
        pipe.hset("login:", token, user);
        pipe.zadd("recent:", timestamp, token);
        if (item != null) {
            pipe.zadd("viewed:" + token, timestamp, item);
            pipe.zremrangeByRank("viewed:" + token, 0, -26);
            pipe.zincrby("viewed:", -1, item);
        }
        pipe.exec();
    }

    private void updateToken(Jedis conn, String token, String user, String item) {
        long timestamp = System.currentTimeMillis() / 1000;
        conn.hset("login:", token, user);
        conn.zadd("recent:", timestamp, token);
        if (item != null) {
            conn.zadd("viewed:" + token, timestamp, item);
            conn.zremrangeByRank("viewed:" + token, 0, -26);
            conn.zincrby("viewed:", -1, item);
        }
    }

    /**
     * 测试2:购买
     * @param conn
     */
    private void testPurchaseItem(Jedis conn) {
        testListItem(conn, true);
        // 初始化买家信息
        conn.hset("users:buyerId", "funds", "100");
        conn.hset("users:buyerId", "name", "buyerName");
        Map<String, String> buyersMap = conn.hgetAll("users:buyerId");
        System.out.println("输出卖家信息:");
        for (Map.Entry<String, String> entry : buyersMap.entrySet()) {
            System.out.println(" " + entry.getKey() + " " + entry.getValue());
        }
        boolean result = purchaseItem(conn, "userID1", "Item1", "buyerId", 10);
    }

    /**
     * 购买
     * @param conn
     * @param sellerId
     * @param itemId
     * @param buyerId
     * @param lprice
     * 以下逻辑类似于发布商品
     */
    private boolean purchaseItem(Jedis conn, String sellerId, String itemId, String buyerId, int lprice) {
        String buyer = "users:" + buyerId;
        String seller = "users:" + sellerId;
        Long endTime = System.currentTimeMillis() + 5000;
        String inventory = "inventory:" + buyerId;
        String item = itemId + '.' + sellerId;
        while (System.currentTimeMillis() < endTime) {
            conn.watch("market:", buyer);
            double price = conn.zscore("market:", item);
            double funds = Double.parseDouble(conn.hget(buyer, "funds"));
            if (price != lprice || price > funds) {
                conn.unwatch();
                return false;
            }
            Transaction trans = conn.multi();
            trans.hincrBy(seller, "funds", (long) price);
            trans.hincrBy(buyer, "funds", (long) -price);
            trans.sadd(inventory, itemId);
            trans.zrem("market:", item);
            List<Object> results = trans.exec();
            // null response indicates that the transaction was aborted due to
            // the watched key changing.
            if (results == null) {
                continue;
            }
            return true;
        }
        return false;
    }

    /**
     * 测试1:
     * 使用事务检查watch,将商品放到市场上
     * @param conn
     * @param nested
     */
    private void testListItem(Jedis conn, boolean nested) {
        if (!nested){
            System.out.println("\n----- testListItem -----");
        }

        String sellerId = "userID1";
        String item = "Item1";
        conn.sadd("inventory:" + sellerId, item);

        System.out.println("当前卖家拥有的商品有:");
        Set<String> itemSet = conn.smembers("inventory:" + sellerId);
        for (String one : itemSet) {
            System.out.println("  " + one);
        }

        listItem(conn, item, sellerId, 10);

        // 输出商场商品:
        System.out.println("输出商场商品:");
        Set<Tuple> sellingItemSets = conn.zrangeWithScores("market:", 0, -1);
        for (Tuple t : sellingItemSets) {
            System.out.println("  " + t.getElement() + "---" + t.getScore());
        }
    }

    /**
     * 发放商品
     *
     * @param conn
     * @param item
     * @param sellerId
     * @param price
     */
    private boolean listItem(Jedis conn, String item, String sellerId, double price) {
        long endTime = System.currentTimeMillis() + 5000;

        String inventory = "inventory:" + sellerId;
        String itemSellerId = item + "." + sellerId;
        while (System.currentTimeMillis() < endTime) {

            // 1.1 监视表inventory,线程可见;乐观锁
            conn.watch(inventory);
            // 1.2 检查卖家是否有这个商品,若没有,返回false
            if (!conn.sismember(inventory, item)) {
                conn.unwatch();
                return false;
            }
            // 1.3 开启事务流水线
            Transaction transaction = conn.multi();
            transaction.zadd("market:", price, itemSellerId);
            transaction.srem(inventory, item);
            // 1.4 执行流水线操作
            List<Object> results = transaction.exec();

            // 1.5 如果事务执行失败,则重试-继续执行,直到超时
            if (results == null) {
                continue;
            }
            return true;
        }
        return false;
    }
}