上篇已经介绍了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,我不是托...
分别为String(字符串)、List(列表)、Set(集合)、Hash(散列)和 Zset(有序集合)
redisTemplate.opsForValue();//操作字符串
redisTemplate.opsForHash();//操作hash
redisTemplate.opsForList();//操作list
redisTemplate.opsForSet();//操作set
redisTemplate.opsForZSet();//操作有序set
两者的关系是StringRedisTemplate继承RedisTemplate。
两者的数据是不共通的;也就是说StringRedisTemplate只能管理StringRedisTemplate里面的数据,RedisTemplate只能管理RedisTemplate中的数据。
SDR默认采用的序列化策略有两种,一种是String的序列化策略,一种是JDK的序列化策略。
StringRedisTemplate默认采用的是String的序列化策略,保存的key和value都是采用此策略序列化保存的。
RedisTemplate默认采用的是JDK的序列化策略,保存的key和value都是采用此策略序列化保存的。
以上两种方式,根据实际业务需求灵活去选择,操作字符串类型用StringRedis Template,操作其它数据类型用Redis Template.
Redis Template的使用分为三步:引依赖,配置,使用...
第一步:引入依赖
org.springframework.boot
spring-boot-starter-data-redis
第二步:配置Redis Template(redisTemplate或StringRedisTemlate根据业务任选一种)
/**
* redis配置类
**/
@Configuration
@EnableCaching//开启注解
public class RedisConfig {
//以下两种redisTemplate自由根据场景选择,优先推荐使用StringRedisTemplate
/**redisTemplate方式*/
@Bean
public RedisTemplate
配置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 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 redisTemplate;
@Override
public void set(String key, Object value) {
ValueOperations valueOperations = redisTemplate.opsForValue();
valueOperations.set(key, value);
}
@Override
public void set(String key, Object value, Long timeOutSec) {
ValueOperations valueOperations = redisTemplate.opsForValue();
valueOperations.set(key, value, timeOutSec, TimeUnit.SECONDS);
}
@Override
public Object get(String key) {
ValueOperations 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