e2

滴滴侠,fai抖

  博客园 :: 首页 :: 新随笔 :: 联系 :: 订阅 :: 管理 ::

前言:

上篇已经介绍了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种数据结构的操作:

  1.  
    redisTemplate.opsForValue();//操作字符串
  2.  
    redisTemplate.opsForHash();//操作hash
  3.  
    redisTemplate.opsForList();//操作list
  4.  
    redisTemplate.opsForSet();//操作set
  5.  
    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的使用分为三步:引依赖,配置,使用...

第一步:引入依赖

  1.  
    <dependency>
  2.  
    <groupId>org.springframework.boot</groupId>
  3.  
    <artifactId>spring-boot-starter-data-redis</artifactId>
  4.  
    </dependency>

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

  1.  
    /**
  2.  
    * redis配置类
  3.  
    **/
  4.  
    @Configuration
  5.  
    @EnableCaching//开启注解
  6.  
    public class RedisConfig {
  7.  
    //以下两种redisTemplate自由根据场景选择,优先推荐使用StringRedisTemplate
  8.  
    /**redisTemplate方式*/
  9.  
    @Bean
  10.  
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
  11.  
    RedisTemplate<Object, Object> template = new RedisTemplate<>();
  12.  
    template.setConnectionFactory(connectionFactory);
  13.  
     
  14.  
    //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式)
  15.  
    Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);
  16.  
     
  17.  
    ObjectMapper mapper = new ObjectMapper();
  18.  
    mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
  19.  
    mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
  20.  
    serializer.setObjectMapper(mapper);
  21.  
     
  22.  
    template.setValueSerializer(serializer);
  23.  
    //使用StringRedisSerializer来序列化和反序列化redis的key值
  24.  
    template.setKeySerializer(new StringRedisSerializer());
  25.  
    template.afterPropertiesSet();
  26.  
    return template;
  27.  
    }
  28.  
    /**StringRedisTemplate方式*/
  29.  
    // @Bean
  30.  
    // public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
  31.  
    // StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
  32.  
    // stringRedisTemplate.setConnectionFactory(factory);
  33.  
    // return stringRedisTemplate;
  34.  
    // }
  35.  
     
  36.  
    }

配置application.yml:

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

第三步:使用

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

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

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

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

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

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

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

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

先写接口RedisService:

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

再写实现类:
 

  1.  
    @Service
  2.  
    public class RedisServiceImpl implements RedisService {
  3.  
     
  4.  
    @Resource
  5.  
    RedisTemplate<String, Object> redisTemplate;
  6.  
     
  7.  
    @Override
  8.  
    public void set(String key, Object value) {
  9.  
    ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
  10.  
    valueOperations.set(key, value);
  11.  
    }
  12.  
     
  13.  
    @Override
  14.  
    public void set(String key, Object value, Long timeOutSec) {
  15.  
    ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
  16.  
    valueOperations.set(key, value, timeOutSec, TimeUnit.SECONDS);
  17.  
    }
  18.  
     
  19.  
    @Override
  20.  
    public Object get(String key) {
  21.  
    ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
  22.  
    return valueOperations.get(key);
  23.  
    }
  24.  
    }

调用:

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

  1.  
    @RequestMapping("getValue")
  2.  
    public ModelAndView getValue(@RequestParam("key") String key, ModelAndView modelAndView) {
  3.  
    modelAndView.addObject("key", key);
  4.  
    User user = new User("老汉",18);
  5.  
    redisService.set(key,user,10L);
  6.  
    Object value = redisService.get(key);
  7.  
    modelAndView.addObject("value",value);
  8.  
    modelAndView.setViewName(PREFIX + "hello.html");
  9.  
    return modelAndView;
  10.  
    }
  11.  
    }

效果:

