1 Redis简介
Redis 是完全开源免费的、遵守BSD协议的高性能数据库。Redis支持String,list,set,zset,hash等数据结构的key-value存储。它支持数据的持久化,支持master-slave模式的数据备份,支持事务。
Redis 优势:
(1)性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s 。
(2)丰富的数据类型 – Redis支持 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。
(3)原子 – Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。
(4)丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等等特性。
2 Redis的Java客户端配置
Redis的java客户端有非常多,下面是Redis官网列出的Java客户端:
名称 | 介绍 |
aredis | 基于 Java 7 NIO Channel API的客户端 |
JDBC-Redis | mavcunha |
Jedis | xetorthio |
JRedis | SunOf27 |
lettuce | 线程安全的支持同步、异步、响应式的高级Java客户端 ,支持集群、哨兵、Pipelining和codecs |
mod-redis | Vert.x (事件驱动框架)官方的 redis.io 消息总线模块 |
redis-protocol | 兼容2.6+版本,高性能的 Java, Java w/Netty & Scala (finagle) client 客户端 |
RedisClient | 图形化Redis客户端 |
Redisson | 包括分布式锁的分布式、可伸缩Java数据结构 |
RJC |
最受欢迎的有Jedis、Lettuce、RedisTemplate,Lettuce基于Netty框架开发,支持同步、异步和响应式模式,多个线程可以共享一个连接实例,而不必担心多线程并发问题;Spring提供的RedisTemplate在Lettuce基础上做了进一步封装,使用起来更加方便。我们试用一下Jedis、RedisTemplate。先上基础设施:Dao接口和工具类:
(1)Dao接口
public interface ICacheDao {
boolean save(UserEntity userEntity);
UserEntity get(int id);
boolean delete(int id);
}
(2)自定义Map和Entity互转工具类。这套简单互转工具我们在Jedis中试用,在RedisTemplate使用时专业的序列化/反序列化工具。
public class EntityUtil {
/**
* 实体转Map
* @param o
* @return
*/
public static Map<String, String> entityToMap(Object o){
if (o == null){
return new HashMap<>(0);
}
Class clazz = o.getClass();
Field[] fields = clazz.getDeclaredFields();
Map<String,String> map = new HashMap<>();
for (Field field:fields){
field.setAccessible(true);
try {
Object value = field.get(o);
if (value == null){
continue;
}
map.put(field.getName(), value.toString());
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
return map;
}
/**
* Map转实体
* @param map
* @param <T>
* @return
*/
public static <T> T mapToEntity(Map<String, String> map, Class<T> clazz){
if (map == null || map.isEmpty() || clazz == null){
return null;
}
T instance = null;
try {
instance = clazz.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
if (instance == null){
return null;
}
for (Map.Entry<String,String> entry:map.entrySet()){
try {
Field field = clazz.getDeclaredField(entry.getKey());
field.setAccessible(true);
field.set(instance, entry.getValue());
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
return instance;
}
}
2.1 Jedis方式
(1)application.properties配置
spring.redis.jedis.pool.max-active=20
spring.redis.jedis.pool.max-idle=4
spring.redis.host=localhost
(2)创建JedisPool
@Configuration
@ConfigurationProperties(prefix = "spring.redis")
public class JedisConfig {
private int maxActive = 8;
private int maxIdle = 8;
private String host = "localhost";
private int maxWaite = -1;
private int port = 6379;
private int timeOut = 30*1000;
@Bean
public JedisPool getJedisPool(){
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(maxActive);
config.setMaxIdle(maxIdle);
config.setMaxWaitMillis(maxWaite);
config.setTestOnBorrow(true);
config.setTestOnReturn(true);
return new JedisPool(config, host, port);
}
(3)DAO实现类
@Component
public class JedisCacheDao implements ICacheDao {
@Autowired
private JedisPool jedisPool;
@Override
public boolean save(UserEntity userEntity){
Jedis jedis = jedisPool.getResource();
String result = jedis.hmset(userEntity.getId() + "", EntityUtil.entityToMap(userEntity));
return "OK".equals(result);
}
@Override
public UserEntity get(int id){
Jedis jedis = jedisPool.getResource();
return EntityUtil.mapToEntity(jedis.hgetAll(id + ""), UserEntity.class);
}
@Override
public boolean delete(int id){
Jedis jedis = jedisPool.getResource();
return jedis.del(id + "")>0;
}
}
(3)测试类
@RunWith(SpringRunner.class)
@SpringBootTest(classes = RedisApplication.class)
public class CacheDaoTest {
@Autowired
@Qualifier("jedisCacheDao")
private ICacheDao cacheDao;
/**
* Method: save(UserEntity userEntity)
*/
@Test
public void testSave() throws Exception {
UserEntity userEntity = new UserEntity();
userEntity.setId("1");
userEntity.setAddress("localhost");
userEntity.setName("him");
boolean successBool = cacheDao.save(userEntity);
System.out.println("save:" + successBool);
}
/**
* Method: get(int id)
*/
@Test
public void testGet() throws Exception {
UserEntity entity = cacheDao.get(1);
if (entity == null) {
System.out.println("没有找到实体");
} else {
System.out.println(entity.getAddress());
}
}
@Test
public void testDelete() throws Exception {
boolean successBool = cacheDao.delete(1);
System.out.println("delete:" + successBool);
}
}
2.2 RedisTemplate方式
(1)application.properties配置
spring.redis.jedis.pool.max-active=20
spring.redis.jedis.pool.max-idle=4
spring.redis.host=localhost
(2)配置RedisTemplate
@Configuration
public class RedisTemplateConfig {
/**
* application.properties配置完成,Spring会配置RedisConnectionFactory这个Bean
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory){
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
jackson2JsonRedisSerializer.setObjectMapper(om);
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(jackson2JsonRedisSerializer);
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashKeySerializer(jackson2JsonRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
(3)简单使用
@Component
public class RedisTemplateCacheDao implements ICacheDao {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public boolean save(UserEntity userEntity) {
ValueOperations<String,Object> kv = redisTemplate.opsForValue();
kv.set("" + userEntity.getId(), userEntity, 20, TimeUnit.SECONDS); //设置过期时间并保存
return true;
}
@Override
public UserEntity get(int id) {
ValueOperations<String,Object> kv = redisTemplate.opsForValue();
Object value = kv.get(id + "");
if (value == null){
return null;
}
return (UserEntity)value;
}
@Override
public boolean delete(int id) {
return redisTemplate.delete("" + id);
}
}
(4)测试类
@RunWith(SpringRunner.class)
@SpringBootTest(classes = RedisApplication.class)
public class RedisTemplateCacheDaoTest {
@Autowired
@Qualifier("redisTemplateCacheDao")
private ICacheDao cacheDao;
@Before
public void before() throws Exception {
}
@After
public void after() throws Exception {
}
/**
* Method: save(UserEntity userEntity)
*/
@Test
public void testSave() throws Exception {
UserEntity userEntity = new UserEntity();
userEntity.setId("1");
userEntity.setAddress("localhost");
userEntity.setName("him");
userEntity.setBirthDay(new Date());
boolean successBool = cacheDao.save(userEntity);
System.out.println("save:" + successBool);
}
/**
* Method: get(int id)
*/
@Test
public void testGet() throws Exception {
UserEntity entity = cacheDao.get(1);
if (entity == null) {
System.out.println("没有找到实体");
} else {
System.out.println(entity.getAddress());
System.out.println(entity.getBirthDay());
}
}
/**
* Method: delete(int id)
*/
@Test
public void testDelete() throws Exception {
boolean successBool = cacheDao.delete(1);
System.out.println("delete:" + successBool);
}
3 Redis的常用操作及适用场景
我们分别描述Jedis和RedisTemplate的API。
3.1 Jedis的常用API
@Component
public class JedisOperation {
@Autowired
private JedisPool jedisPool;
public void add(){
Jedis jedis = jedisPool.getResource();
/*****String*****/
jedis.set("A", "StringValue");
jedis.set("B","StringValueB", "XX", "PX", 20*1000);
jedis.expire("Key", 1000); //设置过期时间的两种方式,创建时设置、创建完成后设置
/******List********/
jedis.lpush("ListB", "ValueA", "ValueB", "ValueN"); //从左边加入2个元素
jedis.lset("ListA", 0, "ListAValue");
/******Set*********/
jedis.sadd("ListB", "ValueA", "ValueA", "ValueN");
/*******ZSet*********/
jedis.zadd("ZSetA", 1, "A");
jedis.zadd("ZSetA", 2, "B");
/******Hash******/
jedis.hmset("Map", new HashMap<String,String>(){{
put("MapA", "B");
put("MapB", "3");
}});
}
public void modify(){
Jedis jedis = jedisPool.getResource();
/****String*****/
jedis.append("A", "aotherString"); //追加
jedis.set("A", "modified"); //修改
/*****List*******/
jedis.lset("ListA", 0, "sss"); //修改0这个位置的元素
/****Set******/
//Set不支持修改,只能删除
/*****zSet*******/
//ZSet不支持修改值,可以修改score
jedis.zincrby("ZSetA", 100,"A");
/********Hash*******/
//Hash不支持字符串修改,支持数值加减
jedis.hincrBy("Map", "MapB", 12); //MapB的值+12
}
public void get(){
Jedis jedis = jedisPool.getResource();
/****String*****/
jedis.get("StringA");
/*****List*******/
jedis.lpop("ListKEY"); //从左边取出第一个
jedis.rpop("ListKey"); //从右边取出第一个
jedis.blpop(10, "ListKEY","ListKEYB"); //有时间限制的阻塞获取
/******Set*******/
jedis.smembers("SetKey");
/******ZSet********/
jedis.zrange("Key", 10, 20); //取出10-20号元素
/*******Map********/
jedis.hgetAll("Key"); //取出Map
jedis.hmget("key","Name","age"); //取出部分属性
}
public void delete(){
Jedis jedis = jedisPool.getResource();
/*****String*******/
jedis.del("Key"); //删除键值对,所有类型都适用
/*****List******/
jedis.lrem("Key", 2, "value"); //删除2个存在的值value
/*****Set******/
jedis.spop("Key"); //随机删除集合中的一个元素
/*****ZSet*******/
jedis.zrem("key", "valueA", "ValueB"); //从集合ZSet中删除2个value;
/******Map*********/
jedis.zrem("key", "valueA","ValueB"); //从Map中删除2个value;
}
}
3.2 RedisTemplate常用API
@Component
public class RedisTemplateOperation {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 5种数据类型的操作及适用场景
*/
public void unboundHandle(){
//【String】
ValueOperations<String, Object> operations = redisTemplate.opsForValue();
operations.set("string_key", "value"); //增
operations.set("string_key", "value1"); //改
Object object = operations.get("string_key"); //查
//【List】,更像一个队列
ListOperations<String,Object> listOperations = redisTemplate.opsForList();
long longValue = listOperations.leftPush("list_key", "value1"); //增
longValue = listOperations.leftPushAll("list_key", "value2", "value3", "value4", "value5"); //增
listOperations.set("list_key", 0, "value0"); //改
listOperations.trim("list_key", 1,4); //删除start-end范围之外的数据
object = listOperations.leftPop("list_key"); //从左侧取
object = listOperations.rightPop("list_key", 1000, TimeUnit.MILLISECONDS); //从右侧取,有超时时间
object = listOperations.rightPopAndLeftPush("list_key", "list_key1"); //从目标右边取出并存入目的地左边
object = listOperations.index("list_key", 0); //取出0号元素
longValue = listOperations.remove("list_key", 1, "value2"); //删除1个value2,没有则返回0
//【Set】,更多用于集合的交并补操作
SetOperations<String,Object> setOperations = redisTemplate.opsForSet();
longValue = setOperations.add("set_key", "value1", "value2", "value3"); //增
longValue = setOperations.add("set_key_1", "value1", "value2", "value3", "value4"); //增
Set set = setOperations.members("set_key_1"); //查
longValue = setOperations.size("set_key"); //元素个数
boolean bool = setOperations.move("set_key", "value2", "set_key_new"); //移动元素
Set<Object> objects = setOperations.intersect("set_key", "set_key_1"); //查询交集
objects = setOperations.union("set_key", "set_key_1"); //查询并集
objects = setOperations.difference("set_key", "set_key_1"); //查询补集(注:没有达到效果)
longValue = setOperations.intersectAndStore("set_key", "set_key_1", "set_key_new"); //存储交集
longValue = setOperations.unionAndStore("set_key", "set_key_1", "set_key_new"); //存储并集
longValue = setOperations.differenceAndStore("set_key", "set_key_1","set_key_new"); //存储补集
List<Object> list = setOperations.pop("set_key", 2); //取出2个元素
//【zSet】,更多用于集合内数据查询
ZSetOperations<String, Object> zSetOperations = redisTemplate.opsForZSet();
bool = zSetOperations.add("zset_key", "value0", 12); //增
bool = zSetOperations.add("zset_key", "value1", 19); //增
bool = zSetOperations.add("zset_key_1", "c", 12); //增
bool = zSetOperations.add("zset_key_1", "value1", 19); //增
longValue = zSetOperations.count("zset_key", 10,100); //计算10<=score<=100的值个数
Double doub = zSetOperations.incrementScore("zset_key", "value0", 8); //value0的score += 8
longValue = zSetOperations.intersectAndStore("zset_key", "zset_key_1", "zset_key_new"); //存储交集且score相加
objects = zSetOperations.range("zset_key", 1,100); //取1<=id<=100的值
RedisZSetCommands.Range range = new RedisZSetCommands.Range();
range.gte("a"); //大于等于10,gt(int min)大于
range.lte("z"); //小于等于100,lt(int max)小于
RedisZSetCommands.Limit limit = new RedisZSetCommands.Limit();
limit.count(20); //limit=20
//类似关系型数据库 select * from zset_key where id<100 and id>0 limit 20;
objects = zSetOperations.rangeByLex("zset_key", range, limit);
//类似关系型数据库 select * from zset_key where score<100 and score>10 and id>5 limit 20;
objects = zSetOperations.rangeByScore("zset_key", 10, 100, 1, 20);
//类似关系型数据库 select id from zset_key where value='value0';
longValue = zSetOperations.rank("zset_key", "value0");
//类似关系型数据库 select * from zset_key where id<100 and id>1 desc;
objects = zSetOperations.reverseRange("zset_key", 1, 100);
//【Map】,相当于Map<K,Map<K,V>> map
HashOperations<String,String,Object> hashOperations = redisTemplate.opsForHash();
hashOperations.put("hash_key", "map_key", "map_value1"); //增
hashOperations.putAll("hash_key", new HashMap<String, Object>(){{put("map_key_1", 2);}}); //增
longValue = hashOperations.increment("hash_key", "map_key_1", 10); //map_value += 10; map_value不是数字会报错
Map map = hashOperations.entries("hash_key"); //查,相当于 Map<K,V> subMap = map.get(hash_key);
object = hashOperations.get("hash_key", "map_key"); //查,相当于 V value = map.get(hash_key).get(map_key);
bool = redisTemplate.delete("string_key"); //删
System.out.println(object + "," + longValue + "," + doub + "," + objects + "," + map);
}
/**
* 绑定键的5种数据类型+1种扩展类型操作
*/
public void boundHandle(){
/**
* 对5种数据类型的绑定,实际是绑定key之后,对key对应的value进行持续疯狂输出,为value的操作提供方便,和上面不绑定
* 的区别是:不绑定操作每次都要指定key,非常繁琐。
*/
//【String】
BoundValueOperations<String, Object> boundValueOperations = redisTemplate.boundValueOps("string_key");
boundValueOperations.append("appendString");
boundValueOperations.set("newString");
boundValueOperations.get();
//【List】
BoundListOperations<String,Object> boundListOperations = redisTemplate.boundListOps("list_key");
boundListOperations.leftPush("value0");
//【set】
BoundSetOperations<String,Object> boundSetOperations = redisTemplate.boundSetOps("set_key");
boundListOperations.range(20, 100);
//【zset】
BoundZSetOperations boundZSetOperations = redisTemplate.boundZSetOps("zset_key");
boundListOperations.leftPushIfPresent("value1"); //如果存在就从左边取一个
//【hash】
BoundHashOperations<String,String,Object> boundHashOperations = redisTemplate.boundHashOps("hash_key");
boundHashOperations.put("map_key", "map_value");
/**
* 1种扩展类型
*/
//【BoundGeoOperations】,地理位置操作
// BoundGeoOperations<K,M>是“坐标(K)-成员(Member)”类型的操作类,boundGeoOps("key")相当于打开一个叫key的图层,这个
// 图层是“坐标(K)-成员(Member)”类型的集合
BoundGeoOperations<String,Object> boundGeoOperations = redisTemplate.boundGeoOps("CHINA:CITY:FOOD");
Point nanjing = new Point(118.803805,32.060168);
Point beijing = new Point(116.397039,39.9077);
//增加
boundGeoOperations.add(nanjing, "nanjing");
boundGeoOperations.add(nanjing, "包子?");
boundGeoOperations.add(nanjing, "还有什么吃的");
boundGeoOperations.add(beijing, "beijing");
boundGeoOperations.add(beijing, "爆肚");
boundGeoOperations.add(beijing, "涮羊肉");
boundGeoOperations.add(beijing, "豆汁");
//计算两个坐标之间的距离
Distance distance = boundGeoOperations.distance("nanjing", "beijing", Metrics.KILOMETERS);
//南京为中心,半径1000*1000M的区域的Member集合
GeoResults<RedisGeoCommands.GeoLocation<Object>> results
= boundGeoOperations.radius(new Circle(beijing, 1000*1000));
//删除
boundGeoOperations.remove("豆汁");
}
}
总结:RedisTemplate的API比Jedis的API更好掌握,使用起来也更直观方便。RedisTemplate是基于jedis的封装,因此效率自然比Jedis要低,至于低多少可以参考本文。另外,地理位置操作是Redis从命令层面提供的支持而非Spring提供的封装,它基于Haversine公式,适用于地球,最大偏差5%,不适用于标准球体,参考中文官网文档。
4 Redis的其它特性
4.1 发布订阅
这里声明一下,大数据量的订阅发布建议使用消息队列(MQ)。Redis的发布订阅在Spring的支持下用起来更顺手,Spring的相关支持在org.springframework.data.redis.listener包中。Redis的发布订阅有Redis命令支持,RedisTemplate发布订阅也是建立在此基础之上。下面是相关代码:
(1)编写发布者
@Component
public class Publisher {
@Autowired
private RedisTemplate redisTemplate;
public boolean publish(String channel, UserEntity message){
if (StringUtils.isEmpty(channel) || message == null){
return false;
}
redisTemplate.convertAndSend(channel, message);
System.out.println("我是发布者,发布了消息,channle:" + channel + "," + message.getAddress());
return true;
}
}
(2)编写订阅者
订阅者需要实现org.springframework.data.redis.connection.MessageListener接口。
@Component
@Scope("prototype")
public class Subscriber implements MessageListener {
@Autowired
private Jackson2JsonRedisSerializer serializer;
@Override
public void onMessage(Message message, @Nullable byte[] pattern) {
Object messageBody = serializer.deserialize(message.getBody());
if (messageBody instanceof UserEntity) {
UserEntity userEntity = (UserEntity) messageBody;
System.out.println("我是订阅者,收到消息:" + userEntity.getAddress());
}
}
}
(3)配置订阅者
@Component
public class ListenerConfig {
@Autowired
private Subscriber subscriberA;
@Autowired
private Subscriber subscriberB;
@Bean
public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
MessageListenerAdapter adapterA = new MessageListenerAdapter(subscriberA);
MessageListenerAdapter adapterB = new MessageListenerAdapter(subscriberB);
container.setConnectionFactory(connectionFactory);
container.addMessageListener(adapterA, new ChannelTopic("test_channel_1"));
container.addMessageListener(adapterB, new ChannelTopic("test_channel_2"));
return container;
}
}
使用RedisTemplate需要配置RedisMessageListenerContainer,RedisMessageListenerContainer有一个内部类
DispatchMessageListener与监听所有订阅的消息,RedisMessageListenerContainer负责循环通知订阅者。
RedisTemplate底层使用netty,PubSubEndPoint的notifyListeners()方法是连接netty和RedisMessageListenerContainer的桥梁。
4.2 Redis事务
(1)配置事务管理器
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) throws SQLException {
return new DataSourceTransactionManager(dataSource);
}
(2)开启RedisTemplate事务支持
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(serializer);
template.setValueSerializer(serializer);
template.setHashKeySerializer(serializer);
template.setHashValueSerializer(serializer);
//打开事务支持
template.setEnableTransactionSupport(true);
template.afterPropertiesSet();
(3)在需要事务支持的方法上标注@Transactional注解(必须Public方法)
@Transactional
public boolean transactionTest(boolean interrupt){}
5 总结
依赖Redis的几种数据类型和高吞吐量特性,Redis可被用来设计成很多有用的数据结构,例如分布式锁、消息总线等,碍于篇幅,我把分布式锁、消息总线的内容转移到下一篇文章中,敬请关注,本文的GitHub地址。