Redis学习笔记(三)

三、三种特殊类型

1.geospatial 地理位置

Redis的Geo在Redis3.2版本时推出了。

① GEOADD key longitude latitude member [longitude latitude member …]

将指定的地理空间位置(纬度、经度、名称)添加到指定的key中

  • 有效的经度从-180度到180度。
  • 有效的纬度从-85.05112878度到85.05112878度。

超过上述范围,指令将会报错。

例子

# 添加地理位置
# 规则:两级无法直接添加,我们一般会下载城市数据,直接通过java程序一次性导入进去
127.0.0.1:6379> geoadd china:city 116.40 39.90 beijing
(integer) 1
127.0.0.1:6379> geoadd china:city 121.47 31.23 shanghai
(integer) 1
127.0.0.1:6379> geoadd china:city 106.50 29.53 chongqin
(integer) 1
127.0.0.1:6379> geoadd china:city 114.05 22.52 shenzhen
(integer) 1
127.0.0.1:6379> geoadd china:city 120.16 30.24 hangzhou 108.96 34.26 xian
(integer) 2

② GEOPOS key member [member …]

从key里返回所有给定位置元素的位置(经度和纬度)

例子

# 获取北京地理位置信息
127.0.0.1:6379> GEOPOS china:city beijing
1) 1) "116.39999896287918091"
   2) "39.90000009167092543"
# 获取上海和杭州地理位置信息
127.0.0.1:6379> GEOPOS china:city shanghai hangzhou
1) 1) "121.47000163793563843"
   2) "31.22999903975783553"
2) 1) "120.1600000262260437"
   2) "30.2400003229490224"

③ GEODIST key member1 member2 [unit]

1.返回两个给定位置之间的距离。
2.如果两个位置之间的其中一个不存在, 那么命令返回空值

unit参数可以用以下单位表示:

  • m 表示单位为米。
  • km 表示单位为千米。
  • mi 表示单位为英里。
  • ft 表示单位为英尺。

默认是以米为单位。

例子

# 上海与北京的距离
127.0.0.1:6379> GEODIST china:city shanghai beijing
"1067378.7564"

④ GEORADIUS key longitude latitude radius

已给定的经纬度为中心,查找与中心距离不超过指定最大距离的所有位置元素

例子

# 以10030这个经纬度为中心,查找在1000km以内的位置元素
127.0.0.1:6379> GEORADIUS china:city 100 30 1000 km
1) "chongqin"
2) "xian"

# withcoord:显示查找出来的元素的地理信息
127.0.0.1:6379> GEORADIUS china:city 120 30 1000 km withcoord
1) 1) "hangzhou"
   2) 1) "120.1600000262260437"
   
# withdist:显示位置元素到中心的距离
127.0.0.1:6379> GEORADIUS china:city 120 30 1000 km withdist
1) 1) "hangzhou"
   2) "30.8146"
2) 1) "shanghai"
   2) "196.2512"
   
# count<count> 设置count来决定返回的数量
127.0.0.1:6379> GEORADIUS china:city 120 30 1000 km withdist count 1
1) 1) "hangzhou"
   2) "30.8146"

# ASC: 根据中心的位置, 按照从近到远的方式返回位置元素。
# DESC: 根据中心的位置, 按照从远到近的方式返回位置元素。
127.0.0.1:6379> GEORADIUS china:city 120 30 1000 km withdist asc
1) 1) "hangzhou"
   2) "30.8146"
2) 1) "shanghai"
   2) "196.2512"
127.0.0.1:6379> GEORADIUS china:city 120 30 1000 km withdist desc
1) 1) "shanghai"
   2) "196.2512"
2) 1) "hangzhou"
   2) "30.8146"

⑤ GEORADIUSBYMEMBER key member radius

与GEORADIUS一样,但是中心点不再是通过输入经纬度决定,而是根据位置元素决定的。

例子

# 返回北京周围1000km的城市地理信息
127.0.0.1:6379> GEORADIUSBYMEMBER china:city beijing 1000 km withdist
1) 1) "beijing"
   2) "0.0000"
2) 1) "xian"
   2) "910.0565"

⑥ GEOHASH key member [member …]

该命令返回11个字符的Geohash字符串

例子

# 将二位的经纬度转换为一维的字符串,如果两个字符串越接近,则距离越近。
127.0.0.1:6379> GEOHASH china:city beijing shanghai
1) "wx4fbxxfke0"
2) "wtw3sj5zbj0"

GEO底层原理实际上就是Zset,我们可以使用Zset来操作它

# 在geo中没有移除功能,我们可以使用Zset来移除一个位置元素
127.0.0.1:6379> ZREM china:city chongqin
(integer) 1
127.0.0.1:6379> ZRANGE china:city 0 -1 withsocres
(error) ERR syntax error
127.0.0.1:6379> ZRANGE china:city 0 -1 withscores
 1) "xian"
 2) "4040115445396757"
 3) "shenzhen"
 4) "4046432193584628"
 5) "hangzhou"
 6) "4054133997236782"
 7) "shanghai"
 8) "4054803462927619"
 9) "beijing"
