JAVA架构(六)--------Redis

                <p>⦁&nbsp;&nbsp; &nbsp;Redis概述<br> ⦁&nbsp;&nbsp; &nbsp;&nbsp; 什么是NOSQL<br> &nbsp;&nbsp; nosql工具也是一种简易数据库,它主要是一种基于内存的数据库,并提供一定的持久化功能。Redis和MongoDB是当前使用最广泛的NOSQL。我们课上主要介绍的是Redis技术,它的性能十分优越,可以支持每秒十几万的读/写操作,其性能远超数据库,并支持集群,分布式,主从同步等配置,原则上可以无限拓展,让更多的数据存储在内存中,而更让我们感到欣喜的是它还支持一定的事务能力,这在高并发访问的场景下保证数据安全和一致性特别有用。<br> ⦁&nbsp;&nbsp; &nbsp;Redis性能优越的原因<br> 基于ANSI C语言编写的,接近于汇编语言的机器语言,运行十分快速。<br> 基于内存的读/写,速度自然比数据库的磁盘读写要快很多。<br> 它的数据库结构只有6种类型,数据结构比较简单,因此规则较少,而数据库则是范式,完整性,规范性需要考虑的规则较多,处理业务会比较复杂。<br> ⦁&nbsp;&nbsp; &nbsp;Redis在Java Web中的应用<br> ⦁&nbsp;&nbsp; &nbsp;缓存<br> &nbsp;<br> ⦁&nbsp;&nbsp; &nbsp;高速读写场合<br> &nbsp;<br> ⦁&nbsp;&nbsp; &nbsp;Redis基本安装和使用<br> ⦁&nbsp;&nbsp; &nbsp;在Windows下安装Redis<br> &nbsp; 打开网址:https://github.com/ServiceStack/redis-windows/tree/master/downloads<br> &nbsp;就可看到如下所示界面</p> 

 
把 Redis 文件下载下来,进行解压缩
 
 
为了方便,我们可以在这个目录,新建一个文件 startup.cmd, 用记事本打开,输入如下内容:
redis-server redis.windows.conf
这个命令调用 redis-server.exe 的命令读取    的内容,用来启动 redis, 保存好了双击就可以看到 Redis 启动的信息了。

 
启动成功。
这时候可以双击放在同一个文件夹下文件 redis-cli.exe, 它是一个 Redis 自带的客户端工具,这样就可以连接到 Redis 服务器了。
客户端工具:https://github.com/ServiceStackApps/RedisReact#download
 


⦁    简介 Redis 的 6 种数据类型
 Redis 是一种基于内存的数据库,并且提供一定的持久化功能,它是一种键值(key-value)数据库,使用 key 作为索引找到当前缓存的数据,并返回给程序调用者。当前的 Redis 支持 6 中数据类型,它们分别是字符串(String)、列表(List)、集合(set)、哈希结构(hash)、有序集合(zset)和基数(HyperLogLog)。使用 Redis 编程要熟悉这 6 种数据类型,并且了解它们常用的命令。Redis 定义的这 6 种数据类型是十分有用的,它除了提供简单的存储功能外,还能对存储的数据进行一些计算,比如字符串可以支持浮点数的自增、自减、字符串求子串、集合求交集、并集、有序集合进行排序等,所以使用它们有利于对一些不太大的数据集合进行快速计算,简化编程,同时它也比数据库要快得多,所以它对系统性能的提升十分有意义。
                        Redis 的 6 种数据类型的基本描述
 
此外,Redis 还支持一些事务、发布订阅消息模式、主从复制、持久化等作为 Java 开发人员需要知道的功能。
⦁    在 Spring Boot 中使用 Redis
⦁       引入 Spring-boot-stater-data-redis
   <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <!-- 不依赖 Redis 的异步客户端,因为默认是依赖这个的,我想用的是 jedis 驱动 -->
            <exclusions>
                <exclusion>
                    <groupId>io.lettuce</groupId>
                    <artifactId>lettuce-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- 引入 Redis 的客户端驱动 jedis -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>
⦁    Spring-data-redis 项目简介
    在 Java 中与 Redis 连接的驱动存在很多中,目前比较广泛使用的是 Jedis,其他还有 Lettue、
Jredis 和 Srp。我们推荐使用的类库 Jedis。当我们在项目中引入 Spring-data-redis 项目后,Spring 提供了一个 RedisConnectionFactory 接口,通过它可以生成一个 RedisConnection 接口对象,而 RedisConnection 接口对象是对 Redis 底层接口的封装。例如,这里我们使用的 Jedis 驱动,那么 Spring 会提供 RedisConnection 接口的实现类 JedisConnection 去封装原有的 Jedis(redis.clients.jedis.Jedis)对象。
在 Spring 中是通过 RedisConnection 接口操作 Redis 的,而 RedisConnection 则是对原生的 Jedis 进行封装的。要想获得 RedisConnection 接口对象,使用通过 RedisConnectionFactory 接口去生成的,所以第一步要配置的便是这个工厂了,而配置这个工厂主要是配置 Redis 的连接池,对于连接池可以限定其最大连接数、超时时间等属性。
⦁    创建 RedisConnectionFactory 对象

@Configuration
public class RedisConfig {

    private RedisConnectionFactory connectionFactory = null;

