Spring boot 集成redis、redislock
一、redis
1. redis介绍
- 定义:redis是用C语言开发的开源高性能基于内存运行的键值对NoSql数据库;
- 特点:
- 在6之前是单线程,之后便是多线程
- 高效性:因为基于内存,读取速度是110000次/s,写的速度是81000次/s;
- 原子性:redis所有操作都是原子性。支持对几个操作合并后的原子性操作
- 数据类型丰富
- 稳定性:持久化、主从复制(集群)
- ttl(过期时间),事务,消息订阅;
- 支持的数据类型:
string
:- redis最基本的类型,一个key对应一个value;
- value可以是任何数据,如:图片、序列化对象
- value最大512M
list
:- 一个key有多个value
- 按照插入顺序排序,可以添加元素到列表的头部或尾部
- 底层是双向链表,通过索引下标的操作中间的节点性能会较差
set
- 相比list,set可以自动去重,set提供了判断某个成员是否在一个set集合内的重要接口
- set是String的无序集合,底层其实是一个value为null的hash表。添加、删除、查找的复杂度都是O(1)
hash
:- 键值对集合
- 是string类型的field和value的映射表,hash特别适合用于存储对象
zset(sorted set)
:- 不重复
- 每个成员都关联了一个评分(score),这个评分被用来按照最低分到最高分的方式排序集合中的成员。集合成员唯一、评分不重复;
- 可以根据评分或次序来获取一个范围的元素;
- 应用场景:
- 热点数据缓存,降低数据库io;
- 分布式架构,session共享
- 最新数据:通过list实现按自然时间排序的数据
- 排行榜:利用zset(有序集合)
- 时效性数据:如手机验证码,Expire过期
- 计数器,秒杀:原子性、自增方法incr、decr
- 去除大量数据中的重复数据:利用set集合
- 构建队列:list集合
- 发布订阅消息系统:pub/sub
- 不适用:
- 需要事务支持
- 基于sql的结构化查询存储,处理复杂的关系,需要用户自定义条件的查询
2. redis常用命令
- string
get key
:获取key的值set key v
:设置key的值del key
:删除key(应用于所有类型)incr key
:将储存的值加上1decr key
:将储存的值减去1incrby key amout
:加上整数amountdecrby key amout
:减去整数amountincrbybyfloat key amout
:加上浮点数amount字符串二进制append key v
:将值追加到key当前储存值的末尾getrange key start end
:获取下标start到end的字符串setrange key offset v
:将字符串看做二进制位串,并将位串中偏移量为offset的二进制位的值getbit key offset
:将字符串看做是二进制位串值为1的二进制位的数量,如果给定了可选的start偏移量和end偏移量,那么只对偏移量指定范围的二进制位进行统计;bitop operation dest-key key-name [key-name …]
:对一个或多个二进制位串进行 并and,或 or,异或XOR,非NOT 在内的任意一种安位运算符操作(bitwise operation),并将计算的结果放到dest -key里面
- list
rpush key [v…]
:将一个或多个加入列表右端lpush key [v…]
:将一个或多个加入列表左端rpop key
:移除并返回最右端的元素lpop key
:移除并返回列表最左端的元素lindex key size
:返回下标(偏移量)为size的元素lrange key start end
:返回从start 到end的元素 包含start和endltrim key start end
:只保留从start 到end的元素 包含start和end
- hash
hmget hkey key
:获取多个值hmset hkey key v
:为多个key设置值hdel hkey key
:删除多个值并返回hlen hkey
返回总数量hexists hkey key
:检查key是否存在在散列中hkeys hkey
:获取散列中所有keyhvals hkey
:获取三列中所有值hgetall hkey
:获取散列hincrby hkey key increment
:为key的值上加上整数incrementhincrbyfloat hkey key increment
:为key的值上加上浮点数increment
- set
sadd key item
:添加多个,返回新添加的个数(已存在的不算)- `srem key item:从集合移除多个元素 ,返回被移除元素的数量
sismember key item
:检查元素item是否在集合中scard key
:返回集合总数smembers key
:返回所有元素srandmember key cout
:随机返回cout个元素 cout为正整数 随机元素不重复 相反可能会出现重复spop key
:随机的移除一个元素 并返回已删除的元素smove key1 key2 item
:如果key1中包含item 移除key1中的item 添加到key2中,成功返回1 失败返回`0- `差运算 sdiffstore newkey key key1:将存在于key集合但是不存在key1…集合的其他元素 放
- `newkey里面(咬掉一口剩下的)
交运算 sinter key
:返回所有集合的交集(返回我们都有的的)交运算 sinterstore newkey key
:返回多个集合的交集生成集合newkey并运算 sunion key
:(返回我们不重复的所有元素 )并运算 sunion newkey key
:结果放到new key中
- zset
zadd key score member
:添加多个zerm key memer
:移除多个zcard key
:返回所有成员zincrby key incremnet member
:将member成员的分值加上incrementzcount key min max
:返回分值在 min和max中间的排名zrank key member
: 返回成员member在集合中的排名zscore key member
: 返回member的分值zrange key start stop
:返回 介于两者之间的成员
3. spring boot集成redis
- pom
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
-
配置属性
spring: # redis配置 redis: host: 192.168.5.128 port: 6379 password: 123456 timeout: 1000 jedis: pool: min-idle: 5 # 控制一个连接池里最小空闲jedis实例,默认值8 max-active: 10 # 最大连接实例数,默认8 max-idle: 10 # 控制一个连接池里最大空闲jedis实例,默认值8 max-wait: 2000 # 等待可用连接时间单位为毫秒,默认为-1表示永不超时,一旦超过等待时间则直接抛出
-
编码
-
启动类:无需多余注解
-
测试类:
// 获取一个String类型的key System.out.println("获取一个String类型的key:" + stringRedisTemplate.opsForValue().get("name")); // 获取一个key值 System.out.println("获取一个key值:" + redisTemplate.opsForValue().get("name")); // 判断某个key是否存在 Boolean a = redisTemplate.hasKey("a"); System.out.println("判断某个key是否存在:" + a); // 删除key if (a){ redisTemplate.delete("a"); System.out.println("删除了key为a"); } // 指定key的失效时间:key,时间,单位 redisTemplate.expire("name", 1000, TimeUnit.MILLISECONDS); // 获取某个key的过期时间 System.out.println("获取某个key的过期时间:" + redisTemplate.getExpire("name"));
-
3. 异常
- 出现问题的代码环境:
//redis用的jdk默认的序列化,这样存进去会出现乱码
redisTemplate.opsForValue().set("user","admin");
- 进入容器查看key
127.0.0.1:6379> keys *
1) "name"
2) "a"
3) "\xac\xed\x00\x05t\x00\x04user"
4) "age"
-
原因:
- spring-data-redis的RedisTemplate<K, V>在操作redis时默认使用JdkSerializationRedisSerializer来进行序列化
-
解决:
- 方案一:更改序列化方式(不推荐)
@Autowired(required = false) public void setRedisTemplate(RedisTemplate redisTemplate) { RedisSerializer stringSerializer = new StringRedisSerializer(); redisTemplate.setKeySerializer(stringSerializer); redisTemplate.setValueSerializer(stringSerializer); redisTemplate.setHashKeySerializer(stringSerializer); redisTemplate.setHashValueSerializer(stringSerializer); this.redisTemplate = redisTemplate; }
- 方案二:使用StringRedisTemplate
@Autowired private StringRedisTemplate stringRedisTemplate; stringRedisTemplate.opsForValue().set("user","admin"); System.out.println(stringRedisTemplate.opsForValue().get("name"));
4. StringRedisTemplate和RedisTemplate区别
- RedisTemplate使用的是 JdkSerializationRedisSerializer
- 可以用来存储对象,但是要实现Serializable接口
- 以二进制方式存储,内容没有可读性
- StringRedisTemplate使用的是 StringRedisSerializer序列化String
- 主要用来存储字符串,StringRedisSerializer的泛型指定的是String,当存入对象的时候回报错:
can not cast into String
- 可见性强,更易维护
- 主要用来存储字符串,StringRedisSerializer的泛型指定的是String,当存入对象的时候回报错:
二、redis lock
1. 分布式锁
- 定义:应用于分布式环境下多个节点之间进行同步或者协作的锁;
- 特性:
- 互斥:保证只有持有锁的实例中的线程才能操作
- 可重入:同一个实例的同一个线程可以多次获取锁
- 锁超时:支持超时自动释放锁,避免死锁;
- 谁加的锁只能谁释放;
2. Redis lock实现原理
加锁时存入,释放锁则删除;
两个方法可以实现:
- setnx(set if not exists):存在则不操作返回0,不存在则set返回1,这是redis的方法;
- setIfAbsent:同setnx一样,set的key不存在则set并返回true,存在不操作,返回0;这是Java的方法;
3. 如何避免死锁
- 在set的时候,再给key设置一个自动过期时间
- 在2.6之前,语法是 setnx key val,不支持超时设置,要与expire配合
- 在2.6之后,进行了增强,支持超时设置
- 但setIfAbsent底层有一个设置过期时间的重载方法;
4. 在项目中配置redis lock
- utils:自己封装一个redis lock utils,有加锁方法,释放锁方法,具体思路如下:
- 加锁方法:
- 使用
setIfAbsent
方法存入key,如果是第一次set则直接返回;操作成功 - 如果之前已set,说明持有锁者还在使用资源,此时需要续费,自动延长过期时间
- 使用
getAndSet
方法重新设置该key的值
- 使用
- 使用
- 释放锁:
- 删除该key:
- 在删除之前,先判断该key的值是否为空且取出来的value要与传进来的value相等
- 删除该key:
- 加锁方法:
@Slf4j
@Component
public class RedisLock {
@Autowired
private StringRedisTemplate redisTemplate;
public boolean lockV3(String key, String value) {
// 当加锁字段不存在时,返回true,
if (redisTemplate.opsForValue().setIfAbsent(key, value)) {
return true;
}
// 如果key存在,则取出来
String oldValue = redisTemplate.opsForValue().get(key);
// 判断key不为空,并且还没有过期
if (Objects.nonNull(oldValue) && System.currentTimeMillis() > Long.parseLong(oldValue)) {
// 获取原来的key并重新赋值
String valueByGetAndSet = redisTemplate.opsForValue().getAndSet(key, value);
// 复制成功或者旧值与现在的值相等
if (Objects.isNull(valueByGetAndSet) || valueByGetAndSet.equals(oldValue)) {
return true;
}
}
return false;
}
public void unLockV2(String key, String value) {
String oldValue = redisTemplate.opsForValue().get(key);
if (Objects.nonNull(oldValue) && oldValue.equals(value)) {
try {
redisTemplate.delete(key);
} catch (Exception e) {
log.error("解锁失败:{}", e);
}
}
}
}
5. 分布式锁的实现方式
- MySQL
- 通过数据库事务+行锁实现
- 缺点:
- 需要手动提交事务,如果忘记提交,则会死锁,危险性高;可通过wait设置等待时间
- 事务中如果有其他逻辑操作,会导致锁时间长,影响性能
- 注意:查询要有明确主键,无主键则为表锁,涉及数据库操作统一使用for update加锁
- redis:
- setnx(set if not exists)
- setIfAbsent
- 缺点:
- 如果set成功,还没来得及释放,服务挂了,这个key永远不会被获取到
- set时,还没来得及expire,服务挂了,会造成锁不释放
- zookeeper