10) "4069885360207904"

使用场景:
可以用来查看朋友的定位、附件的人、打车距离等等。

2.Hyperloglog

简介

Redis2.8.9版本就更新了Hyperloglog数据结构
Redis Hyperloglog 是基数统计的算法!

基数简单点说就是在一个集合中不重复元素的个数

使用场景:
网站的用户访问量,用户多次访问同一网站,只当作一次。

传统方式:set保存用户的id,然后统计一下set中的元素数量。
但这种方式保存了大量的用户id,比较麻烦,而且我们的目的是统计数量,不是保存用户id。

Hyperloglog的优点:占用的内存是固定。放2的64次方个不同的元素,只需12KB内存。

使用指令

命令描述
PFADD将除了第一个参数以外的参数存储到以第一个参数为变量名的HyperLogLog结构中.
PFCOUNT统计Hyperlolog基数的数量,返回的可见集合基数并不是精确值, 而是一个带有 0.81% 标准错误(standard error)的近似值.
PFMERGE将多个 HyperLogLog 合并(merge)为一个 HyperLogLog

例子

# 创建一组元素mykey
127.0.0.1:6379> PFADD mykey a b c d f
(integer) 1
# 统计mykey的基数数量
127.0.0.1:6379> PFCOUNT mykey
(integer) 5
# Hyperloglog的数据结构不允许存在重复的元素,操作失败
127.0.0.1:6379> PFADD mykey a b c
(integer) 0
# 再创建一组元素key
127.0.0.1:6379> PFADD key x y z
(integer) 1
# 将key合并到mykey中去
127.0.0.1:6379> PFMERGE mykey key
OK
# 最后的数量是合并后的数量
127.0.0.1:6379> PFCOUNT mykey
(integer) 8

如果允许容错,那么一定可以使用Hyperloglog
如果不允许容错,就使用set或者自己的数据类型

3.Bitmaps

使用场景:
统计用户信息,活跃、不活跃,登录、为登录,打卡、未打卡。总之,两总状态的都可以使用bitmaps

Bitmaps位图,是一种数据结构,都是操作二进制来进行记录,就只有0和1两种状态。

使用bitmap来记录一周的打卡记录
0代表为打开,1代表打卡

127.0.0.1:6379> setbit sign 0 0
(integer) 1
127.0.0.1:6379> setbit sign 1 1
(integer) 0
127.0.0.1:6379> setbit sign 2 1
(integer) 0
127.0.0.1:6379> setbit sign 3 1
(integer) 0
127.0.0.1:6379> setbit sign 4 1
(integer) 0
127.0.0.1:6379> setbit sign 5 1
(integer) 0
127.0.0.1:6379> setbit sign 6 0
(integer) 0

查看周一是否打卡

127.0.0.1:6379> getbit sign 0
(integer) 0

统计打卡的天数

127.0.0.1:6379> bitcount sign
(integer) 5

四、Redis事务

Redis事务本质:一组命令的集合!可以一次执行多个命令,所有命令都会序列化,按顺序地串行化执行而不会被其它命令插入,不许插队。

一个队列中,一次性、顺序性、排他性的执行一系列命令

Redis单条命令保存原子性,但是事务不保证原子性。

1.redis的事务:

  • 开启事务(MULTI)
  • 命令入队()
  • 执行事务(EXEC)

2.正常执行一段事务!

MULTI 命令用于开启一个事务,它总是返回 OK 。 MULTI 执行之后, 客户端可以继续向服务器发送任意多条命令, 这些命令不会立即被执行, 而是被放到一个队列中, 当 EXEC命令被调用时, 所有队列中的命令才会被执行。

# 开启事务
127.0.0.1:6379> MULTI
OK
# 执行3个命令
127.0.0.1:6379(TX)> set key1 v1
QUEUED #提示命令进入队列
127.0.0.1:6379(TX)> set key2 v2
QUEUED
127.0.0.1:6379(TX)> get key1
QUEUED
# 执行事务,按照命令的执行顺序输出
127.0.0.1:6379(TX)> EXEC
1) OK
2) OK
3) "v1"

3.放弃事务

通过调用 DISCARD , 客户端可以清空事务队列, 并放弃执行事务。

# 开启事务
127.0.0.1:6379> MULTI
OK
# 执行一系列命令
127.0.0.1:6379(TX)> set name zhangsan
QUEUED
127.0.0.1:6379(TX)> set age 18
QUEUED
# 取消事务
127.0.0.1:6379(TX)> DISCARD
OK
# 事务被取消,事务队列中的命令没有被执行
127.0.0.1:6379> get name
(nil)

