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