    @Bean(name = "RedisConnectionFactory")
    public RedisConnectionFactory initRedisConnectionFactory() {

        if (this.connectionFactory != null) {
            return this.connectionFactory;
        }
        JedisPoolConfig poolConfig = new JedisPoolConfig();

        // 最大空闲数
        poolConfig.setMaxIdle(30);
        // 最大连接数
        poolConfig.setMaxTotal(50);
        // 最大等待毫秒数
        poolConfig.setMaxWaitMillis(2000);
        // 创建 Jedis 连接工厂
        JedisConnectionFactory connectionFactory = new JedisConnectionFactory(poolConfig);
        // 获取单机的 Redis 配置
        RedisStandaloneConfiguration rsCfg = connectionFactory.getStandaloneConfiguration();
        connectionFactory.setHostName("localhost");
        connectionFactory.setPort(6379);
        // connectionFactory.setPassword(password);

        this.connectionFactory = connectionFactory;
        return connectionFactory;

    }
}


这里通过一个连接池的配置创建了一个 RedisConnectionFactory,通过它就能创建 RedisConnection 接口对象了。但是我们在使用一条连接时,需要先从 RedisConnectionFactory 工厂获取,然后在使用完成还要关闭它,Spring 为了进一步简化开发,提供了 RedisTemplate。
⦁    RedisTemplate
RedisTemplate 是一个强大的类,首先它会自动从 RedisConnectionFactory 工厂中获取连接,然后执行对应的 Redis 命令,在最后还会关闭 Redis 的连接。这些在 RedisTemplate 中都被封装了,所以开发者并不需要开发者关注 Redis 连接的闭合问题。
在上面的配置类中添加如下代码清单。
@Bean(name = "redisTemplate")
    public RedisTemplate<Object, Object> initRedisTemplate() {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(initRedisConnectionFactory());
        return redisTemplate;
    }
⦁    测试 RedisTemplate
public class Main {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(RedisConfig.class);
        RedisTemplate redisTemplate = ctx.getBean(RedisTemplate.class);
        // 拿到 redisTemplate 之后就可以操作 redis 了
        redisTemplate.opsForValue().set("key1", "value1");

        redisTemplate.opsForHash().put("hash", "fidld", "hvalue");
        
    }
}  

⦁    使用 Redis 命令查询键信息
 
⦁    序列化器和反序列化器
通过上面步骤可以看到,Redis 存入的并不是”key1”这样的字符串,这是怎么回事呢?
首先需要清楚,Redis 是一种基于字符串存储的 NOSQL,而 Java 是基于对象的语言,对象是无法存储到 Redis 中的,不过 Java 提供了序列化机制,只需要实现了 java.io.Serializable 接口,就代表这个对象能够进行序列化,通过将类对象进行序列化就能够得到二进制字符串,这样 Redis 就可以将这些对象以字符串进行存储。Java 也可以将那些二进制字符串通过反序列化转为对象,通过这个原理,Spring 提供了序列化器的机制,并且实现了几个序列化器。
对于序列化器,Spring 提供了 RedisSerializer 接口,它有两个方法:serialize, 它能把那些可以序列化的对象转换为二进制字符串。 deserialize, 它能通过反序列化把二进制字符串转换为 Java 对象。
我们主要讨论的是 StringRedisSerializer 和 JdkSerializationRedisSerializer, 其中 JdkSerializationRedisSerializer 是 RedisTemplate 默认的序列化器。所以看到了上面的结果。

 
RedisTemplate 提供了如下几个可以配置的属性

属性    描述    备注
defaultSerializer    默认序列化器    如果没设置,则使用 JdkSerializationRedisSerializer
keySerializer    Redis 键序列化器    如果没设置,则使用默认序列化器
valueSerializer    Redis 值序列化器    如果没设置,则使用默认序列化器
hashKeySerializer    散列结构 field 序列化器    如果没设置,则使用默认序列化器
hashValueSerializer    Redis 散列结构 value 序列化器    如果没设置,则使用默认序列化器
stringSerializer    字符串序列化器     RedisTemplate 自动赋值为 StringRedisSerializer 对象

   通过上述讲解我们可以看到,在上面的例子中,我们什么都没设置,因此默认使用 JdkSerializationRedisSerializer 对对象进行序列化和反序列化。所以我们得到的是复杂的字符串,为了方便我们查询 Redis 数据,我们希望将 Redis 的键以普通字符串保存。
⦁    使用字符串序列化器

    @Bean(name = "redisTemplate")
    public RedisTemplate<Object, Object> initRedisTemplate() {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(initRedisConnectionFactory());
        // RedisTemplate 会自动初始化 StringRedisSerializer,所以这里可以直接获取
        RedisSerializer stRedisSerializer = redisTemplate.getStringSerializer();
        // 设置字符串序列化器,这样 Spring 就会把 Redis 的 key 当做字符串处理了
        redisTemplate.setKeySerializer(stRedisSerializer);
        redisTemplate.setHashKeySerializer(stRedisSerializer);
        redisTemplate.setHashValueSerializer(stRedisSerializer);
        return redisTemplate;
    }

    
⦁    Spring 对 Redis 数据类型操作的封装
Redis 能够支持 7 种数据类型,字符串、散列、列表(链表)、集合、有序集合、基数和地理位置。
为此,Spring 针对每一种数据结构的操作都提供了对应的操作接口。
 
它们都可以通过 RedisTemplate 得到,
  // 获取 Redis 数据类型操作接口

