从Spring Redis到浅谈分布式锁

Java Redis客户端概述

  • Jedis是Redis的Java实现的客户端,提供了基本类型的支持,提供了比较全面的Redis命令的支持;了解Redis命令就能比较熟练的使用Jedis;

Jedis使用阻塞的I/O,且其方法调用都是同步的,程序流需要等到sockets处理完I/O才能执行,不支持异步。Jedis客户端实例不是线程安全的,所以需要通过连接池来使用Jedis。

  • Redisson基于Netty框架来实现的,其方法调用是异步的。Redisson的API是线程安全的,所以可以操作单个Redisson连接来完成各种操作。实现了分布式和可扩展的Java数据结构,提供很多分布式相关操作服务,例如,分布式锁,分布式集合等

Redisson的宗旨是促进使用者对Redis的关注分离,从而让使用者能够将精力更集中地放在处理业务逻辑上。Redisson中的方法则是进行比较高的抽象,每个方法调用可能进行了一个或多个Redis方法调用。

  • Lettuce:基于Netty框架的事件驱动的高级Redis客户端,用于线程安全同步,异步和响应使用,支持集群,Sentinel,管道和编码器。目前springboot默认使用的客户端。
  • Jedis api 在线网址:http://tool.oschina.net/uploads/apidocs/redis/clients/jedis/Jedis.html
  • redisson 官网地址:https://redisson.org/
  • redisson git项目地址:https://github.com/redisson/redisson
  • lettuce 官网地址:https://lettuce.io/
  • lettuce git项目地址:https://github.com/lettuce-io/lettuce-core

Redis可视化工具

https://www.jianshu.com/p/721c3762a777

RedisDesktopManager(免/收费)、FastoRedis 收费

https://juejin.im/post/5e9423806fb9a03c590df5c3

redis desktop manager、medis、AnotherRedisDesktopManager、FastoRedis 等等

Spring Data Redis

简述

目前的安装 redis版本 5.0.4,最新版本6.0;以下演示代码SpringBoot版本2.1.3;lettuce版本是5.1.4

Spring Data Redis目前支持两种驱动:Jedis和Lettuce,可以看成是这两种驱动的统一封装,以高度统一的形式屏蔽了底层驱动的操作细节,向用户提供一种统一的API

springboot 2.x版本中默认客户端是用lettuce实现的;在 springboot 1.5.x版本的默认的Redis客户端是 Jedis实现。

配置文件一览参考,注意网上很多都是spring.redis.jedis,不要随意引用,注意自己底层的实现

spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=root

## spring.redis.jedis和下面类似
# 连接池最大连接数(使用负值表示没有限制) 默认为8
spring.redis.lettuce.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认为-1
spring.redis.lettuce.pool.max-wait=-1ms
# 连接池中的最大空闲连接 默认为8
spring.redis.lettuce.pool.max-idle=8
# 连接池中的最小空闲连接 默认为 0
spring.redis.lettuce.pool.min-idle=0

简单问题一览

Redis序列化报错

IllegalArgumentException: DefaultSerializer requires a Serializable payload but received
要缓存的 Java 对象必须实现 Serializable 接口,因为 Spring 会将对象先序列化再存入 Redis

Redis乱码

配置RedisTemplate序列化不然会乱码
注意:添加序列化之前的key,要屏蔽掉序列化才能找到;乱码要乱码的一样
RedisTemplate和StringRedisTemplate,配置就是全换成StringRedisSerializer

RedisTemplate使用的是 JdkSerializationRedisSerializer
StringRedisTemplate使用的是 StringRedisSerializer

代码演示

opsForValue - 操作 Key - Value ,简单的键值对
opsForList - 操作列表
opsForSet - 操作集合(set)
opsForZSet - 操作有序集合(sorted set,在 Redis 内叫 zSet)
opsForHash - 操作 Map 集合
opsForGeo - 操作地理信息,可以当作 HashSet 看,不过里面的 Key 是二维地理坐标
opsForHyperLogLog - 操作 HyperLogLog 值,是 counter 。
opsForCluster - 操作集群,管理集群用。

配置redisTemplate

    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        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);

        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;
    }
 @Autowired
    RedisTemplate redisTemplate;

    @ApiOperation(value = "根据key(id)查询")
    @RequestMapping(value = "/{key}", method = RequestMethod.GET)
    public Object getById(@PathVariable("key") String key){
        log.info("根据key {} 进行查询", key);
        Person person = (Person)redisTemplate.opsForValue().get(key);
        return person;
    }

    @ApiOperation("用id做key,保存")
    @RequestMapping(value = "/save", method = RequestMethod.POST)
    public void save(@RequestBody Person person){
        log.info("调用保存接口");
        //不存在就保存 存在就更新
        redisTemplate.opsForValue().set(person.getId(), person);
    }

    @ApiOperation("删除")
    @ApiImplicitParam(name = "key", value = "用户id")
    @RequestMapping(value = "/{key}", method = RequestMethod.DELETE)
    public Boolean delete(@PathVariable("key") String key){
        log.info("调用删除接口");
        // 不存在就返回false
        return  redisTemplate.delete(key);
    }

    @ApiOperation("更新")
    @RequestMapping(value = "/update", method = RequestMethod.PUT)
    public Object update(@ModelAttribute Person person){
        //得到旧值   不存在就保存 存在就更新
        Object andSet = redisTemplate.opsForValue().getAndSet(person.getId(), person);
        return andSet;
    }