4.事务中的错误

使用事务可能会发生以上两种错误:

  1. 编译时错误:在执行EXEC之前失败。命令在入队时发生语法错误,比如参数格式不对,参数名称不对,这种情况将直接报错。就好比是java中语法不对,发生编译异常一样。

实例测试

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
# 这里少写了一个参数,发生了编译错误
127.0.0.1:6379(TX)> getset k1
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> exec
(error) EXECABORT Transaction discarded because of previous errors.
# k3 获取不到
127.0.0.1:6379> get k3
(nil)
# k1 获取不到
127.0.0.1:6379> get k1
(nil)
  1. 运行时错误:在执行EXEC之后失败。举个例子,事务的命令处理了错误类型的键,比如,使用incr去自增一个存储着字符串的键。好比是java在运行中发生各种异常

实例测试

127.0.0.1:6379> MULTI 
OK
127.0.0.1:6379(TX)> set k1 a
QUEUED
# k1存储的是字符串a,自增1会发生错误
127.0.0.1:6379(TX)> INCR k1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
# 虽然发生错误,但不影响后面的命令执行
127.0.0.1:6379(TX)> exec
1) OK
2) (error) ERR value is not an integer or out of range
3) OK
4) OK
# 仍可以获取k3的值
127.0.0.1:6379> get k3
"v3"

第一种情况,事务会被取消,事务所有的命令都不会执行,可以保证事务的原子性;
第二种情况,报错的命令返回错误,其他命令正常执行,不能保证事务的原子性。

5.监控(WATCH)

悲观锁:

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

乐观锁:

  • 很乐观,认为什么时候都不会出问题,所以不会上锁。更新数据的时候去判断一下,在此期间是否有人修改数据。
  • 获取version
  • 更新的时候比较version

使用Redis的WATCH命令实现乐观锁

为了测试多线程修改值,我们要创建两个连接

连接1:
这里我们先不执行exec命令
在这里插入图片描述
连接2:
在连接1执行exec命令之前,修改money的值
在这里插入图片描述
最后执行exec会返回一个nil
在这里插入图片描述

1.被 WATCH 的键会被监视,并会发觉这些键是否被改动过了。 如果有至少一个被监视的键在 EXEC 执行之前被修改了,那么整个事务都会被取消, EXEC 返回nil来表示事务已经失败。
2.当 EXEC 被调用时, 不管事务是否成功执行, 对所有键的监视都会被取消。
3.我们也可以使用UNWATCH命令手动取消目前对键的监控。

以下是exec执行前没有修改money的值而正常执行的流程

127.0.0.1:6379> get money
"1000"
127.0.0.1:6379> WATCH money
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> DECRBY money 100
QUEUED
127.0.0.1:6379(TX)> exec
1) (integer) 900

五、Jedis

Jedis是Redis官方推荐的java连接开发工具!是使用java操作Redis的中间件。

1.导入依赖

    <dependencies>
        <!--        导入jedis-->
        <!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.5.2</version>
        </dependency>

        <!--        导入fastjson-->
        <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.75</version>
        </dependency>

    </dependencies>

2.连接Redis数据库

因为我的Redis是在Linux上的,所以我们要修改以下配置文件才能使用java进行远程连接

2.1.首先要检查以下Linux的防火墙是否开放了6379端口
在这里插入图片描述
这里我的端口已经开放,如果没有开放,请使用以下命令开放端口
在这里插入图片描述
最后在重启防火墙,修改配置之后必须重启防火墙,不然不会生效。
在这里插入图片描述

2.2.查看配置文件redis.conf

redis默认只允许本地访问,要使redis可以远程访问可以修改redis.conf。

先注释掉bind 127.0.0.1 -::1,使所有的ip都可以访问redis,若想指定多个ip访问,可以修改bind后面的ip。

在这里插入图片描述
在这里插入图片描述
在修改protected-mode 为no
在这里插入图片描述
2.3.重启redis服务

# 查看redis进程的pid
[root@localhost xconfig]# ps -aux|grep redis
root      58203  0.1  0.5 162496  9920 ?        Ssl  16:04   0:02 redis-server *:6379
root      59396  0.0  0.0 112824   984 pts/0    R+   16:25   0:00 grep --color=auto redis
# 通过kill指令传入pid将redis-server进程杀死
[root@localhost xconfig]# kill -9 58203
[root@localhost xconfig]# cd ..
# 重启一下redis-server
[root@localhost bin]# redis-server xconfig/redis.conf

这样我们就可以通过java程序操作Redis了。

public class TestPing {

    public static void main(String[] args) {
        //new 一个Jedis对象即可
        Jedis jedis = new Jedis("192.168.242.132",6379);
        // 返回PING代表连接成功
        System.out.println(jedis.ping());
        //关机连接
        jedis.close();
    }
}

