(二)特殊数据类型、事务、Jedis

一、三种特殊数据类型

1.1、geospatial地理位置

Redis的Geo在3.2版本就推出了,这个功能可以推算地理位置的信息,两地之间的距离,方圆几里的人。
可用于朋友定位,附近的人,打车距离计算等。
GEO 的数据结构总共有六个常用命令:geoadd、geopos、geodist、georadius、georadiusbymember、geohash

  1. geoadd

    # 语法
    geoadd key longitude latitude member ...
    # 将给定的空间元素(纬度、经度、名字)添加到指定的键里面。
    # 这些数据会以有序集合的形式被储存在键里面,从而使得georadius和georadiusbymember这样的命令可以在之后通过位置查询取得这些元素。
    # geoadd命令以标准的x,y格式接受参数,所以用户必须先输入经度,然后再输入纬度。
    # geoadd能够记录的坐标是有限的:非常接近两极的区域无法被索引。
    

    测试:百度搜索经纬度查询,模拟真实数据

    127.0.0.1:6379> geoadd china:city 116.23 40.22 北京
    (integer) 1
    127.0.0.1:6379> geoadd china:city 121.48 31.40 上海 113.88 22.55 深圳 120.21
    30.20 杭州
    (integer) 3
    127.0.0.1:6379> geoadd china:city 106.54 29.40 重庆 108.93 34.23 西安 114.02
    30.58 武汉
    (integer) 3
    
  2. geopos

    # 语法
    geopos key member [member...]
    #从key里返回所有给定位置元素的位置(经度和纬度)
    
    127.0.0.1:6379> geopos china:city 北京
    1) 1) "116.23000055551528931"
    2) "40.2200010338739844"
    127.0.0.1:6379> geopos china:city 上海 重庆
    1) 1) "121.48000091314315796"
    2) "31.40000025319353938"
    2) 1) "106.54000014066696167"
    2) "29.39999880018641676"
    127.0.0.1:6379> geopos china:city 新疆
    1) (nil)
    
  3. geodist

    # 语法
    geodist key member1 member2 [unit]
    # 返回两个给定位置之间的距离,如果两个位置之间的其中一个不存在,那么命令返回空值。
    # 指定单位的参数unit必须是以下单位的其中一个:
    # m表示单位为米
    # km表示单位为千米
    # mi表示单位为英里
    # ft表示单位为英尺
    # 如果用户没有显式地指定单位参数,那么geodist默认使用米作为单位。
    
    127.0.0.1:6379> geodist china:city 北京 上海
    "1088785.4302"
    127.0.0.1:6379> geodist china:city 北京 上海 km
    "1088.7854"
    127.0.0.1:6379> geodist china:city 重庆 北京 km
    "1491.6716"
    
  4. georadius

    # 语法
    georadius key longitude latitude radius m|km|ft|mi [withcoord][withdist][withhash][asc|desc][count count]
    # 以给定的经纬度为中心, 找出某一半径内的元素
    

    测试:重新连接 redis-cli,增加参数 --raw ,可以强制输出中文,不然会乱码

    [root@kuangshen bin]# redis-cli --raw -p 6379
    # 在 china:city 中寻找坐标 100 30 半径为 1000km 的城市
    127.0.0.1:6379> georadius china:city 100 30 1000 km
    重庆
    西安
    # withdist 返回位置名称和中心距离
    127.0.0.1:6379> georadius china:city 100 30 1000 km withdist
    重庆
    635.2850
    西安
    963.3171
    # withcoord 返回位置名称和经纬度
    127.0.0.1:6379> georadius china:city 100 30 1000 km withcoord
    重庆
    106.54000014066696167
    29.39999880018641676
    西安
    108.92999857664108276
    34.23000121926852302
    # withdist withcoord 返回位置名称 距离 和经纬度 count 限定寻找个数
    127.0.0.1:6379> georadius china:city 100 30 1000 km withcoord withdist count
    1
    重庆
    635.2850
    106.54000014066696167
    29.39999880018641676
    127.0.0.1:6379> georadius china:city 100 30 1000 km withcoord withdist count
    2
    重庆
    635.2850
    106.54000014066696167
    29.39999880018641676
    西安
    963.3171
    108.92999857664108276
    34.23000121926852302
    
  5. georadiusbymember

    # 语法
    georadiusbymember key member radius m|km|ft|mi [withcoord][withdist][withhash][asc|desc][count count]
    # 找出位于指定范围内的元素,中心点是由给定的位置元素决定
    
    127.0.0.1:6379> GEORADIUSBYMEMBER china:city 北京 1000 km
    北京
    西安
    127.0.0.1:6379> GEORADIUSBYMEMBER china:city 上海 400 km
    杭州
    上海
    
  6. geohash

    # 语法
    geohash key member [member...]
    # Redis使用geohash将二维经纬度转换为一维字符串,字符串越长表示位置更精确,两个字符串越相似
    表示距离越近。
    
    127.0.0.1:6379> geohash china:city 北京 重庆
    wx4sucu47r0
    wm5z22h53v0
    127.0.0.1:6379> geohash china:city 北京 上海
    wx4sucu47r0
    wtw6sk5n300
    
  7. zrem
    GEO没有提供删除成员的命令,但是因为GEO的底层实现是zset,所以可以借用zrem命令实现对地理位置信息的删除

    127.0.0.1:6379> geoadd china:city 116.23 40.22 beijin
    1
    127.0.0.1:6379> zrange china:city 0 -1 # 查看全部的元素
    重庆
    西安
    深圳
    武汉
    杭州
    上海
    beijin
    北京
    127.0.0.1:6379> zrem china:city beijin # 移除元素
    1
    127.0.0.1:6379> zrem china:city 北京 # 移除元素
    1
    127.0.0.1:6379> zrange china:city 0 -1
    重庆
    西安
    深圳
    武汉
    杭州
    上海
    