set和list简单操作

  @Autowired
    RedisTemplate redisTemplate;

    @ApiOperation(value = "多个value,分割,保存到set中")
    @RequestMapping(value = "/saveToSet/{key}", method = RequestMethod.POST)
    public Long save(@PathVariable("key") String key, @RequestParam String values){
        log.info("保存values:{}", values);
        // set保存,返回保存长度;重复项-1
       return redisTemplate.opsForSet().add(key, values.split(","));
    }

    @ApiOperation(value = "将set中的值,移动到list中")
    @RequestMapping(value = "/moveToList", method = RequestMethod.POST)
    public long move(@RequestParam String setKey, @RequestParam String listKey){
        log.info("将set{}中的值,移动到list{}中:", setKey, listKey);
        Set members = redisTemplate.opsForSet().members(setKey);
        log.info("将要copy的set长度为{},values{}", redisTemplate.opsForSet().size(setKey), members.toArray());
        // 返回list长度
        return redisTemplate.opsForList().leftPushAll(listKey, members);
    }

    @ApiOperation(value = "从list弹出一个值")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "direction", value = "方向", dataType = "String", paramType = "query",
                    allowableValues = "left,right")
    })
    @RequestMapping(value = "/listPop", method = RequestMethod.GET)
    public Object lkstPop(@RequestParam String listKey, @RequestParam String direction){
        if(direction.equalsIgnoreCase("right")){
            return redisTemplate.opsForList().rightPop(listKey);
        }
        return redisTemplate.opsForList().leftPop(listKey);
    }

浅谈分布式锁

最早通过技术专题讨论第四期:漫谈分布式锁了解分布式锁的概念;简单理解的话,就是需要一个分布式情况下的协调器(对外是相对单点的),那我们常用的选项就有以下几项:

  • 基于数据库

在数据库中创建一张表,表里包含方法名等字段,并且在方法名字段上面创建唯一索引,执行某个方法需要使用此方法名向表中插入数据,成功插入则获取锁,执行结束则删除对应的行数据释放锁

  • 基于缓存数据库Redis

Redis性能好并且实现方便,但是单节点的分布式锁在故障迁移时产生安全问题,Redlock是Redis的作者 Antirez 提出的集群模式分布式锁,基于N个完全独立的Redis节点实现分布式锁的高可用

  • 基于ZooKeeper

ZooKeeper 是以 Paxos 算法为基础的分布式应用程序协调服务,为分布式应用提供一致性服务的开源组件

Redis分布式锁

简单分布式锁

最初分布式锁借助于setnx和expire命令,但是这两个命令不是原子操作,如果执行 setnx之后获取锁但是此时客户端挂掉,这样无法执行expire设置过期时间就导致锁 一直无法被释放,因此在2.8版本中Antirez为setnx增加了参数扩展, 使得setnx和expire具备原子操作性。

方法Boolean setIfAbsent(K key, V value, long timeout, TimeUnit unit);是在Spring Data Redis2.1版本中新增的;

加锁和续期:加锁使用测试2;续期使用测试3

    @ApiOperation("测试1")
    @RequestMapping(value = "/test3", method = RequestMethod.GET)
    public boolean test3(@ModelAttribute Person person){
        // 设置过期时间 10秒/ 不存在返回false
        return redisTemplate.expire(person.getName(), 10, TimeUnit.SECONDS);
    }

    @ApiOperation("测试2,不存在key就新建并设置过期时间")
    @RequestMapping(value = "/test", method = RequestMethod.GET)
    public Boolean test(@ModelAttribute Person person){
        //test:张三  文件夹
        return redisTemplate.opsForValue().setIfAbsent(person.getName(), person, 10, TimeUnit.SECONDS);
    }

    @ApiOperation("测试3,存在就更新并设置过期时间,存在返回true,不存在返回false")
    @RequestMapping(value = "/test2", method = RequestMethod.GET)
    public Boolean test2(@ModelAttribute Person person){
        return redisTemplate.opsForValue().setIfPresent(person.getName(), person, 10, TimeUnit.SECONDS);
    }

待优化

续期优化:可以在事务中check锁是你的锁,然后再续期,不要瞎续期;或者用redis lua脚本

解锁优化:同理检查锁是你的锁

Redis集群和RedLock算法

如果在分布式环境中,线程A设置锁后,master节点挂了,但是锁没有同步给slave,然后slave升级成master节点,这时候线程B来设置锁,导致线程A和B同时拥有锁,由此产生了RedLock算法;

在Redis的分布式环境中,我们假设有N个完全互相独立的Redis节点,在N个Redis实例上使用与在Redis单实例下相同方法获取锁和释放锁。

现在假设有5个Redis主节点(大于3的奇数个),这样基本保证他们不会同时都宕掉,获取锁和释放锁的过程中,客户端会执行以下操作:

1.获取当前Unix时间,以毫秒为单位
2.依次尝试从5个实例,使用相同的key和具有唯一性的value获取锁
当向Redis请求获取锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间,这样可以避免客户端死等
3.客户端使用当前时间减去开始获取锁时间就得到获取锁使用的时间。当且仅当从半数以上的Redis节点取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功
4.如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间,这个很重要
5.如果因为某些原因,获取锁失败(没有在半数以上实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁,无论Redis实例是否加锁成功,因为可能服务端响应消息丢失了但是实际成功了,毕竟多释放一次也不会有问题

当然有人也对这个算法提出了质疑,这个问题仁者见仁智者见智;

Java的redLock实现包是Redisson,可以使用Redisson直接来使用集群的分布式锁。

参考链接:

https://docs.spring.io/spring-data/redis/docs/2.1.19.RELEASE/reference/html/

https://blog.csdn.net/hanchao5272/article/details/99695360

https://www.cnblogs.com/backnullptr/p/12155812.html

猜你喜欢

转载自blog.csdn.net/Zzhou1990/article/details/107910479
今日推荐