// 获取地理位置操作接口
        redisTemplate.opsForGeo();
        // 获取散列操作接口
        redisTemplate.opsForHash();
        // 获取基数操作接口
        redisTemplate.opsForHyperLogLog();
        // 获取列表操作接口
        redisTemplate.opsForList();
        // 获取集合操作接口
        redisTemplate.opsForSet();
        // 获取字符串操作接口
        redisTemplate.opsForValue();
        // 获取有序集合操作接口
        redisTemplate.opsForZSet();
⦁    Redis 数据结构常用命令
⦁      Redis 数据结构 ----- 字符串
   字符串是 Redis 最基本的数据结构,它将以一个键和一个值存储于 Redis 内部,它犹如 Java 的 Map 结构,让 Redis 通过键去找到值。

 

 

                              字符串的一些基本命令
命令    说明    备注
Set key value    设置键值对    最常用的写入命令
get key    通过键获取值    最常用的读取命令
del key    通过 key,删除键值对    删除命令。返回删除数,注意,它是一个通用命令,换句话说在其他数据结构中,也可以使用它

Strlen key    求 key 指向字符串的长度    返回长度
getset key value      修改原来 key 的对应值,并将旧值返回    如果原来值为空,则返回为空,并设置新值
getrange key start end    获取子串    记字符串的长度为 len,把字符串看作一个数组,而 redis 是以 0 开始计数的,所以 start 和 end 的取值范围为 0 到 len-1

append key value    将新的字符串 value 加入到原来 key 指向的字符串末    返回 key 指向新字符串的长度
public class RedisMain {

    public static void main(String[] args) {
        
        
        ApplicationContext ctx=new AnnotationConfigApplicationContext(RedisConfig.class);
        RedisTemplate redisTemplate=ctx.getBean(RedisTemplate.class);
        // 设值
        redisTemplate.opsForValue().set("key1", "value1");
        redisTemplate.opsForValue().set("key2", "value2");
        // 通过 key 获取值
        String value1=(String) redisTemplate.opsForValue().get("key1");
        System.out.println(value1);
        // 通过 key 删除值
        redisTemplate.delete("key1");
        // 求长度
        Long length=redisTemplate.opsForValue().size("key2");
        System.out.println(length);
        
        // 设置新值并返回旧值
        String oldValue2=(String) redisTemplate.opsForValue().getAndSet("key2", "new_value2");
        System.out.println(oldValue2);
        // 求子串
        String rangValue2=redisTemplate.opsForValue().get("key2",0, 3);
        System.out.println(rangValue2);
        // 追加字符串到末尾
        int newLen=redisTemplate.opsForValue().append("key2", "_app");
        System.out.println(newLen);
        
        

    }

}
命令    说明    备注
incr key    在原字段上加 1    只能对整数操作
incrby  key increment    在原字段上加上整数(increment)    只能对整数操作
decr key    在原字段上减 1    只能对整数操作
decrby key decrement     在原字段上减去整数(decrement)    只能对整数操作
Incrbyfloat key   increment    在原来字段上加上浮点数 increment    可以操作浮点数或者整数
⦁    Redis 数据结构 --------- 哈希
Redis 中哈希结构就如同 java 的 map 一样,一个对象里面有许多键值对,它特别适合存储对象的。在 redis 中,hash 是一个 String 类型的 field 和 value 的映射表,因此我们存储的数据实际在 redis 内存中都是一个个字符串而已。
                        Redis   hash 结构命令
        命令           说明             备注
hdel key field     删除 hash 结构中某个字段    可以进行多个字段的删除
hexists  key  field    判断 hash 结构中是否存在 field 字段    存在返回 1,否则返回 0
hgetall  key      获取所有 hash 结构中的键值    返回键和值
hincrby key field increment    指定给 hsah 结构中的某个字段上加一个整数    要求该字段也是整数字符串
hincrbyfloat key field increment    指定给 hash 结构中的某个字段加上一个浮点数    要求该字段也是数字型字符串
hkeys  key    返回 hash 中所有的键    ————
hlen   key     返回 hash 中键值对的数量    ————
hmget  key field     返回 hash 中指定的键和值可以是多个    依次返回
hmset  key field1 value1
    Hash 结构中设置设置多个键值对    ———
hset  key field value    在 hash 结构中设置键值对    单个设值
hsetnx key field value
    当 hash 结构中不存在对应的键才设置值    ———
hvals key
    获取 hash 结构中的所有值    ———
    String key ="hash";
        Map<String,String> map=new HashMap<>();
        map.put("f1", "v1");
        map.put("f2", "v2");
        // 相当于 hmset 命令
        redisTemplate.opsForHash().putAll(key, map);
        // 相当于 hset 命令
        redisTemplate.opsForHash().put(key, "f3", 6);
        // 相当于 hexists key filed 命令
        boolean exists=redisTemplate.opsForHash().hasKey(key, "f3");
        // 相当于 hgetall 命令
        Map keyValMap=redisTemplate.opsForHash().entries(key);
        // 相当于 hincrby 命令
        redisTemplate.opsForHash().increment(key, "f3", 2);
        // 相当于 hincrbyfloat 命令
        redisTemplate.opsForHash().increment(key, "f3", 0.88);
        // 相当于 hvals 命令
        List valueList=redisTemplate.opsForHash().values(key);
        // 相当于 hkeys 命令
        Set keyList=redisTemplate.opsForHash().keys(key);
        
        List<String> fieldList=new ArrayList<String>();
        fieldList.add("f1");
        fieldList.add("f2");
        // 相当于 hmget 命令
        List vaList2=redisTemplate.opsForHash().multiGet(key,keyList);
        // 相当于 hsetnx 命令
        boolean success =redisTemplate.opsForHash().putIfAbsent(key, "f4", "val4");
        // 相当于 hdel 命令
        Long result=redisTemplate.opsForHash().delete(key, "f1","f2");