1.2、HyperLogLog

基数:
A{1,3,5,7,8,9,7}
B{1,3,5,7,8}
A的基数集是B(即去重后的集合),基数(一个集合中不重复的元素个数)=5,可以接受误差

  1. 简介
    Redis2.8.9就更新了Hyperloglog数据结构
    Redis Hyperloglog是用来做基数统计的算法
    优点:占用的内存是固定的。如果要存内存角度来比较的话,首选Hyperloglog
    网页的UV(访问人数统计:一个人访问多次网站,但是还是算作一个人)
    传统的方式,set保存用户的id,然后就可以统计set中的元素数量作为标准判断。这个方式如果保存大量的用户id,就会比较麻烦。我们的目的是为了计数,而不是保存用户id。
    Hyperloglog有0.81%错误率,在统计UV任务中可以忽略不计
  2. 使用
    # 创建一组元素
    127.0.0.1:6379> PFADD mykey a b c d e f g h i j
    1
    # 查看set中的元素个数
    127.0.0.1:6379> PFCOUNT mykey
    10
    127.0.0.1:6379> PFADD mykey2 i j z x c v b n m
    1
    # 将mykey 和mykey2合并为mykey3 ,结果是一个并集
    127.0.0.1:6379> PFMERGE mykey3 mykey mykey2
    OK
    127.0.0.1:6379> PFCOUNT mykey3
    15
    

1.3、BitMap

在开发中,可能会遇到这种情况:需要统计用户的某些信息,如活跃或不活跃,登录或者不登录;又如需要记录用户一年的打卡情况,打卡了是1, 没有打卡是0,如果使用普通的 key/value存储,则要记录365条记录,如果用户量很大,需要的空间也会很大。
Redis提供了 Bitmap 位图这中数据结构,Bitmap 就是通过操作二进制位来进行记录,即为 0 和 1;如果要记录 365 天的打卡情况,使用 Bitmap表示的形式大概如下:0101000111000111…
BitMap 就是通过一个 bit 位来表示某个元素对应的值或者状态, 其中的 key 就是对应元素本身

  1. setbit(设置操作)

    SETBIT key offset value : 设置 key 的第 offset 位为value (1或0)
    # 使用 bitmap 来记录上述事例中一周的打卡记录如下所示:
    # 周一:1,周二:0,周三:0,周四:1,周五:1,周六:0,周天:0 (1 为打卡,0 为不打卡)
    127.0.0.1:6379> setbit sign 0 1
    0
    127.0.0.1:6379> setbit sign 1 0
    0
    127.0.0.1:6379> setbit sign 2 0
    0
    127.0.0.1:6379> setbit sign 3 1
    0
    127.0.0.1:6379> setbit sign 4 1
    0
    127.0.0.1:6379> setbit sign 5 0
    0
    127.0.0.1:6379> setbit sign 6 0
    0
    
  2. getbit(获取操作)
    GETBIT key offset 获取offset设置的值,未设置过默认返回0

    127.0.0.1:6379> getbit sign 3 # 查看周四是否打卡
    1
    127.0.0.1:6379> getbit sign 6 # 查看周七是否打卡
    0
    
  3. bitcount(统计操作)
    bitcount key [start, end] 统计 key 上位为1的个数

    # 统计这周打卡的记录,可以看到只有3天是打卡的状态:
    127.0.0.1:6379> bitcount sign
    3
    

二、事务

2.1、事务

Redis事务本质:
一组命令的集合。一个事物中的所有命令都会被序列化,在事务执行的过程中,会按照顺序执行。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。

Redis事务的特点:
一次性、顺序性、排他性

