三、三种特殊类型
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
已给定的经纬度为中心,查找与中心距离不超过指定最大距离的所有位置元素
例子
# 以100,30这个经纬度为中心,查找在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.事务中的错误
使用事务可能会发生以上两种错误:
- 编译时错误:在执行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)
- 运行时错误:在执行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控制台结果:
命令查看结果: