【redis】使用redisTemplate优雅地操作redis及使用redis实现分布式锁

前言:

上篇已经介绍了redis及如何安装和集群redis,这篇介绍如何通过工具优雅地操作redis.

Long Long ago,程序猿们还在通过jedis来操作着redis,那时候的猿类,一个个累的没日没夜,重复的造着轮子,忙得没时间陪家人,终于有一天猿类的春天来了,spring家族的redis template 解放了程序猿的双手,于是猿类从使用Jedis石器时代的进入自动化时代...

redis template是对jedis的高度封装,让java对redis的操作更加简单,甚至连小学生都可以驾驭...

在正式进入学习前,先给大家介绍一款Redis可视化工具,个人感觉比Redis Desktop Manager这类工具好用很多,而且是国产的,如果公司有服务器的话,可以部署上去,然后今后大家都可以直接去使用,比较方便.

传送门:http://www.treesoft.cn/dms.html

亦可百度搜treesoft,我不是托...


在正式学习之前,我们再来回顾一下Redis的支持存储的五大数据类型:

分别为String(字符串)、List(列表)、Set(集合)、Hash(散列)和 Zset(有序集合)

RedisTemplate中封装了对5种数据结构的操作:

redisTemplate.opsForValue();//操作字符串
redisTemplate.opsForHash();//操作hash
redisTemplate.opsForList();//操作list
redisTemplate.opsForSet();//操作set
redisTemplate.opsForZSet();//操作有序set

StringRedisTemplate与RedisTemplate

  • 两者的关系是StringRedisTemplate继承RedisTemplate。

  • 两者的数据是不共通的;也就是说StringRedisTemplate只能管理StringRedisTemplate里面的数据,RedisTemplate只能管理RedisTemplate中的数据。

  • SDR默认采用的序列化策略有两种,一种是String的序列化策略,一种是JDK的序列化策略。

    StringRedisTemplate默认采用的是String的序列化策略,保存的key和value都是采用此策略序列化保存的。

    RedisTemplate默认采用的是JDK的序列化策略,保存的key和value都是采用此策略序列化保存的。

     以上两种方式,根据实际业务需求灵活去选择,操作字符串类型用StringRedis Template,操作其它数据类型用Redis Template.


Redis Template的使用分为三步:引依赖,配置,使用...

第一步:引入依赖

     <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
     </dependency>

第二步:配置Redis Template(redisTemplate或StringRedisTemlate根据业务任选一种)

/**
 * redis配置类
 **/
@Configuration
@EnableCaching//开启注解
public class RedisConfig {
     //以下两种redisTemplate自由根据场景选择,优先推荐使用StringRedisTemplate
    /**redisTemplate方式*/
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式)
        Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);

        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        serializer.setObjectMapper(mapper);

        template.setValueSerializer(serializer);
        //使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }
      /**StringRedisTemplate方式*/
//    @Bean
//    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
//        StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
//        stringRedisTemplate.setConnectionFactory(factory);
//        return stringRedisTemplate;
//    }

}

配置application.yml:

spring:
  redis:
    host: 192.168.1.1
    password: 123456 # 没密码的话不用配
    port: 6379
    database: 10 #我这里因为从可视化工具里发现10这个库比较空,为了方便演示,所以配了10.

第三步:使用

为了今后使用方便,其实你可以封装一个RedisService,其功能有点类似JPA或者MyBatis这种,把需要对redis的存取操作封装进去,当然这一步只是建议,封不封由你...

由于之前配置了redisTemplate及其子类,故需要使用@Resource注解进行调用.

@Resource
private RedisTemplate<String, Object> redisTemplate;//类型可根据实际情况走

然后就可以根据redisTemplate进行各种数据操作了:

使用:redisTemplate.opsForValue().set("name","tom");
结果:redisTemplate.opsForValue().get("name")  输出结果为tom

更多的我就不演示了,只要你对redis的5大数据类型的基本操作掌握即可轻松使用,,比较简单,没啥意思,如果感兴趣可以参考这篇博客,写得十分详细:

https://blog.csdn.net/ruby_one/article/details/79141940

下面我主要说一下前面提到的封装RedisService,二话不说我先上代码为敬:

先写接口RedisService:

/**Redis存取操作*/
public interface RedisService {
    void set(String key,Object value);//无过期时间
    void set(String key,Object value,Long timeOutSec);//带过期时间,单位是秒,可以配.
    Object get(String key);
}

再写实现类:
 

@Service
public class RedisServiceImpl implements RedisService {
    
    @Resource
    RedisTemplate<String, Object> redisTemplate;

    @Override
    public void set(String key, Object value) {
        ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
        valueOperations.set(key, value);
    }

    @Override
    public void set(String key, Object value, Long timeOutSec) {
        ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
        valueOperations.set(key, value, timeOutSec, TimeUnit.SECONDS);
    }

    @Override
    public Object get(String key) {
        ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
        return valueOperations.get(key);
    }
}

调用:

随便写了两个页面,第一个页面表单传Key过来,第二个页面对Key的value进行封装并存入redis,再取出来作展现:

    @RequestMapping("getValue")
    public ModelAndView getValue(@RequestParam("key") String key, ModelAndView modelAndView) {
        modelAndView.addObject("key", key);
        User user = new User("老汉",18);
        redisService.set(key,user,10L);
        Object value = redisService.get(key);
        modelAndView.addObject("value",value);
        modelAndView.setViewName(PREFIX + "hello.html");
        return modelAndView;
    }
}

效果:

然后我们进入TreeSoft来看一下redis中的数据是否有存进来:

可以看到,没有问题,数据已经进来,10秒后再次刷新页面,数据已经过期,从redis数据库中正常消失,完全符合预期.

前面提到了redisTemplate和StringRedisTemplate,下面我们看看他们除了我前面提到的那些差别,还有哪些地方不一样:

重启项目后,同样的数据,看下效果:

结果未变,但redis中的数据变成了这样...查看不了,删除不了,修改不了,因为乱码了...看上去这种序列化方式似乎更加安全,但事实上,只是因为这款工具不支持显示这样的序列化方式编码,换一个可视化工具结果就不一样了,所以不要被表面现象蒙蔽了,要多文档及源码,两者真正的差别是在操作数据类型上,StringRedisTemplate只适合操作String类型的,其他类型一律用RedisTemplate.

关于redis Template已是高度封装了,对各种数据类型的操作都比较简单,其他数据类型的操作我就不一一演示了,其实自从有了json,StringRedis Template 也可以用来存储其他数据类型了,万物皆字符串,管你是什么类型,都可以用Json字符串来表示,所以大家重点掌握String类型的数据存取即可.


分布式锁:

在单体应用架构中,遇到并发安全性问题时我们可以通过同步锁Synchronized,同步代码块,ReentrantLock等方式都可以解决,但在分布式系统中,JDK提供的这些并发锁都失效了,我们需要一把"全局的锁",所有的分布式系统共享这把锁,这把锁同一时间内只能被一个系统拥有,拥有锁的系统获得一些相应的权限,其它系统需要等待拥有锁的系统释放锁,然后去竞争这把锁,只有拥有这把锁的系统才具有相应权限.

分布式锁目前比较常见的有3种实现方式,一种是基于Redis实现的,一种是基于zookeeper实现的,还有一种是基于数据库层面的乐观锁和悲观锁.

本篇只介绍基于Redis的实现方式,其它两种请翻阅本博,均有介绍和实现.

学之前先来了解一个将会用到的Redis命令

setNX(set if not exist):意思是如果不存在才会设置值,否则啥也不做,如果不存在,设置成功后返回值为1,失败则返回0;

下面说一下实现原理:

1.所有系统在接收到请求后都去创建一把锁,这把锁的key均相同,但只有一个系统能最终创建成功,其他系统创建失败.
2.创建锁成功的系统继续进行后续操作,比如下单,保存数据至数据库...未获得锁的系统等待,直到该系统操作完成后把锁释放,继续开始竞争该锁.
为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:

1.互斥性。在任意时刻,只有一个客户端能持有锁。
2.不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
3.具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
4.解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了

逻辑比较简单,我直接上代码:

/**
*初始化Jedis连接池
*/
public class JedisPoolConfig {
    private static JedisPool pool = null;

    /**
     * 静态代码块 构建redis连接池
     */
    static {
        if (pool == null) {
            redis.clients.jedis.JedisPoolConfig config = new redis.clients.jedis.JedisPoolConfig();
            //控制一个pool可分配多少个jedis实例,通过pool.getResource()来获取;
            //如果赋值为-1,则表示不限制;如果pool已经分配了maxActive个jedis实例,则此时pool的状态为exhausted(耗尽)。
            config.setMaxTotal(50);
            //控制一个pool最多有多少个状态为idle(空闲的)的jedis实例。
            config.setMaxIdle(10);
            //表示当borrow(引入)一个jedis实例时,最大的等待时间,如果超过等待时间,则直接抛出JedisConnectionException;单位毫秒
            //小于零:阻塞不确定的时间,  默认-1
            config.setMaxWaitMillis(1000 * 100);
            //在borrow(引入)一个jedis实例时,是否提前进行validate操作;如果为true,则得到的jedis实例均是可用的;
            config.setTestOnBorrow(true);
            //return 一个jedis实例给pool时,是否检查连接可用性(ping())
            config.setTestOnReturn(true);
            //connectionTimeout 连接超时(默认2000ms)
            //soTimeout 响应超时(默认2000ms)
            pool = new JedisPool(config, "192.168.1.1", 6379, 10000);
        }
    }

    /**
     * 方法描述 获取Jedis实例
     *
     * @return
     */
    public static Jedis getJedis() {
        return pool.getResource();
    }

    /**
     * 方法描述 释放jedis连接资源
     *
     * @param jedis
     */
    public static void returnResource(Jedis jedis) {
        if (jedis != null) {
            jedis.close();
        }
    }

}
public class DistributeLock {

    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";
    private static final Long RELEASE_SUCCESS = 1L;

    /**
     * 尝试获取分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    public static boolean acquire(Jedis jedis, String lockKey, String requestId, int expireTime) {

        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;

    }

    /**
     * 释放分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public static boolean release(Jedis jedis, String lockKey, String requestId) {

        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));

        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }

}

在锁的创建中,创建和设置过期时间必须保持原子性操作,否则万一服务器在创建锁时宕机了,该节点变为永久节点,会造成死锁.

在锁的释放中,判断当前锁是否有效和删除该锁也必须保持原子性操作,否则万一服务器在判断锁是否有效后发生GC或者其它卡顿,可能会造成误删,所以这里用了Lua脚本去执行,确保原子性.

另外上面有提到解铃还须系铃人,故需要一个requestId来区分不同的请求.

原本想用redisTemplate来实现的,事实上我也确实用redisTemplate写了一个,但因为自己不会写lua脚本,在锁的释放这里不能做到原子性操作,所以借鉴了别人用Jedis方式的实现.

参考资料:https://www.cnblogs.com/linjiqin/p/8003838.html

https://blog.csdn.net/g6U8W7p06dCO99fQ3/article/details/81073892

猜你喜欢

转载自blog.csdn.net/lovexiaotaozi/article/details/83505131