Redis事务没有隔离级别的概念:
批量操作在发送 EXEC 命令前被放入队列缓存,并不会被实际执行。

Redis不保证原子性:
Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行。

Redis事务的三个阶段:

  • 开始事务(multi)
  • 命令入队(其他命令)
  • 执行事务(exec)
  1. 正常执行事务

    127.0.0.1:6379> multi  # 开启事务
    OK
    # 命令入队
    127.0.0.1:6379> set k1 v1
    QUEUED
    127.0.0.1:6379> set k2 v2
    QUEUED
    127.0.0.1:6379> get k2
    QUEUED
    127.0.0.1:6379> set k3 v3
    QUEUED
    127.0.0.1:6379> exec  # 执行事务,每次执行完后事务销毁,再次使用需要重新开启
    1) OK
    2) OK
    3) "v2"
    4) OK
    
  2. 放弃事务

    DISCARD  # 取消事务,事务队列中的命令都不会被执行
    
  3. 异常
    若在事务队列中存在命令性错误(即编译型异常,代码有问题),事务中所有的命令都不会被执行。
    如果事务队列中存在语法性错误,(即运行时异常,如java中1/0异常0),那么执行命令的时候,其他命令是可以正常执行的,错误命令会抛出异常

2.2、监控

悲观锁:

  • 认为什么时候都会出问题,无论做什么都会加锁

乐观锁:

  • 认为什么时候都不会出现问题,所以不会加锁。更新数据的时候去判断一下,在此期间是否有人修改过数据
  • 获取version
  • 更新的时候比较version
127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> set out 0
OK
# unwatch 如果事务执行失败,就先解锁
127.0.0.1:6379> watch money  # 监视money对象
OK
127.0.0.1:6379> multi  # 事务正常结束,数据期间没有发生变动,这个时候就正常执行成功
OK
127.0.0.1:6379> decrby money 20
QUEUED
127.0.0.1:6379> INCRBY out 20
QUEUED
127.0.0.1:6379> exec
1) (integer) 80
2) (integer) 20

一但执行 EXEC 开启事务的执行后,无论事务使用执行成功, WARCH 对变量的监控都将被取消。故当事务执行失败后,需重新执行WATCH命令对变量进行监控,并开启新的事务进行操作。

总结:
watch指令类似于乐观锁,在事务提交时,如果watch监控的多个KEY中任何KEY的值已经被其他客户端更改,则使用EXEC执行事务时,事务队列将不会被执行,同时返回Nullmulti-bulk应答以通知调用者事务执行失败。

三、Jedis

Jedis是Redis官方推荐的Java连接开发工具。

3.1、测试连接

  1. 导入依赖

    <!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
    <dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.2.0</version>
    </dependency>
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.58</version>
    </dependency>
    
  2. 编码测试

    public class Ping {
    	public static void main(String[] args) {
    		Jedis jedis = new Jedis("127.0.0.1",6379);
    		System.out.println("连接成功");
    		//查看服务是否运行
    		System.out.println("服务正在运行: "+jedis.ping());
    	}
    }
    
  3. 运行结果

    连接成功
    服务正在运行: PONG
    
  4. Jedis的命令就是Redis的所有指令

3.2、事务

public static void main(String[] args) {
    Jedis jedis = new Jedis("127.0.0.1", 6379);
    JSONObject jsonObject = new JSONObject();
    jsonObject.put("hello", "world");
    jsonObject.put("name", "java");
    Transaction multi = jedis.multi();// 开启事务
    String result = jsonObject.toJSONString();
    jedis.watch(result); // 开启监控
    try {
        multi.set("user1", result);
        //再存入一条数据
		multi.set("json2", result);
		//这里引发了异常,用0作为被除数
		int i = 100/0;
		//如果没有引发异常,执行进入队列的命令
        multi.exec(); // 执行事务
    } catch (Exception e) {
        multi.discard(); // 放弃事务
        e.printStackTrace();
    }finally {
        System.out.println(jedis.get("user1"));
        jedis.close(); // 关闭连接
    }

}

四、SpringBoot整合

4.1、概述

SpringBoot操作数据层:所有的方式都封装在spring-data中
在SpringBoot中一般使用RedisTemplate提供的方法来操作Redis。
说明:在SpringBoot2.x之后,原来使用的jedis被换为了lettuce
jedis:采用的是直连,多个线程操作的话,是不安全的。如果想要避免不安全,使用jedis pool的连接池。更像BIO
lettuce:采用netty,实例可以在多个线程中进行共享,不存在线程不安全的情况。可以减少线程数量。更像NIO

4.2、整合测试

JedisPoolConfig(这个是配置连接池)
RedisConnectionFactory这个是配置连接信息,这里的RedisConnectionFactory是一个接口,我们需要使用它的实现类,在Spring Data Redis方案中提供了以下四种工厂模型:
JredisConnectionFactory
JedisConnectionFactory
LettuceConnectionFactory
SrpConnectionFactory