连接成功
在这里插入图片描述

3.使用Jedis开启事务

public class TestTX {

    public static void main(String[] args) {
        Jedis jedis = new Jedis("192.168.242.132",6379);
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("name","zhangsan");
        jsonObject.put("age","18");
        String s = jsonObject.toString();
        //开启事务
        Transaction multi = jedis.multi();
        try {
            multi.set("user1",s);
            multi.set("user2",s);
            //执行事务
            multi.exec();
        } catch (Exception e) {
            //放弃事务
            multi.discard();
            e.printStackTrace();
        }finally {
            System.out.println(jedis.get("user1"));
            System.out.println(jedis.get("user2"));
            //关闭连接
            jedis.close();
        }
    }
}

六、SpringBoot整合Redis

1.导入的依赖

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

说明:在SpringBoot2.x之后,原来使用的jedis被替换为了lettuce
jedis:采用的直连,多个线程操作的话,是不安全的,如果想要避免不安全,使用jedis pool连接池。BIO模式。
lettuce:采用netty,实例可以在多个线程中进行共享,不存在线程不安全的情况。可以减少线程数量,更像NIO模式。
在这里插入图片描述
Redis自动配置类的分析:

@Bean
	@ConditionalOnMissingBean(name = "redisTemplate")//在没有redisTemplate这个bean时才生效,这样我们可以自定义一个RedisTemple来替换。
	@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
	public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
	//默认的RedisTemplate没有过多的设置,redis对象都是需要序列化。(尤其是使用了netty这种异步通信,默认使用jdk序列化)
		RedisTemplate<Object, Object> template = new RedisTemplate<>();
		template.setConnectionFactory(redisConnectionFactory);
		return template;
	}

	@Bean
	@ConditionalOnMissingBean
	@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
	public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
		StringRedisTemplate template = new StringRedisTemplate();
		template.setConnectionFactory(redisConnectionFactory);
		return template;
	}

2.配置连接

spring:
  redis:
    host: 192.168.242.132	//虚拟机的ip
    port: 6379

3.测试

    @Test
    void contextLoads() {
        //redisTemplate 操作不同的数据类型,api与redis指令一样
        //opsForValue   操作字符串 类似String
        //opsForHash、opsForList、opsForSet、opsForGeo
        //opsForHyperLogLog、opsForZSet
        //除了数据的基本操作外,还可以直接使用redisTemplate,比如事务
//        redisTemplate.multi();
//        redisTemplate.discard();
//        redisTemplate.watch();
        //获取Redis的连接对象
//        RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
//        connection.flushDb();
//        connection.flushAll();
        redisTemplate.opsForValue().set("name","zhangsan");
        System.out.println(redisTemplate.opsForValue().get("name"));
    }

4.自定义RedisTemplate实现序列化

先准备一个User对象

@Component
@NoArgsConstructor
@AllArgsConstructor
@Data
public class User implements Serializable{

    private String name;
    private int age;
}

这里我们先不实现序列化接口,测试一下

 	@Autowired
    private RedisTemplate redisTemplate;
    
 	@Test
    void test() {
        //真实的开发一般都是使用json来传递对象
        User user = new User("张三", 3);
        redisTemplate.opsForValue().set("user", user);
        System.out.println(redisTemplate.opsForValue().get("user"));
    }

发生了序列化的异常
在这里插入图片描述
之后我们再让User对象实现序列化,然后登录Xshell使用指令查看。
这里我们可以看到key和value都是被转义过的。
在这里插入图片描述
接下来通过查看源码分析一下:
我们进入到RedisTemplate类中可以看到这四种序列化配置
在这里插入图片描述
可以看到它们默认都是被jdk的方式序列化,而使用jdk序列化的方式会将字符转义
在这里插入图片描述

修改Redis序列化的方法是setKeySerializer,它需要传入一个RedisSerializer对象
在这里插入图片描述
RedisSerializer是一个接口,它一共有以下几种实现方式,这里我们选择json的序列化方式
在这里插入图片描述
这是一个自定义RedisTemplate模板

@Configuration
public class RedisConfig {

    //编写自定义的RedisTemplate
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        //设置Redis连接工厂
        template.setConnectionFactory(redisConnectionFactory);
        //Json序列化配置
        //传入Object对象
        Jackson2JsonRedisSerializer<Object> jsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jsonRedisSerializer.setObjectMapper(objectMapper);
        //String 序列化
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        //key 采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        //hash的key 采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        //value 采用json的序列化方式
        template.setValueSerializer(jsonRedisSerializer);
        //hash的value 采用json的序列化方式
        template.setHashValueSerializer(jsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }
}

使用我们自定义的RedisTemplate设置序列化方式之后,我们再来测试以下。

idea控制台结果:
在这里插入图片描述
命令查看结果:
在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值