参考牛客网高级项目教程
狂神说Redis教程笔记
1.SpringBoot整合redis基本配置
1. 使用java操作redis基础
Jedis
-
**使用java操作redis的一个中间件,Redis 官方推荐的 java连接开发工具 ** -
Jedis是Redis官方推出的一款面向Java的客户端,提供了很多接口供Java语言调用。 -
使用jedis对象,操作方法函数与redis的api完全一致 public class TestPing {
public static void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1", 6379);
String response = jedis.ping();
System.out.println(response);
}
}
Spring Data redis
SpringData
SpringData 也是和 SpringBoot 齐名的项目!
SpringBoot 操作数据全部封装在spring-data这个接口中
lettuce
-
在 SpringBoot2.x 之后,原来使用的jedis 被替换为了 lettuce? -
jedis : 采用的直连,多个线程操作的话,是不安全的,
- 如果想要避免不安全的,使用 jedis pool 连接池! 更像 BIO(阻塞) 模式
-
lettuce : 采用netty,实例可以再多个线程中进行共享,不存在线程不安全的情况!
SpringDataRedis简介
- Spring-data-redis是spring大家族的一部分,提供了在srping应用中通过简单的配置访问redis服务,
- 对reids底层开发包(Jedis, JRedis, and RJC)进行了高度封装,
- RedisTemplate提供了redis各种操作、异常处理及序列化,支持发布订阅,并对spring 3.1 cache进行了实现。
2.导入依赖
- 版本号父类依赖中有测试好兼容性比较好的版本,不写,默认使用父类的中指明的版本
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
3. 源码分析和配置连接
源码分析
RedisAutoConfiguration
现在我们回到RedisAutoConfiguratio
-
只有两个简单的Bean
- RedisTemplate
- StringRedisTemplate
-
当看到xxTemplate时可以对比RestTemplat、SqlSessionTemplate,通过使用这些Template来间接操作组件。那么这俩也不会例外。分别用于操作Redis和Redis中的String数据类型。 -
在RedisTemplate上也有一个条件注解@ConditionalOnMissingBean,说明我们是可以对其进行定制化的 -
编写配置文件然后连接Redis,就需要阅读RedisProperties
RedisProperties
这是一些基本的配置属性。
还有一些连接池相关的配置。注意使用时一定使用Lettuce的连接池。
自定义连接配置
- 本项目中,使用空白的数据库11
- 注意spring.redis.host在linux系统下不能写localhost
#redis相关配置
spring.redis.database=11
spring.redis.host=虚拟机ip地址
spring.redis.port=6379
直接使用RedisTemplate测试
-
编写配置文件 一定是虚拟机的ip,不是localhost或127.0.0.1 #redis相关配置
spring.redis.database=11
spring.redis.host=192.168.***.***
spring.redis.port=6379
-
使用RedisTemplate @SpringBootTest
class Redis02SpringbootApplicationTests {
@Autowired
private RedisTemplate redisTemplate;
@Test
void contextLoads() {
redisTemplate.opsForValue().set("mykey","kuangshen");
System.out.println(redisTemplate.opsForValue().get("mykey"));
}
}
-
测试结果 此时我们回到Redis查看数据时候,惊奇发现全是乱码,可是程序中可以正常输出: 这时候就关系到存储对象的序列化问题,在网络中传输的对象也是一样需要序列化,否者就全是乱码。 因此需要自定义RedisTemplate和自定义序列化方式
4. 自定义RedisTemplate
源码分析-为何需要自定义RedisTemplate
@ConditionalOnMissingBean
- 使用**@ConditionalOnMissingBean,没有自定义value值名称的bean时,才会注入当前类**
- 即表明只要自定义name的值的类,就会注入自定义的类,
- RedisTemplate是用来访问redis数据的模板类
- Spring自带的key是Object类,使用范围更广,但对于redis的key一般都是String,使用不方便
源码默认jdk序列化
问题:
源码中的RedisTemplate默认使用的是jdk序列化,
原因
在最开始就能看到几个关于序列化的参数,默认都是null,即不指明序列化器。
在启动配置的函数afterPropertiesSet中,会新建默认的jdk序列化器,
后续我们定制RedisTemplate就可以对其进行修改
自定义RedisTemplate
配置类的创建
- 参照源码,将Object类型改成String类型,并创建实例
- 注入连接工厂redisConnectionFactory,并将连接工厂传给实例
- 最后返回这个bean,即将这个bean注入到SpringIOC中
@Configuration
public class RedisConfig {
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
RedisTemplate<String, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
序列化的设置与启用
RedisSerializer
- 需要改用JSON或者String类型的序列化
- RedisSerializer提供了多种序列化方案:
- 对于key,使用框架中自带的json序列化
- 对于value,使用框架中自带的String序列化
setKeySerializer
RedisTemplate调用set序列化的方法
afterPropertiesSet
@Configuration
public class RedisConfig {
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
RedisTemplate<String, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
template.setKeySerializer(RedisSerializer.string());
template.setValueSerializer(RedisSerializer.json());
template.setHashKeySerializer(RedisSerializer.string());
template.setHashValueSerializer(RedisSerializer.json());
template.afterPropertiesSet();
return template;
}
}
扩展:自定义Redis工具类
SpringBoot整合Redis及Redis工具类撰写
java redisUtils工具类很全
2. 使用自定义的RedisTemplate操作redis
解决不能连接redis的问题
spring boot连接linux服务器上的redis
1.opsForValue()-String
- 使用opsForValue()访问String类型数据
@Test
public void testString() {
String redisKey = "test:count";
redisTemplate.opsForValue().set(redisKey, 1);
System.out.println(redisTemplate.opsForValue().get(redisKey));
System.out.println(redisTemplate.opsForValue().increment(redisKey));
System.out.println(redisTemplate.opsForValue().decrement(redisKey));
}
2.opsForHash()-哈希表
@Test
public void testHashes() {
String redisKey = "test:user";
redisTemplate.opsForHash().put(redisKey, "id", 1);
redisTemplate.opsForHash().put(redisKey, "username", "zhangSan");
System.out.println(redisTemplate.opsForHash().get(redisKey, "id"));
System.out.println(redisTemplate.opsForHash().get(redisKey, "username"));
}
3.opsForList()-list数据
@Test
public void testLists() {
String redisKey = "test:ids";
redisTemplate.opsForList().leftPush(redisKey, 101);
redisTemplate.opsForList().leftPush(redisKey, 102);
redisTemplate.opsForList().leftPush(redisKey, 103);
System.out.println(redisTemplate.opsForList().size(redisKey));
System.out.println(redisTemplate.opsForList().index(redisKey, 0));
System.out.println(redisTemplate.opsForList().index(redisKey, 2));
System.out.println(redisTemplate.opsForList().range(redisKey, 0, 2));
System.out.println(redisTemplate.opsForList().leftPop(redisKey));
System.out.println(redisTemplate.opsForList().leftPop(redisKey));
System.out.println(redisTemplate.opsForList().leftPop(redisKey));
}
4.opsForSet()-set数据
@Test
public void testSets() {
String redisKey = "test:teachers";
redisTemplate.opsForSet().add(redisKey, "刘备", "关羽", "张飞", "赵云", "诸葛亮");
System.out.println(redisTemplate.opsForSet().size(redisKey));
System.out.println(redisTemplate.opsForSet().pop(redisKey));
System.out.println(redisTemplate.opsForSet().members(redisKey));
}
5
诸葛亮
[张飞, 刘备, 赵云, 关羽]
5.opsForZSet()-有序set
@Test
public void testSortedSets() {
String redisKey = "test:students";
redisTemplate.opsForZSet().add(redisKey, "唐僧", 80);
redisTemplate.opsForZSet().add(redisKey, "孙悟空", 90);
redisTemplate.opsForZSet().add(redisKey, "猪八戒", 70);
redisTemplate.opsForZSet().add(redisKey, "沙僧", 60);
redisTemplate.opsForZSet().add(redisKey, "白龙马", 50);
System.out.println(redisTemplate.opsForZSet().zCard(redisKey));
System.out.println(redisTemplate.opsForZSet().score(redisKey, "猪八戒"));
System.out.println(redisTemplate.opsForZSet().reverseRank(redisKey, "猪八戒"));
System.out.println(redisTemplate.opsForZSet().reverseRange(redisKey, 0, 2));
}
5
70.0
2
[孙悟空, 唐僧, 猪八戒]
6.测试全局数据
@Test
public void testKeys() {
redisTemplate.delete("test:user");
System.out.println(redisTemplate.hasKey("test:user"));
redisTemplate.expire("test:students", 10, TimeUnit.SECONDS);
}
7.多次访问一个key,可以进行绑定,简化代码
- 调用绑定的函数接口,可以将一种数据类型的key绑定,这样,所有操作都是基于这个key的
- BoundValueOperations
- BoundHashOperations…
@Test
public void testBoundOperations() {
String redisKey = "test:count";
BoundValueOperations operations = redisTemplate.boundValueOps(redisKey);
operations.increment();
System.out.println(operations.get());
redisKey = "test:user";
BoundHashOperations hashOperations = redisTemplate.boundHashOps(redisKey);
hashOperations.put("username", "zhang");
System.out.println(hashOperations.get("username"));
}
测试中常见错误总结
spring boot连接linux服务器上的redis
SpringBoot连接不上linux虚拟机启动的redis服务,一般是以下几个坑:
-
linux设置了防火墙,阻止了外在客户端的访问
-
解决:最简单直接的方法就是把linux的防火墙关了,(本人学习的虚拟机上可以这样) service iptables stop
或者也可以试试把6379端口暴露出来。。 firewall-cmd --zone=public --add-port=6379/tcp --permanent
然后重启一下防火墙 systemctl restart firewalld
-
redis配置文件中,
- 要将保护模式去掉,否则在没有设置密码的情况下依旧会阻止外在客户端访问redis服务
-
要注释掉127.0.0.1的限制 -
虚拟机采用NAT模式,查看下面两个勾是否勾中,子网地址必须是你上面那个ip的同段,比如虚拟机ip地址为192.168.59.128,那么这里的子网地址必须是192.168.59.*。 如果不是,网络中心-找到VMware Virtual Ethernet Adapter for VMnet8右键属性,找到Ipv4属性修改。 -
连接时,要使用虚拟机的ip,而不是localhost或者127.0.0.1
3.事务处理
Redis的单条命令是保证原子性的,但是redis事务不能保证原子性
- Redis事务没有隔离级别的概念
- redis启动事务后,不立即执行命令,而是将命令先后放入队列中,提交时,再一并执行
- 因此,若存在运行期错误,只是当前命令不执行,事务中其他的命令依旧执行,不能保证原子性
- 还要注意,在事务中查询,提交前不会有结果,故,要在提交后再查询
Redis事务本质:一组命令的集合。
----------------- 队列 set set set 执行 -------------------
事务中每条命令都会被序列化,执行过程中按顺序执行,不允许其他命令进行干扰。
Redis事务操作过程
multi-开启事务
命令入队
exec-执行事务
所以事务中的命令在加入时都没有被执行,直到提交时才会开始执行(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 k1
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> keys *
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK
3) "v1"
4) OK
5) 1) "k3"
2) "k2"
3) "k1"
discard-取消事务
- 事务一旦取消,就结束了事务,在事务中执行的命令均不会提交,即均不会执行
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> DISCARD
OK
127.0.0.1:6379> EXEC
(error) ERR EXEC without MULTI
127.0.0.1:6379> get k1
(nil)
事务中出现错误的处理
代码语法错误(编译时异常)
- 代码语法错误(编译时异常)所有的命令都不执行,相当于回滚
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> error k1
(error) ERR unknown command `error`, with args beginning with: `k1`,
127.0.0.1:6379> get k2
QUEUED
127.0.0.1:6379> EXEC
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k1
(nil)
代码逻辑错误 (运行时异常)
- **其他命令可以正常执行 ** >>> 所以不保证事务原子性
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> INCR k1
QUEUED
127.0.0.1:6379> get k2
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK
3) (error) ERR value is not an integer or out of range
4) "v2"
监控
锁的思想
悲观锁:
- 很悲观,认为什么时候都会出现问题,无论做什么都会加锁
乐观锁:
- 很乐观,认为什么时候都不会出现问题,所以不会上锁!更新数据的时候去判断一下,在此期间是否有人修改过这个数据
- 获取version
- 更新的时候比较version
watch key
使用watch key 监控指定数据,相当于乐观锁加锁。
正常执行
127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> set use 0
OK
127.0.0.1:6379> watch money
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> DECRBY money 20
QUEUED
127.0.0.1:6379> INCRBY use 20
QUEUED
127.0.0.1:6379> exec
1) (integer) 80
2) (integer) 20
测试多线程修改值,使用watch可以当做redis的乐观锁操作(相当于getversion)
我们启动另外一个客户端模拟插队线程。
线程1:
127.0.0.1:6379> watch money
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> DECRBY money 20
QUEUED
127.0.0.1:6379> INCRBY use 20
QUEUED
127.0.0.1:6379>
模拟线程插队,线程2:
127.0.0.1:6379> INCRBY money 500
(integer) 600
12
回到线程1,执行事务:
127.0.0.1:6379> EXEC
(nil)
127.0.0.1:6379> get money
"600"
127.0.0.1:6379> get use
"0"
解锁获取最新值,然后再加锁进行事务。
unwatch 进行解锁。
注意:每次提交执行exec后都会自动释放锁,不管是否成功
Spring编程式事务管理redis事务
- 由于redis只对局部一些命令执行事务,因此使用编程式事务比较合适
RedisOperations
- RedisOperations会代替redisTemplate去执行redis的访问
@Test
public void testTransaction() {
Object obj = redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations redisOperations) throws DataAccessException {
String redisKey = "test:tx";
redisOperations.multi();
redisOperations.opsForSet().add(redisKey, "zhangSan");
redisOperations.opsForSet().add(redisKey, "liShi");
redisOperations.opsForSet().add(redisKey, "wanWu");
System.out.println(redisOperations.opsForSet().members(redisKey));
return redisOperations.exec();
}
});
System.out.println(redisTemplate.opsForSet().members("test:tx"));
System.out.println(obj);
}
[]
[wanWu, liShi, zhangSan]
[1, 1, 1, [wanWu, liShi, zhangSan]]
|