Redis(五)------事务、Jdies、SpringBoot集成Redis
8、事务
-
Redis的单条命令保证原子性,Redis事务不保证原子性
-
Redis事务本质:一组命令的集合
-
事务中每条命令都会被序列化,执行过程中按顺序执行,不允许其他命令进行干扰
-
Redis事务没有隔离级别的概念
-
事务特性:
- 一次性
- 顺序性
- 排他性
-
所有事务中的命令在加入时都没有被执行,直到提交时才会开始执行(exec)一次性完成
8.1 Redis事务操作过程
- 开启事务(multi)
- 命令入队
- 执行事务(exec)
- 取消执行(discard)
8.1.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 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"
8.1.2 取消事务
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)
8.1.3 事务错误(运行时错误,编译时错误)
- 代码语法错误(编译时异常)
# 编译时异常,是语法错误,所有命令都不生效
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"
8.2 监控 Watch
8.2.1 悲观锁
- 很悲观,认为什么时候都会出现问题,无论做什么都会加锁
8.2.2 乐观锁
- 很乐观,认为什么时候都不会出现问题,不会上锁
- 更新数据的时候判断一下,在此期间是否有人修改过这个数据
- 获取version,更新的时候比较version
8.2.3 Redis测试监控测试
- 正常执行成功
127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> set out 0
OK
# 监视 money 对象
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 out 20
QUEUED
127.0.0.1:6379> exec
1) (integer) 80
2) (integer) 20
- 测试多线程修改值,使用watch,可以当作Redis的乐观锁操作
# 第一个客户端 先不执行exec 因为此时watch后的值还是原来的值
127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> set out 0
# 监视 money
127.0.0.1:6379> watch money
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> DECRBY money 10
QUEUED
127.0.0.1:6379> INCRBY out 10
QUEUED
# 第二个客户端
127.0.0.1:6379> set money 80
OK
# 第二个客户端执行完后 我再执行exec 就报错了
# 执行前,另一个线程,修改了money的值,会导致事务执行失败
127.0.0.1:6379> exec
(nil)
- 如果修改失败,获得最新的值
# 如果发现事务执行失败,就先解锁
127.0.0.1:6379> unwatch
OK
# 获取最新的值,再次监视,select version
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 out 20
QUEUED
# 比对监视的值是否发生了变化,如果没有,执行成功,如果变量的版本变化了,执行失败
127.0.0.1:6379> exec
1) (integer) 60
2) (integer) 20
- 注:每次提交执行exec后都会自动释放锁,不管是否成功
9、Jedis
Jedis
是Redis官方推荐的java连接开发工具,使用java操作Redis的中间件- 如果要使用java操作Redis,一定要对Jedis非常熟悉!
9.1 导入对应的依赖
<!--导入Jedis的包-->
<dependencies>
<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.3.0</version>
</dependency>
<!--fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.73</version>
</dependency>
</dependencies>
9.2 编码测试
-
开启防火墙中6379端口:
firewall-cmd --zone=public --add-port=6379/tcp --permanent
,并且重启防火墙:systemctl restart firewalld.service
-
远程连接linux服务器,需要修改
redis.config
# 后台运行
daemonize yes
protected-mode no
# bind是默认运行端口
注释掉 bind 127.0.0.1
-
修改完成后,重启redis,
./redis-cli -h linuxIP -p 6379
,ping成功则连接成功 -
连接数据库,操作命令,断开连接
public class TestPing {
public static void main(String[] args) {
//第一步 new Jedis对象
Jedis jedis = new Jedis("linux服务器端口号",6379);
//Jedis所有的命令就是学习的Redis所有指令
//之前的指令就是方法
System.out.println(jedis.ping());
}
}
9.3 常用的API
- String
- List
- Set
- Hash
- Zset
9.4 Jedis事务
public class TestPing {
public static void main(String[] args) {
//第一步 new Jedis对象
Jedis jedis = new Jedis("101.35.2.137",6379);
//Jedis所有的命令就是学习的Redis所有指令
//之前的指令就是方法 jedis.set()
System.out.println(jedis.ping());
JSONObject jsonObject = new JSONObject();
jsonObject.put("hello", "world");
jsonObject.put("name", "zzz");
// 开启事务
Transaction multi = jedis.multi();
String result = jsonObject.toJSONString();
// jedis.watch(result)
try {
multi.set("user1", result);
multi.set("user2", result);
// 执行事务
multi.exec();
}catch (Exception e){
// 放弃事务
multi.discard();
} finally {
System.out.println(jedis.get("user1"));
System.out.println(jedis.get("user2"));
// 关闭连接
jedis.close();
}
}
}
10、SpringBoot整合Redis
10.1 简介
-
SpringBoot操作数据:Spring-data、jpa、jdbc、mongodb、redis
-
SpringBoot数据操作,都被封装在SpringData中。SpringData也是和SpringBoot齐名的项目
-
在SpringBoot2.0版本后,原来使用的
jedis
替换成了lettuce
-
Jedis:
采用的直连,多个线程操作是不安全的,如果想要避免不安全情况,使用jedis pool
连接池。更像BIO模式 -
Lettuce:
采用的netty,实例可以在多个线程中进行共享,不存在线程不安全的情况,可以减少线程数量,不需要开连接池。更像NIO模式
10.2 整合测试
-
步骤:新建项目,导入依赖,配置连接,测试
-
新建SpringBoot项目,勾选相关依赖
- Redis对应依赖,点击其中有lettuce的包,继续点击,其中有netty的包
<!--操作redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
RedisAutoConfiguration
源码分析
@Bean
// 我们可以自己定义一个redisTemplate来替换这个默认的
// 当redisTemplate不存在时 默认模板就生效
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
// 默认的 RedisTemplate 没有过多的设置,redis 对象都是需要序列化的
// 两个泛型都是 Object, Object 的类型,使用时需要强制转换
//<String, Object>
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
//由于String是redis中最常使用的类型,所以单独提出来了一 个bean
@ConditionalOnMissingBean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
- 配置文件
application.propertise
,可以配置什么属性,都写在了RedisProperties
类
# SpringBoot 所有的配置类,都有一个自动配置类
# 自动配置类会绑定一个properties配置文件
# RedisAutoConfiguration 绑定 RedisProperties
spring.redis.host=远程服务器IP
spring.redis.port=6379
- 测试
@SpringBootTest
class Redis02SpringbootApplicationTests {
//注入默认配置类
@Autowired
private RedisTemplate redisTemplate;
@Test
void contextLoads() {
// redisTemplate 操作不同的数据类型,api指令是一样的
// opsForValue 操作字符串 类似String
// opsForList 操作List 类似List
// opsForSet,opsForHash,opsForZSet,opsForGeo,opsForHyperLogLog
// 除了基本的操作,常用的方法都可以直接通过redisTemplate操作,比如事务,和基本的CRUD
// 获取redis的连接对象
// RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
// connection.flushDb(); 清除当前数据库
// connection.flushAll(); 清除全部数据库
//尽量不用中文 可能会被转义
redisTemplate.opsForValue().set("mykey","zzz");
System.out.println(redisTemplate.opsForValue().get("mykey"));
}
}
- 所有对象的保存都需要序列化!!!!!!!!
10.3 编写RedisTemplate
- 默认RedisTemplate中,关于值的序列化配置
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer keySerializer = null;
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer valueSerializer = null;
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer hashKeySerializer = null;
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer hashValueSerializer = null;
- 默认RedisTemplate中,默认的序列化方式,是JDK序列化,会使字符串转义,可以使用josn进行序列化
if (defaultSerializer == null) {
defaultSerializer = new JdkSerializationRedisSerializer(
classLoader != null ? classLoader : this.getClass().getClassLoader());
}
- 编写自己的
RedisTemplate
@Configuration
public class RedisConfig {
//编写自己的RedisTemplate,在AutoConfiguration中复制默认的函数,进行改写
// 这是一个固定模板,大家在企业中,拿去就可以直接使用
// 自己定义了一个 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;
}
}
- 编写RedisUtil
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
// 在我们真实的分发中,或者在公司,一般都可以看到一个公司自己封装RedisUtil
@Component
public final class RedisUtil {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// =============================common============================
/**
* 指定缓存失效时间
* @param key 键
* @param time 时间(秒)
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}
// ============================String=============================
/**
* 普通缓存获取
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
* @param key 键
* @param delta 要增加几(大于0)
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
* @param key 键
* @param delta 要减少几(小于0)
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
// ================================Map=================================
/**
* HashGet
* @param key 键 不能为null
* @param item 项 不能为null
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
* @param key 键
* @param map 对应多个键值
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并设置时间
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除hash表中的值
*
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
*
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
*
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
// ============================set=============================
/**
* 根据key获取Set中的所有值
* @param key 键
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据value从一个set中查询,是否存在
*
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入set缓存
*
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 将set数据放入缓存
*
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0)
expire(key, time);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取set缓存的长度
*
* @param key 键
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除值为value的
*
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
// ===============================list=================================
/**
* 获取list缓存的内容
*
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
*
* @param key 键
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引 获取list中的值
*
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @param time 时间(秒)
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0)
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0)
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据索引修改list中的某条数据
*
* @param key 键
* @param index 索引
* @param value 值
* @return
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除N个值为value
*
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
}