然后我们进入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.  
    1.所有系统在接收到请求后都去创建一把锁,这把锁的key均相同,但只有一个系统能最终创建成功,其他系统创建失败.
  2.  
    2.创建锁成功的系统继续进行后续操作,比如下单,保存数据至数据库...未获得锁的系统等待,直到该系统操作完成后把锁释放,继续开始竞争该锁.
  1.  
    为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
  2.  
     
  3.  
    1.互斥性。在任意时刻,只有一个客户端能持有锁。
  4.  
    2.不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
  5.  
    3.具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
  6.  
    4.解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了

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

  1.  
    /**
  2.  
    *初始化Jedis连接池
  3.  
    */
  4.  
    public class JedisPoolConfig {
  5.  
    private static JedisPool pool = null;
  6.  
     
  7.  
    /**
  8.  
    * 静态代码块 构建redis连接池
  9.  
    */
  10.  
    static {
  11.  
    if (pool == null) {
  12.  
    redis.clients.jedis.JedisPoolConfig config = new redis.clients.jedis.JedisPoolConfig();
  13.  
    //控制一个pool可分配多少个jedis实例,通过pool.getResource()来获取;
  14.  
    //如果赋值为-1,则表示不限制;如果pool已经分配了maxActive个jedis实例,则此时pool的状态为exhausted(耗尽)。
  15.  
    config.setMaxTotal(50);
  16.  
    //控制一个pool最多有多少个状态为idle(空闲的)的jedis实例。
  17.  
    config.setMaxIdle(10);
  18.  
    //表示当borrow(引入)一个jedis实例时,最大的等待时间,如果超过等待时间,则直接抛出JedisConnectionException;单位毫秒
  19.  
    //小于零:阻塞不确定的时间, 默认-1
  20.  
    config.setMaxWaitMillis(1000 * 100);
  21.  
    //在borrow(引入)一个jedis实例时,是否提前进行validate操作;如果为true,则得到的jedis实例均是可用的;
  22.  
    config.setTestOnBorrow(true);
  23.  
    //return 一个jedis实例给pool时,是否检查连接可用性(ping())
  24.  
    config.setTestOnReturn(true);
  25.  
    //connectionTimeout 连接超时(默认2000ms)
  26.  
    //soTimeout 响应超时(默认2000ms)
  27.  
    pool = new JedisPool(config, "192.168.1.1", 6379, 10000);
  28.  
    }
  29.  
    }
  30.  
     
  31.  
    /**
  32.  
    * 方法描述 获取Jedis实例
  33.  
    *
  34.  
    * @return
  35.  
    */
  36.  
    public static Jedis getJedis() {
  37.  
    return pool.getResource();
  38.  
    }
  39.  
     
  40.  
    /**
  41.  
    * 方法描述 释放jedis连接资源
  42.  
    *
  43.  
    * @param jedis
  44.  
    */
  45.  
    public static void returnResource(Jedis jedis) {
  46.  
    if (jedis != null) {
  47.  
    jedis.close();
  48.  
    }
  49.  
    }
  50.  
     
  51.  
    }
  1.  
    public class DistributeLock {
  2.  
     
  3.  
    private static final String LOCK_SUCCESS = "OK";
  4.  
    private static final String SET_IF_NOT_EXIST = "NX";
  5.  
    private static final String SET_WITH_EXPIRE_TIME = "PX";
  6.  
    private static final Long RELEASE_SUCCESS = 1L;
  7.  
     
  8.  
    /**
  9.  
    * 尝试获取分布式锁
  10.  
    * @param jedis Redis客户端
  11.  
    * @param lockKey 锁
  12.  
    * @param requestId 请求标识
  13.  
    * @param expireTime 超期时间
  14.  
    * @return 是否获取成功
  15.  
    */
  16.  
    public static boolean acquire(Jedis jedis, String lockKey, String requestId, int expireTime) {
  17.  
     
  18.  
    String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
  19.  
     
  20.  
    if (LOCK_SUCCESS.equals(result)) {
  21.  
    return true;
  22.  
    }
  23.  
    return false;
  24.  
     
  25.  
    }
  26.  
     
  27.  
    /**
  28.  
    * 释放分布式锁
  29.  
    * @param jedis Redis客户端
  30.  
    * @param lockKey 锁
  31.  
    * @param requestId 请求标识
  32.  
    * @return 是否释放成功
  33.  
    */
  34.  
    public static boolean release(Jedis jedis, String lockKey, String requestId) {
  35.  
     
  36.  
    String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
  37.  
    Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
  38.  
     
  39.  
    if (RELEASE_SUCCESS.equals(result)) {
  40.  
    return true;
  41.  
    }
  42.  
    return false;
  43.  
    }
  44.  
     
  45.  
    }

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

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

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

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

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

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

 

posted on 2019-09-06 19:31  纯黑Se丶  阅读(1949)  评论(0)    收藏  举报