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

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

 
把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);

posted @ 2022-08-11 18:50  码海兴辰  阅读(99)  评论(0编辑  收藏  举报