⦁            Redis 数据结构——链表
链表结构是 Redis 中一个常用的结构,它可以存储多个字符串,而且它是有序的。Redis 链表是双向的,可以从左到右,也可以从右到左遍历它存储的节点。

但是使用链表结构就意味着读性能的丧失,而链表结构的优势在于插入和删除的便利。
命令    说明    备注
Lpush  key node1 [node2]…    把节点 node1 加入到链表最左边    如果 node1、node2 …noden 这样加入,那么链表开头从左到右的顺序是 noden、…node2、node1
Rpush key node1 [node2]…    把节点 node1 加入到链表的最右边    如果 node1、node2 …noden 这样加入,那么链表开头从左到右的顺序是 node1、node2、..noden
Lindex key index    读取下标为 index 的节点    返回节点字符串,从 0 开始算
Llen key    求链表的长度    返回链表节点数
Lpop key    删除左边第一个节点,并将其返回    
Rpop key    删除右边第一个节点,并将其返回    
Linsert key before|after pivot  node    插入节点 node,并且可以指定在值为 pivot 的节点的前面或后面    如果列表不存在,则报错;如果没有值为对应的 pivot, 也会插入失败返回 -1
Lpushx list node    如果存在 key 为 list 的链表,则插入节点 node,并且作为从左到右的第一个节点    如果 list 不存在,则失败
Rpushx list node     如果存在 key 为 list 的链表,则插入节点 node,并且作为从左到右的最后一个节点    如果 list 不存在,则失败
Lrange list start end    获取链表 list 从 start 下标到 end 下标的节点值    包含 start 和 end 下标
Lrem list count value    如果 count 为 0,则删除所有值等于 value 的节点; 如果 count 不是 0,则先对 count 取绝对值,假设绝对值为 abs,然后从左到右删除不大于 abs 个等于 value 的节点    
Lset key index node     设置列表下标为 index 的节点的值为 node    
Ltrim key start stop    修剪链表,只保留从 start 到 stop 的区间的节点,其余的都删除掉    包含 start 和 end 的下标的节点会保留
需要指出的是,之前这些操作链表的命令都是进程不安全,因为当我们操作这些命令的时候,其他 redis 的客户端也可能操作同一个链表,这样就会造成并发数据安全和一致性的问题,尤其是当你操作一个数据量不小的链表结构时,常常会遇到这样的问题。为了克服这些问题,redis 提供了链表的阻塞命令,它们在运行的时候,会给链表加锁,以保证操作链表的命令安全性。
                        链表的阻塞命令
       命 令          说明       备注
Blpop key timeout    移出并获取列表的第一个元素,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止    相当于 lpop 命令,它的操作是进程安全的
(timeout: 单位是秒)
Brpop key timeout    移出并获取列表的最后一个元素,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止    相当于 rpop 命令,它的操作是进程安全的
Rpoplpush   src  dest    按从左到右的顺序,将一个链表的最后一个元素移除,并插入目标链表最左边    不能设置超时时间
Brpoplpush  src dest timeout    按从左到右的顺序,将一个链表的最后一个元素移除,并插入到目标链表左边,并可以设置超时时间    可设置超时时间
当使用这些命令时,Redis 就会对对应的链表加锁,加锁的结果就是其他的进程不能再读取或写入该链表,只能等待命令结束。
public class RedisMain3 {

    public static void main(String[] args) throws UnsupportedEncodingException {

        ApplicationContext ctx = new AnnotationConfigApplicationContext(RedisConfig.class);
        RedisTemplate redisTemplate = ctx.getBean(RedisTemplate.class);
        // 删除链表,以便我们可以反复测试
        redisTemplate.delete("list");
        // 把 node3 插入链表 list
        redisTemplate.opsForList().leftPush("list", "node3");
        List<String> nodeList = new ArrayList<>();
        for (int i = 2; i >= 1; i++) {
            nodeList.add("node" + i);
        }
        // 相当于 lpush 把多个值从左插入链表
        redisTemplate.opsForList().leftPushAll("lsit", nodeList);
        // 从右边插入一个节点
        redisTemplate.opsForList().rightPush("list", "node4");
        // 获取下标为 0 的节点
        String node1 = (String) redisTemplate.opsForList().index("list", 0);

        // 获取链表长度
        long size = redisTemplate.opsForList().size("list");
        // 从左边弹出一个节点
        String lpop = (String) redisTemplate.opsForList().leftPop("list");
        // 从右边弹出一个节点
        String rpop = (String) redisTemplate.opsForList().rightPop("list");

        // 注意,需要使用更为底层的命令才能操作 linsert 命令
        // 使用 linset 命令在 node2 前插入一个节点
        redisTemplate.getConnectionFactory().getConnection().lInsert("list".getBytes("utf-8"),
                RedisListCommands.Position.AFTER, "node2".getBytes("utf-8"), "after_node".getBytes("utf-8"));
        // 判断 list 是否存在,如果存在则从左边插入 head 节点
        redisTemplate.opsForList().leftPushIfPresent("lsit", "head");
        // 判断 list 是否存在,如果存在则从右边插入 end 节点
        redisTemplate.opsForList().rightPushIfPresent("list", "end");
        // 从左到右,或者下标 0 到 10 的节点元素
        List vaList = redisTemplate.opsForList().range("list", 0, 10);

        nodeList.clear();

        for (int i = 1; i <= 3; i++) {
            nodeList.add("node");
        }
        // 在链表左边插入三个值为 node 的节点
        redisTemplate.opsForList().leftPushAll("list", nodeList);
        // 从左到右删除至多 3 个节点
        redisTemplate.opsForList().remove("list", 3, "node");
        // 给链表下标为 0 的节点设置新值
        redisTemplate.opsForList().set("list", 0, "new_head_value");

        // ------Spring 对 Redis 阻塞命令的操作 --------------
        redisTemplate.delete("lsit1");
        redisTemplate.delete("lsit2");
        // 初始化链表 list1
        List<String> nodeList2 = new ArrayList<>();
        for (int i = 1; i <= 5; i++) {
            nodeList2.add("node" + i);
        }
        redisTemplate.opsForList().leftPushAll("lsit1", nodeList);
        // sping 使用参数超时时间作为阻塞命令区分,等价于 blpop 命令,并且可以设置时间参数
        redisTemplate.opsForList().leftPop("list", 1, TimeUnit.SECONDS);
        // Spring 使用参数超时时间作为阻塞命令区分,等价与 brpop 命令,并且可以设置时间参数
        redisTemplate.opsForList().rightPop("lsit", 1, TimeUnit.SECONDS);
        nodeList2.clear();
        // 初始化链表 list2
        for (int i = 1; i <= 3; i++) {
            nodeList2.add("data" + i);
        }
        redisTemplate.opsForList().leftPushAll("lsit2", nodeList2);
        // 相当于 rpoplpush 命令,弹出 list1 最右边的节点,插入到 list2 最左变
        redisTemplate.opsForList().rightPopAndLeftPush("lsit1", "lsit2");
        // 相当于 brpoplpush 命令,弹出 list1 最右边的节点,插入到 list2 最左边,注意在 spring 中使用超时参数区分
        redisTemplate.opsForList().rightPopAndLeftPush("lsit1", "lsit2", 1, TimeUnit.SECONDS);

    }

}
⦁    Redis 数据结构——集合
Redis 的集合不是一个线性结构,而是一个哈希表结构,它的内部会根据 hash 分子来存储和查找数据。因为采用哈希表结构,所以对于 Redis 集合的插入、删除和查找的复杂度都是 O(1).
⦁    对于集合而言,它的每一个元素都是不能重复的,当插入相同的记录的时候会失败。
⦁    集合是无序的
⦁    集合的每一个元素都是 String 数据结构类型
        命令            说明       备注
Sadd key member1 [member2 …]    给键为 key 的集合增加成员    可以同时增加多个
Scard key      统计键为 key 的集合成员数    
Sdiff key1 [key2]    找出两个集合的差集,谁在前以谁为基准    参数如果单是 key。那么 Redis 就返回这个 key 的所有元素
Sdiffstore des key1 [key2]    先按 sdiff 命令规则,找出 key1 和 key2 两个集合的差集,然后将其保存到 des 集合中    
Sinter key1 [key2]    求 key1 和 key2 两个集合的交集    参数如果单是 key。那么 Redis 就返回这个 key 的所有元素
Sinterstore des key1 [key2]    先按 sinter 命令规则,找出 key1 和 key2 两个集合的交集,然后将其保存到 des 集合中    
Sismember key member    判断 member 是否键为 key 的集合成员    如果是返回 1,否则返回 0
Smembers key    返回集合中所有成员    
Smove src des member    将成员 member 从集合 src 迁徙到 des 中    
Spop key    随机弹出 (删除) 集合的一个元素    注意其随机性,因为集合元素是无序的
Srandmember key [count]    随机返回 (不删除) 集合中一个或多个元素,count 为限制返回总数,如果 count 为负数,则先求其绝对值    Count 为整数,如果不填默认为 1,如果 count 大于等于集合总数,则返回整个集合
Srem key member1[member2..]    移除集合中的元素,可以是多个元素    
Sunion key1 [key2]    求两个集合的并集    参数如果是单个 key,那么 redis 就返回这个 key 所有的元素
Sunionstore des key1 key2    先执行 sunion 命令求出交集,然后保存到键为 des 的集合中    

/**
 * 使用 Spring 测试 Redis 集合
 *
 * @author apple
 *
 */
public class RedisMain4 {

