文章目录
一、三种特殊数据类型
1.1、geospatial地理位置
Redis的Geo在3.2版本就推出了,这个功能可以推算地理位置的信息,两地之间的距离,方圆几里的人。
可用于朋友定位,附近的人,打车距离计算等。
GEO 的数据结构总共有六个常用命令:geoadd、geopos、geodist、georadius、georadiusbymember、geohash
-
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
-
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)
-
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"
-
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
-
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 杭州 上海
-
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
-
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,可以接受误差
- 简介
Redis2.8.9就更新了Hyperloglog数据结构
Redis Hyperloglog是用来做基数统计的算法
优点:占用的内存是固定的。如果要存内存角度来比较的话,首选Hyperloglog
网页的UV(访问人数统计:一个人访问多次网站,但是还是算作一个人)
传统的方式,set保存用户的id,然后就可以统计set中的元素数量作为标准判断。这个方式如果保存大量的用户id,就会比较麻烦。我们的目的是为了计数,而不是保存用户id。
Hyperloglog有0.81%错误率,在统计UV任务中可以忽略不计 - 使用
# 创建一组元素 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 就是对应元素本身
-
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
-
getbit(获取操作)
GETBIT key offset 获取offset设置的值,未设置过默认返回0127.0.0.1:6379> getbit sign 3 # 查看周四是否打卡 1 127.0.0.1:6379> getbit sign 6 # 查看周七是否打卡 0
-
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)
-
正常执行事务
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
-
放弃事务
DISCARD # 取消事务,事务队列中的命令都不会被执行
-
异常
若在事务队列中存在命令性错误(即编译型异常,代码有问题),事务中所有的命令都不会被执行。
如果事务队列中存在语法性错误,(即运行时异常,如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、测试连接
-
导入依赖
<!-- 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>
-
编码测试
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()); } }
-
运行结果
连接成功 服务正在运行: PONG
-
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
整合:
-
导入依赖
<!-- 操作SpringRedis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
-
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
-
测试
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、封装工具类
-
新建项目,导入redis的启动器
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
-
配置redis
# Redis服务器地址 spring.redis.host=127.0.0.1 # Redis服务器连接端口 spring.redis.port=6379
-
分析 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。 -
自定义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; } }
-
测试
@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")); }