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
:将储存的值加上1 -
decr key
:将储存的值减去1 -
incrby key amout
:加上整数amount -
decrby 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和end -
ltrim 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
:获取散列中所有key -
hvals hkey
:获取三列中所有值 -
hgetall hkey
:获取散列 -
hincrby hkey key increment
:为key的值上加上整数increment -
hincrbyfloat 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成员的分值加上increment -
zcount 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
- 可见性强,更易维护
二、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相等
@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