整合:

  1. 导入依赖

    <!-- 操作SpringRedis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
  2. yaml配置

    spring:
    	redis:
    		host: 127.0.0.1
    		port: 6379
    		password: 123456
    	jedis:
    		pool:
    			max-active: 8
    			max-wait: -1ms
    			max-idle: 500
    			min-idle: 0
    	lettuce:
    		shutdown-timeout: 0ms
    
  3. 测试

    class ApplicationTests {
    	@Autowired
    	private RedisTemplate redisTemplate;
    
    	@Test
    	void contextLoads() {
            // 在企业开发中,一般都不会使用原生的方式编写代码
            
    		// redisTemplate操作不同的数据类型,api和指令是一样的
    		// opsForValue:操作字符串,类似String类型
    		// opsForList:操作List
    		// opsForSet
    		// opsForZSet
    		// opsForHash
    		// opsForGeo
    		// opsForHyperLogLog
    
    		// 除了基本的操作,常用的方法都可以直接通过redisTemplate操作,比如事务和基本的CRUD
    
    		// 获取redis的连接对象
    		// RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
    		// connection.flushDb();
    		// connection.flushAll();
    		redisTemplate.opsForValue().set("mykey", "hello");
    		System.out.println(redisTemplate.opsForValue().get("mykey"));
    	}
    }
    

4.3、封装工具类

  1. 新建项目,导入redis的启动器

    <dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
  2. 配置redis

    # Redis服务器地址
    spring.redis.host=127.0.0.1
    # Redis服务器连接端口
    spring.redis.port=6379
    
  3. 分析 RedisAutoConfiguration 自动配置类

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(RedisOperations.class)
    @EnableConfigurationProperties(RedisProperties.class)
    @Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
    public class RedisAutoConfiguration {
    	@Bean
    	@ConditionalOnMissingBean(name = "redisTemplate") // 可以自己定义一个RedisTemplate来替换这个默认的
    	public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
    	    // 默认的RedisTemplate  没有过多的设置,redis对象都是需要序列化的
    	    // 两个泛型都是Object类型,后续使用需要强制转换
    	    RedisTemplate<Object, Object> template = new RedisTemplate();
    	    template.setConnectionFactory(redisConnectionFactory);
    	    return template;
    	}
    
    	@Bean
    	@ConditionalOnMissingBean  // 由于String类型是redis中最常使用的类型,所以单独提出来了一个bean
    	public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
    	    StringRedisTemplate template = new StringRedisTemplate();
    	    template.setConnectionFactory(redisConnectionFactory);
    	    return template;
    	}
    }
    

    通过源码可以看出,SpringBoot自动帮我们在容器中生成了一个RedisTemplate和一个StringRedisTemplate。
    但是,这个RedisTemplate的泛型是<Object,Object>,写代码不方便,需要写好多类型转换的代码;我们需要一个泛型为<String,Object>形式的RedisTemplate。
    并且,这个RedisTemplate没有设置数据存在Redis时,key及value的序列化方式。
    看到这个@ConditionalOnMissingBean注解,就知道如果Spring容器中有RedisTemplate对象,这个自动配置的RedisTemplate不会实例化。因此我们可以直接自己写个配置类,配置RedisTemplate。

  4. 自定义RedisTemplate

    @Configuration
    public class RedisConfig {
    
        // 自定义RedisTemplate
        @Bean
        // 告诉编译器忽略指定的警告,不用在编译完成后出现警告信息
        @SuppressWarnings("all")
        public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
            // 为了自己开发方便,一般直使用<String, Object>
            RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
            template.setConnectionFactory(factory);
    
            // Json序列化配置
            Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
            ObjectMapper om = new ObjectMapper();
            om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
            om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
            jackson2JsonRedisSerializer.setObjectMapper(om);
    
            // String的序列化
            StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
            // key采用String的序列化方式
            template.setKeySerializer(stringRedisSerializer);
            // hash的key也采用String的序列化方式
            template.setHashKeySerializer(stringRedisSerializer);
            // value序列化方式采用jackson
            template.setValueSerializer(jackson2JsonRedisSerializer);
            // hash的value序列化方式采用jackson
            template.setHashValueSerializer(jackson2JsonRedisSerializer);
            template.afterPropertiesSet();
            return template;
        }
    
    }
    
  5. 测试

    @Test
    public void test() throws JsonProcessingException {
        // 真实的开发一般都使用json来传递对象
        User user = new User("hello", 3);
        //String jsonUser = new ObjectMapper().writeValueAsString(user);
        redisTemplate.opsForValue().set("user", user);
        System.out.println(redisTemplate.opsForValue().get("user"));
    }
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值