    public static void main(String[] args) {

        ApplicationContext ctx = new AnnotationConfigApplicationContext(RedisConfig.class);
        RedisTemplate redisTemplate = ctx.getBean(RedisTemplate.class);
        Set set = null;
        // 将元素加入列表
        redisTemplate.boundSetOps("set1").add("v1", "v2", "v3", "v4", "v5", "v6");
        redisTemplate.boundSetOps("set2").add("v0", "v2", "v4", "v6", "v8");
        // 求集合长度
        redisTemplate.opsForSet().size("set1");
        // 求差集
        set = redisTemplate.opsForSet().difference("set1", "set2");
        // 求并集
        set = redisTemplate.opsForSet().intersect("set1", "set2");

        // 判断是否集合中元素
        boolean exists = redisTemplate.opsForSet().isMember("set1", "v1");
        // 获取集合中所有的元素
        set = redisTemplate.opsForSet().members("set1");
        // 从集合中随机弹出一个元素
        String val = (String) redisTemplate.opsForSet().pop("set1");
        // 随机获取一个集合的元素
        val = (String) redisTemplate.opsForSet().randomMember("set1");
        // 随机获取 2 个集合元素
        List list = redisTemplate.opsForSet().randomMembers("set1", 2L);
        // 删除一个集合元素,参数可以是多个
        redisTemplate.opsForSet().remove("set1", "v1");
        // 求两个集合的并集
        redisTemplate.opsForSet().union("set1", "set2");
        // 求两个集合的差集,并保存到集合 differ_set
        redisTemplate.opsForSet().differenceAndStore("set1", "set2", "diff_set");
        // 求两个集合的交集,并保存到集合 inter_set 中
        redisTemplate.opsForSet().intersectAndStore("set1", "set2", "inter_set");
        // 求两个集合的并集,并保存到集合 union_set 中
        redisTemplate.opsForSet().unionAndStore("set1", "set2", "union_set");

    }

}
⦁    Redis 数据结构——有序集合
有序集合和集合类似,只是说它是有序的,和无序集合的主要区别在于每一个元素除了值之外,还会多一个分数。分数是一个浮点数,在 Java 中是使用双精度表示,根据分数,Redis 就可以支持对分数从小到大或者从大到小的排序。这里和无序集合一样,对于每个元素都是唯一的,但是对于不同的元素而言,它的分数可以一样。元素也是 String 数据类型,也是一种基于 hash 的存储结构。集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 o(1).
有序集合是依赖 key 标识它属于哪个集合,依赖分数进行排序,所以值和分数是必须的,而实际上不仅仅可以对分数进行排序,在满足一定条件下,也可以对值进行排序。

    命令          说明            备注
Zadd key score1 value1[score2 value2….]    向有序集合的 key,增加一个或多个成员    如果不存在对应的 key,则创建键为 key 的有序集合
Zcard key     获取有序集合的成员数    
Zcount key min max    根据分数返回对应的成员列表的个数    Min 为最小值,max 为最大值,默认包含 min 和 max
Zincrby key increment member    给有序集合成员为 member 的分数增加 increment    
Zinterstore desKey numkeys key1[key2 key3…]    求多个集合的交集,并将结果保存到 deskey 中    Number 是一个整数,表示多少个有序集合
Zlexcount key min max    求有序集合 key 成员值在 min 和 max 的范围    
Zrang key start stop    按分数的大小(从小到大)返回成员,加入 start 和 stop 参数可以截取某一段返回。如果输入可选项 withscore,则连同分数一起返回    包含 start 和 stop,这个 start 和 sop 是下标
Zrank key member    按从小到大求有序集合的排行    排名第一为 0
Zrangebylex by min max[limit offset count]    根据值的大小,从小到大排序,min 为最小值,max 为最大值,limit 选项可选,当 Redis 求出范围集合后,会产生下标 0 到 n,然后根据偏移量 offset 和限定返回数 count,返回对应的成员     zrangebylex zset3  [1 [3 limit 1
Zrangebyscore key min max [withscores] [limit offset count]    根据分数大小,从小到大求范围    zrangebyscore zset3 0.1 0.3 withscores  limit 0 2
Zremrangebyscore key start stop    根据分数区间进行删除    
Zremrangebyrank key start stop     按照分数排行从小到大的排序删除,从 0 开始计算    
Zremrangebylex key min max    按照值的分布进行删除    
Zrevrange key start stop [withscore]    从大到小的按分数排序    
Zrevrangebyscore key max min[withscores]    从大到小的按分数排序    
Zrevrank key member     按从大到小的顺序,求元素的排行    
Zscore key member    返回成员的分数值    返回成员的分数
Zunionstore deskey numkeys key1[key2 key3 …]    求多个有序集合的并集,其中 numKeys 是有序集合的个数    
⦁    基数——HyperLogLog
    基数是一种算法。举个例子,一本英文著作由数百万个单词组成,你的内存却不足以存储他们,那么我先分析一下业务。英文单词本身是有限的,在这本书中的几百万个单词中有许多的重复的单词,扣去这些重复的单词,这本书也就几千到一万多个单词而已,那么内存就足够存储他们了。比如数字集合 {1,2,5,7,9,1,5,9} 的基数集合为 {1,2,5,7,9} 那么基数(不重复元素)就是 5,基数的作用是评估大约要准备多少个存储单元去存储数据,但是基数算法一般存在一定的误差(一般是可控的)。
基数并不是存储元素,存储元素消耗内存空间比较大,而是给某一个有重复元素的数据集合(一般是很大的数据集合)评估需要的空间单元数,所以它没有办法进行存储。
命令    说明    备注
Pfadd key element    添加指定元素到 HyperLogLog 中    如果已经存储元素,则返回 0,添加失败
Pfcount key     返回 HyperLogLog 的基数值    
Pfmerge deskey key1 [key2 key3 ..]    合并多个 HyperLogLog,并将其保存在 desKey 中    
 
分析:首先往一个键为 h1 的 HyperLogLog 插入元素,让其计算基数,到了第 5 个命令“pfadd h1 a”的时候,由于之前已经添加过了,所以返回 0. 它的基数集合为 {a,b,c,d}, 故而求集合长度为 4;之后再添加了第二个基数,它的基数集合是 {a,z}, 所以在 h1 和 h2 合并为 h3 的时候,它的基数集合为 {a,b,c,d ,z}, 所以求取它的基数就是 5.


⦁    在 Spring Boot 中配置和使用 Redis
⦁    引入依赖

        <!-- 引入 redis 的依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <exclusions>
                <!-- 不依赖 Redis 的异步客户端,因为默认是依赖这个的,我想用的是 jedis 驱动 -->
                <exclusion>
                    <groupId>io.lettuce</groupId>
                    <artifactId>lettuce-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <!-- 引入 Redis 的客户端驱动 jedis -->

        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>
⦁    Redis 的一些常用技术
⦁    Redis 的事务
和其他大部分的 NoSQL 不同,Redis 是存在事务的,尽管他没有数据库那么强大,但是还是很有用的,尤其在那些需要高并发的网站中,使用 Redis 读写数据要比数据库快很多,如果使用 Redis 事务在某些场合下替代数据库事务,则可以保证数据的一致性的同时,大幅度的提高数据的读写的响应速度。
Redis 的事务是使用 MULTI-EXEC 的命令组合,使用它可以提供两个重要的保证:
⦁    事务是一个被隔离的操作,事务中的方法都会被 Redis 进行序列化并按顺序执行,事务在执行的过程中不会被其他的客户端发出的命令打断。
⦁    事务是一个原子性的操作,它要么全部执行,要么什么都不执行。
在 Redis 的连接中,请注意要求是一个连接,所以更多的时候使用 SessionCallback 接口(和 RedisCallback 接口一样,他们的作用是让 RedisTemplate 进行回调,通过他们能在同一个连接下执行多个命令)处理。在 Redis 中使用事务会经过 3 个过程:
⦁    开启事务
⦁    命令进入队列
⦁    执行事务
Redis 事务命令
      命令             说明             备注
multi    开启事务命令,之后命令就进入队列,而不会马上执行    在事务生存期间,所有的 Redis 关于数据结构的命令都会入队列
Watch key1 [key2 …]    监听某些键,当被监听的键在事务执行前,发生修改,则事务会被回滚    使用乐观锁
Unwatch key1 [key2…]    取消监听某些键    
exec    执行事务,如果被监听的键没被修改,则采用执行命令,否则就回滚命令    
discard    回滚事务    回滚进入队列的事务命令,之后就不能再使用 exec 命令提交了
           Redis 命令执行事务的过程
 
 
 
/**
 * 在 Spring 中使用 Redis 事务命令
 *
 * @author apple
 *
 */
public class RedisMain6 {

    public static void main(String[] args) {

        ApplicationContext ctx = new AnnotationConfigApplicationContext(RedisConfig.class);
        RedisTemplate redisTemplate = ctx.getBean(RedisTemplate.class);
        SessionCallback sessionCallback = new SessionCallback() {
            @Override
            public Object execute(RedisOperations ops) throws DataAccessException {
                // 开启事务
                ops.multi();
                // 获取字符串绑定键操作接口,这样我们就可以对某个键的数据进行多次操作了
                ops.boundValueOps("key1").set("value1");
                // 注意在事务执行过程中,命令只是进入队列,而没有被执行,所以此处采用 get 命令,而 value 却返回 null
                String value = (String) ops.boundValueOps("key1").get();
                System.out.println("在事务执行过程中,命令只是进入队列,而没有被执行,所以此处采用 get 命令,而 value 却返回 null::" + value);
                // 此时 list 会保存之前进入队列的所有命令的结果
                List list = ops.exec();// 执行事务
                value = (String) redisTemplate.opsForValue().get("key1");
                return value;
            }
        };

        // 执行 Redis 命令
        String vaString = (String) redisTemplate.execute(sessionCallback);
        System.out.println(vaString);

    }

}
⦁    探索 Redis 事务回滚
   1.Redis 事务遇到的命令格式正确而数据类型不符合引发的错误,不回滚
 
说明:从图可知,我们将 key1 设置为字符串,而是用命令 incr 对其自增,但命令只会进入事务队列,而没有被执行,所以不会发生任何错误,而是等待 exec 命令的执行。当 exec 命令执行后,之前进入队列的命令就依次执行,当遇到 incr 时,发生命令操作的数据类型错误,所以显示错误,而其之前的和之后的命令都会被正常执行。事务没有回滚。
     2. 对于命令格式错误
 
    事务发生回滚
⦁    使用 watch 命令监控事务
  在 Redis 中使用 watch 命令可以决定事务是执行还是回滚。一般而言,可以在 multi 命令之前使用 watch 命令来监控某些键值对,然后使用 multi 命令开启事务,执行各类对数据结构进行操作的命令,这个时候这些命令就会进入到队列。当 Redis 使用 exec 命令执行事务的时候,它首先去比对 watch 命令所监控的键值对,如果没有发生变化,那么它会执行事务队列中的命令,提交事务;如果发生变化,那么他不会执行事务中的命令,而去回滚。无论事务是否回滚,Redis 都会去取消执行事务前的 watch 命令。
    
    Redis 参考了多线程中使用的 CAS(比较与交换,Compare and Swap)去执行的。在数据高并发环境的操作中,我们把这样的一个机制称为乐观锁。这句话还是比较抽象的。所以先简要论述其操作过程,当一条线程去执行某些业务逻辑,但是这些业务操作的数据可能被其他线程共享了,这样会引发多线程中数据不一致的问题。为了克服这个问题,首先,在线程开始时读取这些共享数据,并将其保存在当前进程的副本中,称为旧值(old value),watch 命令就是这样的一个功能。然后,开启线程的业务逻辑,由 multi 命令提供这一功能。在执行更新前,比较当前线程副本中保存的旧值和当前线程共享的值是否一致,如果不一致,那么该数据已经被其他线程操作过了,此次更新失败。为了保存一致,线程就不去更新任何值,而将事务回滚;否则就认为他没有被其他线程操作过,执行对应的业务逻辑,exec 命令就是类似这样的功能。
注意:”类似”这个字眼,因为不完全是,原因 CAS 原理会产生 ABA 问题。
ABA 问题
时间顺序    线程 1    线程 2    说明
T1    X=A    ——    线程 1 加入监控 X
T2    复杂运算开始    修改 X=B    线程 2 修改 X,此刻为 B
T3         处理简单业务    ——
T4         修改 X=A    线程 2 修改 X, 此刻又变回 A

T5         结束线程 2    线程 2 结束
T6    检测到 X=A,验证通过,提交事务    ——    CAS 原理检测通过,因为和旧值保持一致
Redis 在执行事务的过程中,并不会阻塞其他连接的并发,而只是通过比较 watch 监控的键值去保证数据的一致性,所以 Redis 多个事务完全可以在非阻塞的多线程环境中并发执行,而不会产生 ABA 问题。
⦁    流水线(pipelined)
  使用队列批量执行一系列的命令,从而提高系统的性能,这就是 Redis 的流水线技术。

/**
 * 使用 Spring 操作 Redis 流水线
 *
 * @author apple
 *
 */
public class RedisMain7 {

    public static void main(String[] args) {

        ApplicationContext ctx = new AnnotationConfigApplicationContext(RedisConfig.class);
        RedisTemplate redisTemplate = ctx.getBean(RedisTemplate.class);
        SessionCallback sessionCallback = new SessionCallback() {

            @Override
            public Object execute(RedisOperations operations) throws DataAccessException {
                for (int i = 0; i < 100000; i++) {
                    int j = i + 1;
                    operations.boundValueOps("pipeline_key_" + j).set("pipeline_value_" + j);
                    operations.boundValueOps("pipeline_key_" + j).get();
                }
                return null;
            }
        };
        long start = System.currentTimeMillis();
        // 执行 Redis 的流水线命令
        List result = redisTemplate.executePipelined(sessionCallback);
        long end = System.currentTimeMillis();
        System.out.println("使用流水线用了:" + (end - start) + "毫秒");
        long start1 = System.currentTimeMillis();
        // 执行 Redis 的流水线命令
        List result2 = (List) redisTemplate.execute(sessionCallback);
        long end1 = System.currentTimeMillis();
        System.out.println("不使用流水线用了:" + (end1 - start1) + "毫秒");

    }

}
⦁    Redis 发布订阅
发布订阅是消息的一种常用模式。例如:在企业分配任务后,可以通过邮件、短信、微信通知到相关责任人,这就是一种典型的发布订阅模式。首先是 Redis 提供一个渠道,让消息能够发送到这个渠道上,而多个系统可以监听这个渠道,如微信、短信、邮件系统都可以监听这个渠道,当一条消息发送到渠道,渠道就会通知它的监听者,这样微信、短信、邮件系统就能够得到这个渠道给它们的消息,这些监听者会根据自己的需要去处理这个消息,于是我们就可以得到各种各样的的通知了。
 
⦁    Redis 消息监听器
   为了接收 Redis 渠道发来的消息,我们先定义一个消息监听器(MessageListener)

@Component
public class RedisMessageListener implements MessageListener {

    @Override
    public void onMessage(Message message, byte[] pattern) {
        // 消息体
        String body = new String(message.getBody());
        // 渠道名称
        String topic = new String(pattern);
        System.out.println(body);
        System.out.println(topic);

    }

}
这里的 onMessage 方法是得到消息后的处理方法,其中 message 参数代表 Redis 发送过来的消息,pattern 是渠道名称。
⦁    监听 Redis 发布的消息
package com.springboot.redis2;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.Topic;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;

@SpringBootApplication
public class SpringbootRedis2Application {

    @Autowired
    private RedisTemplate redisTemplate = null;
    // Redis 连接工厂
    @Autowired
    private RedisConnectionFactory connectionFactory = null;
    // Redis 消息监听器
    @Autowired
    private MessageListener reMessageListener = null;
    // 任务池
    @Autowired
    private ThreadPoolTaskScheduler taskScheduler = null;

    /**
     * 创建任务池,运行线程等待处理 Redis 的消息
     *
     * @return
     */
    @Bean
    public ThreadPoolTaskScheduler initTaskScheduler() {
        if (taskScheduler != null) {
            return taskScheduler;
        }
        taskScheduler = new ThreadPoolTaskScheduler();
        // 设置任务池大小为 20,这样它将可以运行线程,并进行阻塞,等待 Redis 消息的传入。
        taskScheduler.setPoolSize(20);
        return taskScheduler;
    }

    /**
     * 定义 Redis 的监听器
     *
     * @return 监听器
     */
    @Bean
    public RedisMessageListenerContainer initRedisContainer() {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        // Redis 连接工厂
        container.setConnectionFactory(connectionFactory);
        // 设置运行任务池
        container.setTaskExecutor(initTaskScheduler());
        // 定义监听渠道,名称为 topic1
        Topic topic = new ChannelTopic("topic1");
        // 使用监听器监听 Redis 的消息
        container.addMessageListener(reMessageListener, topic);
        return container;
    }

    public static void main(String[] args) {
        SpringApplication.run(SpringbootRedis2Application.class, args);
    }
}
⦁    测试
启动 Spring Boot 项目后,在 Redis 的客户端输入命令
publish topic1 msg
在 Spring 中,我们可以使用 RedisTemplate 来发送消息
redisTemplate.converAndSend(channel,message);