1. Windows下客户端的安装和使用
redis-desktop-manager-0.9.3.817.exe
2. redis客户端使用
基于 redis 开放的通信协议,大神们纷纷开发了各种语言的 redis 客户端,有 c、c++、java、python、php、nodeJs 等等开发语言的客户端,准确来说其实这些客户端都是基于 redis 命令做了一层封装,然后打包成工具以便大家更佳方便的操作 redis,以 Java 项目为例,使用最广的就是以下三种客户端:
2. Jedis
Jedis 是老牌的 Redis 的 Java 客户端,提供了比较全面的 Redis 命令的操作支持,也是目前使用最广泛的客户端。官网:点我跳转
2.1 基本使用
2.1.1 基本使用
首先创建一个普通的 Maven 项目,然后添加Jedis
依赖包!
1 2 3 4 5
| <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>3.9.0</version> </dependency>
|
然后创建一个简单的测试,即可实现连接!
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class JedisMain {
public static void main(String[] args) { // 1.构造一个 Jedis 对象,因为这里使用的默认端口 6379,所以不用配置端口 Jedis jedis = new Jedis("127.0.0.1", 6379); // 2.密码认证 jedis.auth("111111"); // 3.测试是否连接成功 String ping = jedis.ping(); // 4.返回 pong 表示连接成功 System.out.println(ping); } }
|
对于 Jedis 而言,一旦连接上了 Redis 服务器,剩下的操作就非常容易了,由于 Jedis 中的 API 和 Redis 的命令高度一致,所以,Jedis 中的方法见名知意,直接使用即可。
2.1.2 连接池
虽然 redis 服务端是单线程操作,但是在实际项目中,使用 Jedis 对象来操作 redis 时,每次操作都需要新建/关闭 TCP 连接,连接资源开销很高,同时 Jedis 对象的个数不受限制,在极端情况下可能会造成连接泄漏,同时 Jedis 存在多线程不安全的问题。
为什么说 Jedis 线程不安全,更加详细的原因可以访问这个地址。
所以我们需要将 Jedis 交给线程池来管理,使用 Jedis 对象时,从连接池获取 Jedis,使用完成之后,再还给连接池。
在使用之前,需要添加common-pool
线程池依赖包!
1 2 3 4 5
| <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> <version>2.11.1</version> </dependency>
|
创建一个简单的使用线程池测试用例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class JedisPoolMain {
public static void main(String[] args) { // 1. 构造一个 Jedis 连接池 JedisPool pool = new JedisPool("127.0.0.1", 6379); // 2. 从连接池中获取一个 Jedis 连接 Jedis jedis = pool.getResource(); jedis.auth("111111"); // 3. Jedis 操作 String ping = jedis.ping(); System.out.println(ping); // 4. 归还连接 jedis.close(); } }
|
2.1.3 连接池配置
在实际的使用过程中,我们常常会这样来初始化线程池JedisPool
,详细代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
| public class RedisPoolUtils {
private static JedisPool jedisPool = null;
/** * redis服务器地址 */ private static String addr = "127.0.0.1";
/** * redis服务器端口 */ private static int port = 6379;
/** * redis服务器密码 */ private static String auth = "111111";
static{ try { JedisPoolConfig config = new JedisPoolConfig(); //连接耗尽时是否阻塞, false报异常,ture阻塞直到超时, 默认true config.setBlockWhenExhausted(true); //设置的逐出策略类名, 默认DefaultEvictionPolicy(当连接超过最大空闲时间,或连接数超过最大空闲连接数) config.setEvictionPolicyClassName("org.apache.commons.pool2.impl.DefaultEvictionPolicy"); //是否启用pool的jmx管理功能, 默认true config.setJmxEnabled(true); //MBean ObjectName = new ObjectName("org.apache.commons.pool2:type=GenericObjectPool,name=" + "pool" + i); 默认为"pool", JMX不熟,具体不知道是干啥的...默认就好. config.setJmxNamePrefix("pool"); //是否启用后进先出, 默认true config.setLifo(true); //最大空闲连接数, 默认8个 config.setMaxIdle(8); //最大连接数, 默认8个 config.setMaxTotal(8); //获取连接时的最大等待毫秒数(如果设置为阻塞时BlockWhenExhausted),如果超时就抛异常, 小于零:阻塞不确定的时间, 默认-1 config.setMaxWaitMillis(-1); //逐出连接的最小空闲时间 默认1800000毫秒(30分钟) config.setMinEvictableIdleTimeMillis(1800000); //最小空闲连接数, 默认0 config.setMinIdle(0); //每次逐出检查时 逐出的最大数目 如果为负数就是 : 1/abs(n), 默认3 config.setNumTestsPerEvictionRun(3); //对象空闲多久后逐出, 当空闲时间>该值 且 空闲连接>最大空闲数 时直接逐出,不再根据MinEvictableIdleTimeMillis判断 (默认逐出策略) config.setSoftMinEvictableIdleTimeMillis(1800000); //在获取连接的时候检查有效性, 默认false config.setTestOnBorrow(false); //在空闲时检查有效性, 默认false config.setTestWhileIdle(false); //逐出扫描的时间间隔(毫秒) 如果为负数,则不运行逐出线程, 默认-1 config.setTimeBetweenEvictionRunsMillis(-1); jedisPool = new JedisPool(config, addr, port, 3000, auth); } catch (Exception e) { e.printStackTrace(); } }
/** * 获取 Jedis 资源 * @return */ public static Jedis getJedis() { if (jedisPool != null) { return jedisPool.getResource(); } return null; }
/** * 释放Jedis资源 */ public static void close(final Jedis jedis) { if (jedis != null) { jedis.close(); } }
}
|
简单测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public static void main(String[] args) throws InterruptedException { //获取 jedis 客户端 Jedis jedis = RedisPoolUtils.getJedis();
System.out.println("清空数据:"+jedis.flushDB()); System.out.println("判断某个键是否存在:"+jedis.exists("username")); System.out.println("新增<'username','xmr'>的键值对:"+jedis.set("username", "xmr")); System.out.println(jedis.exists("username")); System.out.println("新增<'password','password'>的键值对:"+jedis.set("password", "123")); System.out.print("系统中所有的键如下:"); Set<String> keys = jedis.keys("*"); System.out.println(keys); System.out.println("删除键password:"+jedis.del("password")); System.out.println("判断键password是否存在:"+jedis.exists("password")); System.out.println("设置键username的过期时间为5s:"+jedis.expire("username", 8L)); TimeUnit.SECONDS.sleep(1); System.out.println("查看键username的剩余生存时间:"+jedis.ttl("username")); System.out.println("移除键username的生存时间:"+jedis.persist("username")); System.out.println("查看键username的剩余生存时间:"+jedis.ttl("username")); System.out.println("查看键username所存储的值的类型:"+jedis.type("username")); RedisPoolUtils.close(jedis); }
|
运行结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
| 清空数据:OK 判断某个键是否存在:false 新增<'username','xmr'>的键值对:OK true 新增<'password','password'>的键值对:OK 系统中所有的键如下:[password, username] 删除键password:1 判断键password是否存在:false 设置键username的过期时间为5s:1 查看键username的剩余生存时间:7 移除键username的生存时间:1 查看键username的剩余生存时间:-1 查看键username所存储的值的类型:string
|
2.1.4 字符串常用API操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301
| public class RedisClientUtil {
private static final Logger log = LoggerFactory.getLogger(RedisClientUtil.class);
/** * 获取指定key的值,如果key不存在返回null * 返回值:返回 key 的值,如果 key 不存在时,返回 nil * @param key * @return */ public static String get(String key) { try (Jedis jedis = jedisPool.getResource()) { return jedis.get(key); } catch (Exception e){ log.error("get命令操作失败,请求参数:{}", key,e); } return null; }
/** * 设置key的值为value * 返回值:操作成功完成时返回 OK * @param key * @return */ public static String set(String key, String value) { try (Jedis jedis = jedisPool.getResource()) { return jedis.set(key, value); } catch (Exception e){ log.error("set命令操作失败,参数key:{},参数value:{}", key, value,e); } return null; }
/** * 删除指定的key,返回值:被删除 key 的数量 * 返回值:被删除 key 的数量 * @param key * @return */ public static Long del(String key) { try (Jedis jedis = jedisPool.getResource()) { Long result = jedis.del(key); return jedis.del(key); } catch (Exception e){ log.error("del命令操作失败,参数key:{}", key,e); } return 0L; }
/** * 通过key向指定的value值追加值 * 返回值:追加指定值之后, key中字符串的长度 * @param key * @return */ public static Long append(String key, String value) { try (Jedis jedis = jedisPool.getResource()) { return jedis.append(key, value); } catch (Exception e){ log.error("append命令操作失败,参数key:{},参数value:{}", key, value,e); } return 0L; }
/** * 判断key是否存在 * 返回值:true/false * @param key * @return */ public static Boolean exists(String key) { try (Jedis jedis = jedisPool.getResource()) { return jedis.exists(key); } catch (Exception e){ log.error("exists命令操作失败,参数key:{}", key,e); } return false; }
/** * 设置key的超时时间为seconds * 返回值:若 key 存在返回 1 ,否则返回 0 * @param key * @return */ public static Long expire(String key, long seconds) { try (Jedis jedis = jedisPool.getResource()) { return jedis.expire(key, seconds); } catch (Exception e){ log.error("expire命令操作失败,参数key:{},参数seconds:{}", key, seconds,e); } return 0L; }
/** * 返回 key 的剩余过期时间(单位秒) * 返回值:当 key 不存在时,返回 -2 。 当 key 存在但没有设置剩余生存时间时,返回 -1 。 否则,以秒为单位,返回 key 的剩余生存时间 * @param key * @return */ public static Long ttl(String key) { try (Jedis jedis = jedisPool.getResource()) { return jedis.ttl(key); } catch (Exception e){ log.error("ttl命令操作失败,参数key:{}", key,e); } return 0L; }
/** * 设置指定key的值为value,当key不存在时才设置 * 返回值:设置成功返回 1,设置失败返回 0 * @param key * @return */ public static Long setnx(String key, String value) { try (Jedis jedis = jedisPool.getResource()) { return jedis.setnx(key, value); } catch (Exception e){ log.error("setnx命令操作失败,参数key:{},参数value:{}", key, value,e); } return 0L; }
/** * 设置指定key的值为value,并设置过期时间 * 返回值:设置成功时返回 OK * @param key * @return */ public static String setex(String key, String value, long seconds) { try (Jedis jedis = jedisPool.getResource()) { return jedis.setex(key, seconds, value); } catch (Exception e){ log.error("setex命令操作失败,参数key:{},参数value:{}", key, value,e); } return null; }
/** * 通过key 和offset 从指定的位置开始将原先value替换 * 返回值:被修改后的字符串长度 * @param key * @return */ public static Long setrange(String key, int offset, String value) { try (Jedis jedis = jedisPool.getResource()) { return jedis.setrange(key, offset, value); } catch (Exception e){ log.error("setrange命令操作失败,参数key:{},参数value:{},参数offset:{}", key, value, offset,e); } return null; }
/** * 通过批量的key获取批量的value * 返回值:一个包含所有给定 key 的值的列表。 * @param keys * @return */ public static List<String> mget(String... keys) { try (Jedis jedis = jedisPool.getResource()) { return jedis.mget(keys); } catch (Exception e){ log.error("mget命令操作失败,参数key:{}", keys.toString(),e); } return null; }
/** * 批量的设置key:value,也可以一个 * 返回值:总是返回 OK * @param keysValues * @return */ public static String mset(String... keysValues) { try (Jedis jedis = jedisPool.getResource()) { return jedis.mset(keysValues); } catch (Exception e){ log.error("mset命令操作失败,参数key:{}", keysValues.toString(),e); } return null; }
/** * 设置key的值,并返回一个旧值 * 返回值:返回给定 key 的旧值,当 key 没有旧值时,即 key 不存在时,返回 nil * @param key * @return */ public static String getSet(String key, String value) { try (Jedis jedis = jedisPool.getResource()) { return jedis.getSet(key, value); } catch (Exception e){ log.error("getSet命令操作失败,参数key:{},参数value:{}", key, value,e); } return null; }
/** * 通过下标和 key 获取指定下标位置的 value * 返回值:截取得到的子字符串 * @param key * @return */ public static String getrange(String key, int startOffset, int endOffset) { try (Jedis jedis = jedisPool.getResource()) { return jedis.getrange(key, startOffset, endOffset); } catch (Exception e){ log.error("getrange命令操作失败,参数key:{},参数startOffset:{},参数offset:{}", key, startOffset, endOffset,e); } return null; }
/** * 通过key 对value进行加值+1操作,当value不是int类型时会返回错误,当key不存在是则value为1 * 返回值:执行INCR命令之后 key 的值 * @param key * @return */ public static Long incr(String key) { try (Jedis jedis = jedisPool.getResource()) { return jedis.incr(key); } catch (Exception e){ log.error("incr命令操作失败,参数key:{}", key, e); } return 0L; }
/** * 通过key给指定的value加值 * 返回值:执行INCR命令之后 key 的值 * @param key * @return */ public static Long incrBy(String key, long increment) { try (Jedis jedis = jedisPool.getResource()) { return jedis.incrBy(key, increment); } catch (Exception e){ log.error("incrBy命令操作失败,参数key:{},参数increment:{}", key, increment,e); } return 0L; }
/** * 对key的值做减减操作 * 返回值:执行INCR命令之后 key 的值 * @param key * @return */ public static Long decr(String key) { try (Jedis jedis = jedisPool.getResource()) { return jedis.decr(key); } catch (Exception e){ log.error("decr命令操作失败,参数key:{}", key, e); } return 0L; }
/** * 对key的值做减减操作,减去指定的值 * 返回值:执行INCR命令之后 key 的值 * @param key * @return */ public static Long decrBy(String key, long decrement) { try (Jedis jedis = jedisPool.getResource()) { return jedis.decrBy(key, decrement); } catch (Exception e){ log.error("decrBy命令操作失败,参数key:{},参数decrement:{}", key, decrement,e); } return 0L; }
/** * 通过key获取value值的长度 * 返回值:value值的长度 * @param key * @return */ public static Long strlen(String key) { try (Jedis jedis = jedisPool.getResource()) { return jedis.strlen(key); } catch (Exception e){ log.error("strlen命令操作失败,参数key:{}", key, e); } return 0L; } }
|
2.1.5 哈希常用API操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
| public class RedisClientUtil {
private static final Logger log = LoggerFactory.getLogger(RedisClientUtil.class);
/** * 通过key 和 field 获取指定的 value * 返回值:对应的value值 * @param key * @return */ public static String hget(String key, String field) { try (Jedis jedis = jedisPool.getResource()) { return jedis.hget(key, field); } catch (Exception e){ log.error("hget命令操作失败,参数key:{},参数field:{}", key, field,e); } return null; }
/** * 通过key给field设置指定的值,如果key不存在,则先创建 * 返回值:如果字段是哈希表中的一个新建字段,并且值设置成功,返回 1 ;如果哈希表中域字段已经存在且旧值已被新值覆盖,返回 0 。 * @param key * @return */ public static Long hset(String key, String field, String value) { try (Jedis jedis = jedisPool.getResource()) { return jedis.hset(key, field, value); } catch (Exception e){ log.error("hset命令操作失败,参数key:{},参数field:{},参数value:{}", key, field, value,e); } return 0L; }
/** * 通过key和field判断是否有指定的value存在 * 返回值:true/false * @param key * @return */ public static Boolean hexists(String key, String field) { try (Jedis jedis = jedisPool.getResource()) { return jedis.hexists(key, field); } catch (Exception e){ log.error("hexists命令操作失败,参数key:{},参数field:{}", key, field,e); } return false; }
/** * 通过key返回field的数量 * 返回值:field的数量 * @param key * @return */ public static Long hlen(String key) { try (Jedis jedis = jedisPool.getResource()) { return jedis.hlen(key); } catch (Exception e){ log.error("hlen命令操作失败,参数key:{}", key,e); } return 0L; }
/** * 通过key 删除指定的 field * 返回值:删除的数量 * @param key * @return */ public static Long hdel(String key, String... fields) { try (Jedis jedis = jedisPool.getResource()) { return jedis.hdel(key, fields); } catch (Exception e){ log.error("hdel命令操作失败,参数key:{},参数fields:{}", key, fields.toString(),e); } return 0L; }
/** * 通过key返回所有的field * 返回值:field集合 * @param key * @return */ public static Set<String> hkeys(String key) { try (Jedis jedis = jedisPool.getResource()) { return jedis.hkeys(key); } catch (Exception e){ log.error("hkeys命令操作失败,参数key:{}", key,e); } return null; }
/** * 通过key获取所有的field和value * 返回值:map对象 * @param key * @return */ public static Map<String, String> hgetAll(String key) { try (Jedis jedis = jedisPool.getResource()) { return jedis.hgetAll(key); } catch (Exception e){ log.error("hgetAll命令操作失败,参数key:{}", key,e); } return null; } }
|
2.1.6 列表常用API操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164
| public class RedisClientUtil {
private static final Logger log = LoggerFactory.getLogger(RedisClientUtil.class);
/** * 过key向list头部添加字符串 * 返回值:执行 LPUSH 命令后,列表的长度 * @param key * @return */ public static Long lpush(String key, String... strs) { try (Jedis jedis = jedisPool.getResource()) { return jedis.lpush(key, strs); } catch (Exception e){ log.error("lpush命令操作失败,参数key:{},参数strs:{}", key, strs.toString(),e); } return null; }
/** * 通过key向list尾部添加字符串 * 返回值:执行 RPUSH 命令后,列表的长度 * @param key * @return */ public static Long rpush(String key, String... strs) { try (Jedis jedis = jedisPool.getResource()) { return jedis.rpush(key, strs); } catch (Exception e){ log.error("rpush命令操作失败,参数key:{},参数strs:{}", key, strs.toString(),e); } return null; }
/** * 通过key设置list指定下标位置的value 如果下标超过list里面value的个数则报错 * 返回值:操作成功返回 ok ,否则返回错误信息 * @param key * @return */ public static String lset(String key, Long index, String value) { try (Jedis jedis = jedisPool.getResource()) { return jedis.lset(key, index, value); } catch (Exception e){ log.error("lset命令操作失败,参数key:{},参数index:{},参数value:{}", key, index, value,e); } return null; }
/** * 通过key从对应的list中删除指定的count个 和 value相同的元素 * 返回值:返回被删除的个数 * @param key * @return */ public static Long lrem(String key, long count, String value) { try (Jedis jedis = jedisPool.getResource()) { return jedis.lrem(key, count, value); } catch (Exception e){ log.error("lrem命令操作失败,参数key:{},参数count:{},参数value:{}", key, count, value,e); } return null; }
/** * 通过key保留list中从strat下标开始到end下标结束的value值 * 返回值:操作成功返回 ok ,否则返回错误信息 * @param key * @return */ public static String ltrim(String key, long start, long end) { try (Jedis jedis = jedisPool.getResource()) { return jedis.ltrim(key, start, end); } catch (Exception e){ log.error("ltrim命令操作失败,参数key:{},参数start:{},参数end:{}", key, start, end,e); } return null; }
/** * 通过key从list的头部删除一个value,并返回该value * 返回值:value值 * @param key * @return */ public static String lpop(String key) { try (Jedis jedis = jedisPool.getResource()) { return jedis.lpop(key); } catch (Exception e){ log.error("lpop命令操作失败,参数key:{}", key,e); } return null; }
/** * 通过key从list尾部删除一个value,并返回该元素 * 返回值:value值 * @param key * @return */ public static String rpop(String key) { try (Jedis jedis = jedisPool.getResource()) { return jedis.rpop(key); } catch (Exception e){ log.error("rpop命令操作失败,参数key:{}", key,e); } return null; }
/** * 通过key获取list中指定下标位置的value * 返回值:value值 * @param key * @return */ public static String lindex(String key, long index){ try (Jedis jedis = jedisPool.getResource()) { return jedis.lindex(key, index); } catch (Exception e){ log.error("lindex命令操作失败,参数key:{},参数index:{}", key, index,e); } return null; }
/** * 通过key返回list的长度 * 返回值:value值 * @param key * @return */ public static Long llen(String key) { try (Jedis jedis = jedisPool.getResource()) { return jedis.llen(key); } catch (Exception e){ log.error("llen命令操作失败,参数key:{}", key,e); } return null; }
/** * 通过key获取list指定下标位置的value 如果start 为 0 end 为 -1 则返回全部的list中的value * 返回值:value值 * @param key * @return */ public static List<String> lrange(String key, long start, long end) { try (Jedis jedis = jedisPool.getResource()) { return jedis.lrange(key, start, end); } catch (Exception e){ log.error("lrange命令操作失败,参数key:{},参数start:{},参数end:{}", key, start, end,e); } return null; }
}
|
2.1.7 集合常用API操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
| public class RedisClientUtil {
private static final Logger log = LoggerFactory.getLogger(RedisClientUtil.class);
/** * 通过key向指定的set中添加value * 返回值:添加成功的个数 * @param key * @return */ public static Long sadd(String key, String... members) { try (Jedis jedis = jedisPool.getResource()) { return jedis.sadd(key, members); } catch (Exception e){ log.error("sadd命令操作失败,参数key:{},参数members:{}", key, members.toString(),e); } return null; }
/** * 通过key删除set中对应的value值 * 返回值:删除成功的个数 * @param key * @return */ public static Long srem(String key, String... members) { try (Jedis jedis = jedisPool.getResource()) { return jedis.srem(key, members); } catch (Exception e){ log.error("srem命令操作失败,参数key:{},参数members:{}", key, members.toString(),e); } return null; }
/** * 通过key获取set中value的个数 * 返回值:value的个数 * @param key * @return */ public static Long scard(String key) { try (Jedis jedis = jedisPool.getResource()) { return jedis.scard(key); } catch (Exception e){ log.error("scard命令操作失败,参数key:{}", key,e); } return 0L; }
/** * 通过key判断value是否是set中的元素 * 返回值:true/false * @param key * @return */ public static Boolean sismember(String key, String member) { try (Jedis jedis = jedisPool.getResource()) { return jedis.sismember(key, member); } catch (Exception e){ log.error("sismember命令操作失败,参数key:{},参数member:{}", key, member,e); } return false; }
/** * 通过key获取set中所有的value * 返回值:所有的value * @param key * @return */ public static Set<String> smembers(String key) { try (Jedis jedis = jedisPool.getResource()) { return jedis.smembers(key); } catch (Exception e){ log.error("smembers命令操作失败,参数key:{}", key,e); } return null; }
}
|
2.1.8 有序集合常用API操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145
| public class RedisClientUtil {
private static final Logger log = LoggerFactory.getLogger(RedisClientUtil.class);
/** * 通过key向zset中添加value,score,其中score就是用来排序的 如果该value已经存在则根据score更新元素 * 返回值:被成功添加的新成员的数量,不包括那些被更新的、已经存在的成员 * @param key * @return */ public static Long zadd(String key, double score, String member) { try (Jedis jedis = jedisPool.getResource()) { return jedis.zadd(key, score, member); } catch (Exception e){ log.error("zadd命令操作失败,参数key:{},参数score:{},参数member:{}", key, score, member,e); } return null; }
/** * 通过key删除在zset中指定的value * 返回值:删除个数 * @param key * @return */ public static Long zrem(String key, String... members) { try (Jedis jedis = jedisPool.getResource()) { return jedis.zrem(key, members); } catch (Exception e){ log.error("zrem命令操作失败,参数key:{},参数members:{}", key, members.toString(),e); } return null; }
/** * 通过key增加该zset中value的score的值 * 返回值:member 成员的新分数值 * @param key * @return */ public static Double zincrby(String key, double score, String member) { try (Jedis jedis = jedisPool.getResource()) { return jedis.zincrby(key, score, member); } catch (Exception e){ log.error("zincrby命令操作失败,参数key:{},参数score:{},参数member:{}", key, score, member,e); } return null; }
/** * 通过key返回zset中value的排名 下标从小到大排序 * 返回值:返回 member 的排名 * @param key * @return */ public static Long zrank(String key, String member) { try (Jedis jedis = jedisPool.getResource()) { return jedis.zrank(key, member); } catch (Exception e){ log.error("zrank命令操作失败,参数key:{},参数member:{}", key, member,e); } return null; }
/** * 通过key将获取score从start到end中zset的value socre从大到小排序 当start为0 end为-1时返回全部 * 返回值:返回 member 集合 * @param key * @return */ public static Set<String> zrevrange(String key, long start, long end) { try (Jedis jedis = jedisPool.getResource()) { return jedis.zrevrange(key, start, end); } catch (Exception e){ log.error("zrevrange命令操作失败,参数key:{},参数start:{},参数end:{}", key, start, end,e); } return null; }
/** * 返回指定区间内zset中value的数量 * 返回值:返回 member 集合 * @param key * @return */ public static Long zcount(String key, String min, String max) { try (Jedis jedis = jedisPool.getResource()) { return jedis.zcount(key, min, max); } catch (Exception e){ log.error("zcount命令操作失败,参数key:{},参数min:{},参数max:{}", key, min, max,e); } return null; }
/** * 通过key返回zset中的value个数 * 返回值:返回 member 集合 * @param key * @return */ public static Long zcard(String key) { try (Jedis jedis = jedisPool.getResource()) { return jedis.zcard(key); } catch (Exception e){ log.error("zcard命令操作失败,参数key:{}", key,e); } return null; }
/** * 返回满足pattern表达式的所有key keys(*) 返回所有的key * 返回值:返回 key 集合 * @param pattern * @return */ public static Set<String> keys(String pattern) { try (Jedis jedis = jedisPool.getResource()) { return jedis.keys(pattern); } catch (Exception e){ log.error("keys命令操作失败,参数pattern:{}", pattern,e); } return null; }
/** * 通过key判断值得类型 * 返回值:值的类型 * @param key * @return */ public static String type(String key) { try (Jedis jedis = jedisPool.getResource()) { return jedis.type(key); } catch (Exception e){ log.error("type命令操作失败,参数key:{}", key,e); } return null; }
}
|
2.2 集群配置
在实际的项目生产环境中,redis 通常不是以单台服务实例来运行的,因为一旦服务器挂了,可能所有的下游服务都会受到影响,因此为了保障单台服务器即使出现故障也能运行,通常运维组会搭建集群环境,来保证服务高可用。
搭建的方式有两种,哨兵模式和 Cluster 模式。
- 哨兵模式:对redis服务器进行监控,如果有宕机的,就从备机里面选一个出来作为主机,实现自动切换
- Cluster 模式:将数据进行分片存储,避免全部节点数据都一样,浪费空间
2.2.1 哨兵模式
哨兵模式简单的说,就是一台主机,一台备机,外加一台监控服务,当监控服务观测到主机已经宕机,就会将备用机切换成主机,以便继续提供服务。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| public class RedisPoolUtils {
private static Jedis jedis;
private static JedisSentinelPool jedisSentinelPool;
static{ try { JedisPoolConfig config = new JedisPoolConfig(); //最大空闲连接数, 默认8个 config.setMaxIdle(8); //最大连接数, 默认8个 config.setMaxTotal(8); //最小空闲连接数, 默认0 config.setMinIdle(0); //获取连接时的最大等待毫秒数(如果设置为阻塞时BlockWhenExhausted),如果超时就抛异常, 小于零:阻塞不确定的时间, 默认-1 config.setMaxWaitMillis(3000); //在获取连接的时候检查有效性,表示取出的redis对象可用, 默认false config.setTestOnBorrow(true);
//redis服务器列表 Set<String> sentinels = new HashSet<>(); sentinels.add(new HostAndPort("192.168.43.212", 26379).toString()); sentinels.add(new HostAndPort("192.168.43.213", 26379).toString()); sentinels.add(new HostAndPort("192.168.43.214", 26379).toString());
//初始化连接池 jedisSentinelPool = new JedisSentinelPool("mymaster", sentinels, config, "111111"); // 从池中获取一个Jedis对象 jedis = jedisSentinelPool.getResource(); } catch (Exception e) { e.printStackTrace(); } } }
|
2.2.2 集群模式
为了保证高可用,redis-cluster集群通常会引入主从复制模型,一个主节点对应一个或者多个从节点,当主节点宕机的时候,就会启用从节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| public class RedisPoolUtils {
static{ try { JedisPoolConfig config = new JedisPoolConfig(); //最大空闲连接数, 默认8个 config.setMaxIdle(8); //最大连接数, 默认8个 config.setMaxTotal(8); //最小空闲连接数, 默认0 config.setMinIdle(0); //获取连接时的最大等待毫秒数(如果设置为阻塞时BlockWhenExhausted),如果超时就抛异常, 小于零:阻塞不确定的时间, 默认-1 config.setMaxWaitMillis(3000); //在获取连接的时候检查有效性,表示取出的redis对象可用, 默认false config.setTestOnBorrow(true);
Set<HostAndPort> nodes = new HashSet<>(); nodes.add(new HostAndPort("192.168.43.212", 26379)); nodes.add(new HostAndPort("192.168.43.213", 26379)); nodes.add(new HostAndPort("192.168.43.214", 26379));
JedisCluster jedisCluster = new JedisCluster(nodes, config); jedisCluster.set("key", "hello world");
jedisCluster.close(); } catch (Exception e) { e.printStackTrace(); } } }
|
2.3 优缺点
jedis
客户端是目前使用最广泛的一款 java 客户端,也是老牌的 Redis 的 Java 实现客户端。
优点很突出:
- 比较全面的提供了 Redis 的操作特性,也就是说你能用 redis 命令操作的,Jedis 包都也给你封装好了,直接使用即可
- 使用广泛,易上手
当然,缺点也有:
- Jedis 客户端实例不是线程安全的,需要借助连接池来管理和使用 Jedis
- 使用阻塞的I/O,且其方法调用都是同步的,程序流需要等到 sockets 处理完 I/O 才能执行,不支持异步
3. Lettuce
Lettuce 是Redis的一款高级 Java 客户端,与 Jedis 并列成为最热门的客户端之一,目前已成为 SpringBoot 2.0 版本默认的 redis 客户端。
相比老牌 Jedis,Lettuce 属于后起之秀,不仅功能丰富,而且提供了很多新的功能特性,比如异步操作、响应式编程等等,同时还解决了 Jedis 中线程不安全的问题。官网:点我跳转
3.1 基本使用
首先,创建一个 maven 项目,引入lettuce-core
包,就可以使用了。
1 2 3 4 5
| <dependency> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId> <version>5.3.1.RELEASE</version> </dependency>
|
使用 lettuce 连接 redis,测试是否能正常联通!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class LettuceMain {
public static void main(String[] args) { RedisURI redisUri = RedisURI.builder() .withHost("127.0.0.1") .withPort(6379) .withPassword("111111") .withTimeout(Duration.of(10, ChronoUnit.SECONDS)) .build(); RedisClient redisClient = RedisClient.create(redisUri); StatefulRedisConnection<String, String> connection = redisClient.connect(); RedisCommands<String, String> commands = connection.sync(); System.out.println(commands.ping()); connection.close(); redisClient.shutdown(); } }
|
3.1.1 同步操作
基本上只要是 Jedis 支持的同步命令操作,Lettuce 都支持。
下面,我们以同步操作字符串为例,Lettuce 的 api 操作如下!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| public class LettuceSyncMain {
public static void main(String[] args) { RedisURI redisUri = RedisURI.builder() .withHost("127.0.0.1").withPort(6379).withPassword("111111") .withTimeout(Duration.of(10, ChronoUnit.SECONDS)) .build(); RedisClient redisClient = RedisClient.create(redisUri); StatefulRedisConnection<String, String> connection = redisClient.connect(); //获取同步操作命令工具 RedisCommands<String, String> commands = connection.sync();
System.out.println("清空数据:"+commands.flushdb()); System.out.println("判断某个键是否存在:"+commands.exists("username")); System.out.println("新增<'username','xmr'>的键值对:"+commands.set("username", "xmr")); System.out.println("新增<'password','password'>的键值对:"+commands.set("password", "123")); System.out.println("获取<'password'>键的值:"+commands.get("password")); System.out.println("系统中所有的键如下:" + commands.keys("*")); System.out.println("删除键password:"+commands.del("password")); System.out.println("判断键password是否存在:"+commands.exists("password")); System.out.println("设置键username的过期时间为5s:"+commands.expire("username", 5L)); System.out.println("查看键username的剩余生存时间:"+commands.ttl("username")); System.out.println("移除键username的生存时间:"+commands.persist("username")); System.out.println("查看键username的剩余生存时间:"+commands.ttl("username")); System.out.println("查看键username所存储的值的类型:"+commands.type("username"));
connection.close(); redisClient.shutdown(); } }
|
3.1.2 异步操作
除此之外,Lettuce 还支持异步操作,将上面的操作改成异步处理,结果如下!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| public class LettuceASyncMain {
public static void main(String[] args) throws Exception { RedisURI redisUri = RedisURI.builder() .withHost("127.0.0.1").withPort(6379).withPassword("111111") .withTimeout(Duration.of(10, ChronoUnit.SECONDS)) .build(); RedisClient redisClient = RedisClient.create(redisUri); StatefulRedisConnection<String, String> connection = redisClient.connect(); //获取异步操作命令工具 RedisAsyncCommands<String, String> commands = connection.async();
System.out.println("清空数据:"+commands.flushdb().get()); System.out.println("判断某个键是否存在:"+commands.exists("username").get()); System.out.println("新增<'username','xmr'>的键值对:"+commands.set("username", "xmr").get()); System.out.println("新增<'password','password'>的键值对:"+commands.set("password", "123").get()); System.out.println("获取<'password'>键的值:"+commands.get("password").get()); System.out.println("系统中所有的键如下:" + commands.keys("*").get()); System.out.println("删除键password:"+commands.del("password").get()); System.out.println("判断键password是否存在:"+commands.exists("password").get()); System.out.println("设置键username的过期时间为5s:"+commands.expire("username", 5L).get()); System.out.println("查看键username的剩余生存时间:"+commands.ttl("username").get()); System.out.println("移除键username的生存时间:"+commands.persist("username").get()); System.out.println("查看键username的剩余生存时间:"+commands.ttl("username").get()); System.out.println("查看键username所存储的值的类型:"+commands.type("username").get());
connection.close(); redisClient.shutdown(); } }
|
3.1.3 响应式编程
Lettuce 除了支持异步编程以外,还支持响应式编程,Lettuce 引入的响应式编程框架是Project Reactor
,如果没有响应式编程经验可以先自行了解一下,访问地址https://projectreactor.io/
。
响应式编程使用案例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| public class LettuceMain {
public static void main(String[] args) throws Exception { RedisURI redisUri = RedisURI.builder() .withHost("127.0.0.1").withPort(6379).withPassword("111111") .withTimeout(Duration.of(10, ChronoUnit.SECONDS)) .build(); RedisClient redisClient = RedisClient.create(redisUri); StatefulRedisConnection<String, String> connection = redisClient.connect(); //获取响应式API操作命令工具 RedisReactiveCommands<String, String> commands = connection.reactive();
Mono<String> setc = commands.set("name", "mayun"); System.out.println(setc.block()); Mono<String> getc = commands.get("name"); getc.subscribe(System.out::println); Flux<String> keys = commands.keys("*"); keys.subscribe(System.out::println);
//开启一个事务,先把count设置为1,再将count自增1 commands.multi().doOnSuccess(r -> { commands.set("count", "1").doOnNext(value -> System.out.println("count1:" + value)).subscribe(); commands.incr("count").doOnNext(value -> System.out.println("count2:" + value)).subscribe(); }).flatMap(s -> commands.exec()) .doOnNext(transactionResult -> System.out.println("transactionResult:" + transactionResult.wasDiscarded())).subscribe();
Thread.sleep(1000 * 5); connection.close(); redisClient.shutdown(); } }
|
3.1.4 发布和订阅
Lettuce 还支持 redis 的消息发布和订阅,具体实现案例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| public class LettuceReactiveMain1 {
public static void main(String[] args) throws Exception { RedisURI redisUri = RedisURI.builder() .withHost("127.0.0.1").withPort(6379).withPassword("111111") .withTimeout(Duration.of(10, ChronoUnit.SECONDS)) .build(); RedisClient redisClient = RedisClient.create(redisUri); //获取发布订阅操作命令工具 StatefulRedisPubSubConnection<String, String> pubsubConn = redisClient.connectPubSub(); pubsubConn.addListener(new RedisPubSubListener<String, String>() { @Override public void unsubscribed(String channel, long count) { System.out.println("[unsubscribed]" + channel); } @Override public void subscribed(String channel, long count) { System.out.println("[subscribed]" + channel); } @Override public void punsubscribed(String pattern, long count) { System.out.println("[punsubscribed]" + pattern); } @Override public void psubscribed(String pattern, long count) { System.out.println("[psubscribed]" + pattern); } @Override public void message(String pattern, String channel, String message) { System.out.println("[message]" + pattern + " -> " + channel + " -> " + message); } @Override public void message(String channel, String message) { System.out.println("[message]" + channel + " -> " + message); } }); RedisPubSubAsyncCommands<String, String> pubsubCmd = pubsubConn.async(); pubsubCmd.psubscribe("CH"); pubsubCmd.psubscribe("CH2"); pubsubCmd.unsubscribe("CH");
Thread.sleep(100 * 5); pubsubConn.close(); redisClient.shutdown(); } }
|
3.1.5 客户端资源与参数配置
Lettuce 客户端的通信框架集成了 Netty 的非阻塞 IO 操作,客户端资源的设置与 Lettuce 的性能、并发和事件处理紧密相关,如果不是特别熟悉客户端参数配置,不建议在没有经验的前提下凭直觉修改默认值,保持默认配置就行。
非集群环境下,具体的配置案例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| public class LettuceMain {
public static void main(String[] args) throws Exception { ClientResources resources = DefaultClientResources.builder() .ioThreadPoolSize(4) //I/O线程数 .computationThreadPoolSize(4) //任务线程数 .build(); RedisURI redisUri = RedisURI.builder() .withHost("127.0.0.1").withPort(6379).withPassword("111111") .withTimeout(Duration.of(10, ChronoUnit.SECONDS)) .build(); ClientOptions options = ClientOptions.builder() .autoReconnect(true)//是否自动重连 .pingBeforeActivateConnection(true)//连接激活之前是否执行PING命令 .build(); RedisClient client = RedisClient.create(resources, redisUri); client.setOptions(options); StatefulRedisConnection<String, String> connection = client.connect(); RedisCommands<String, String> commands = connection.sync(); commands.set("name", "关羽"); System.out.println(commands.get("name"));
connection.close(); client.shutdown(); resources.shutdown(); } }
|
集群环境下,具体的配置案例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| public class LettuceMain {
public static void main(String[] args) throws Exception { ClientResources resources = DefaultClientResources.builder() .ioThreadPoolSize(4) //I/O线程数 .computationThreadPoolSize(4) //任务线程数 .build(); RedisURI redisUri = RedisURI.builder() .withHost("127.0.0.1").withPort(6379).withPassword("111111") .withTimeout(Duration.of(10, ChronoUnit.SECONDS)) .build(); ClusterClientOptions options = ClusterClientOptions.builder() .autoReconnect(true)//是否自动重连 .pingBeforeActivateConnection(true)//连接激活之前是否执行PING命令 .validateClusterNodeMembership(true)//是否校验集群节点的成员关系 .build(); RedisClusterClient client = RedisClusterClient.create(resources, redisUri); client.setOptions(options); StatefulRedisClusterConnection<String, String> connection = client.connect(); RedisAdvancedClusterCommands<String, String> commands = connection.sync(); commands.set("name", "张飞"); System.out.println(commands.get("name"));
connection.close(); client.shutdown(); resources.shutdown(); } }
|
3.1.6 线程池配置
Lettuce 连接设计的时候,就是线程安全的,所以一个连接可以被多个线程共享,同时 lettuce 连接默认是自动重连的,使用单连接基本可以满足业务需求,大多数情况下不需要配置连接池,多连接并不会给操作带来性能上的提升。
但在某些特殊场景下,比如事物操作,使用连接池会是一个比较好的方案,那么如何配置线程池呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| public class LettuceMain {
public static void main(String[] args) throws Exception { RedisURI redisUri = RedisURI.builder() .withHost("127.0.0.1") .withPort(6379) .withPassword("111111") .withTimeout(Duration.of(10, ChronoUnit.SECONDS)) .build(); RedisClient client = RedisClient.create(redisUri); //连接池配置 GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig(); poolConfig.setMaxIdle(2);
GenericObjectPool<StatefulRedisConnection<String, String>> pool = ConnectionPoolSupport.createGenericObjectPool(client::connect, poolConfig); StatefulRedisConnection<String, String> connection = pool.borrowObject(); RedisCommands<String, String> commands = connection.sync(); commands.set("name", "张飞"); System.out.println(commands.get("name"));
connection.close(); pool.close(); client.shutdown(); } }
|
3.2 集群配置
3.2.1 主从模式配置
redis 一般采用主从复制模式,搭建高可用的架构,简单的说就一个主节点,多个从节点,自动从主节点同步最新数据。
Lettuce 支持自动发现主从模式下的节点信息,然后保存到本地,具体配置如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public class LettuceMain {
public static void main(String[] args) throws Exception { //这里只需要配置一个节点的连接信息,不一定需要是主节点的信息,从节点也可以;可以自动发现主从节点 RedisURI uri = RedisURI.builder().withHost("192.168.31.111").withPort(6379).withPassword("123456").build(); RedisClient client = RedisClient.create(uri); StatefulRedisMasterReplicaConnection<String, String> connection = MasterReplica.connect(client, StringCodec.UTF8, uri); //从节点读取数据 connection.setReadFrom(ReadFrom.REPLICA);
RedisCommands<String, String> commands = connection.sync(); commands.set("name", "张飞"); System.out.println(commands.get("name"));
connection.close(); client.shutdown(); } }
|
当然我们也可以手动指定集群节点来加载,具体配置如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public class LettuceMain {
public static void main(String[] args) throws Exception { //集群节点 List<RedisURI> uris = new ArrayList(); uris.add(RedisURI.builder().withHost("192.168.31.111").withPort(6379).withPassword("111111").build()); uris.add(RedisURI.builder().withHost("192.168.31.112").withPort(6379).withPassword("111111").build()); uris.add(RedisURI.builder().withHost("192.168.31.113").withPort(6379).withPassword("111111").build());
RedisClient client = RedisClient.create(); StatefulRedisMasterReplicaConnection<String, String> connection = MasterReplica.connect(client, StringCodec.UTF8, uris); //从节点读取数据 connection.setReadFrom(ReadFrom.REPLICA);
RedisCommands<String, String> commands = connection.sync(); commands.set("name", "张飞"); System.out.println(commands.get("name"));
connection.close(); client.shutdown(); } }
|
3.2.2 哨兵模式配置
哨兵模式,也是 redis 实现服务高可用的一大亮点,具体配置实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public class LettuceMain {
public static void main(String[] args) throws Exception { //集群节点 List<RedisURI> uris = new ArrayList(); uris.add(RedisURI.builder().withSentinel("192.168.31.111", 26379).withSentinelMasterId("mymaster").withPassword("123456").build()); uris.add(RedisURI.builder().withSentinel("192.168.31.112", 26379).withSentinelMasterId("mymaster").withPassword("123456").build()); uris.add(RedisURI.builder().withSentinel("192.168.31.113", 26379).withSentinelMasterId("mymaster").withPassword("123456").build());
RedisClient client = RedisClient.create(); StatefulRedisMasterReplicaConnection<String, String> connection = MasterReplica.connect(client, StringCodec.UTF8, uris); //从节点读取数据 connection.setReadFrom(ReadFrom.REPLICA);
RedisCommands<String, String> commands = connection.sync(); commands.set("name", "赵云"); System.out.println(commands.get("name"));
connection.close(); client.shutdown(); } }
|
3.2.3 Cluster 集群模式配置
Cluster 集群模式,是之后推出的一种高可用的架构模型,主要是采用分片方式来存储数据,具体配置如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| public class LettuceReactiveMain4 {
public static void main(String[] args) throws Exception { Set<RedisURI> uris = new HashSet<>(); uris.add(RedisURI.builder().withHost("192.168.31.111").withPort(7000).withPassword("123456").build()); uris.add(RedisURI.builder().withHost("192.168.31.112").withPort(7000).withPassword("123456").build()); uris.add(RedisURI.builder().withHost("192.168.31.113").withPort(7000).withPassword("123456").build()); uris.add(RedisURI.builder().withHost("192.168.31.114").withPort(7000).withPassword("123456").build()); uris.add(RedisURI.builder().withHost("192.168.31.115").withPort(7000).withPassword("123456").build()); uris.add(RedisURI.builder().withHost("192.168.31.116").withPort(7001).withPassword("123456").build());
RedisClusterClient client = RedisClusterClient.create(uris); StatefulRedisClusterConnection<String, String> connection = client.connect(); RedisAdvancedClusterCommands<String, String> commands = connection.sync(); commands.set("name", "关羽"); System.out.println(commands.get("name"));
//选择从节点,只读 NodeSelection<String, String> replicas = commands.replicas(); NodeSelectionCommands<String, String> nodeSelectionCommands = replicas.commands(); Executions<List<String>> keys = nodeSelectionCommands.keys("*"); keys.forEach(key -> System.out.println(key));
connection.close(); client.shutdown(); } }
|
3.3 优缺点
Lettuce 相比老牌的 Jedis 客户端,功能更加强大,不仅解决了线程安全的问题,还支持异步和响应式编程,支持集群,Sentinel,管道和编码器等等功能。
4. Redission
什么是 Redisson?来自于官网上的描述内容如下!
Redisson 是一个在 Redis 的基础上实现的 Java 驻内存数据网格客户端(In-Memory Data Grid)。它不仅提供了一系列的 redis 常用数据结构命令服务,还提供了许多分布式服务,例如分布式锁、分布式对象、分布式集合、分布式远程服务、分布式调度任务服务等等。
相比于 Jedis、Lettuce 等基于 redis 命令封装的客户端,Redisson 提供的功能更加高端和抽象,逼格高!
官网:点我跳转
4.1 基本使用
跟过去一样,首先创建一个 maven 项目,添加Redisson
依赖包。
1 2 3 4 5
| <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.13.6</version> </dependency>
|
单机环境下,简单样例如下!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class RedissonMain {
public static void main(String[] args) { Config config = new Config(); config.useSingleServer() .setAddress("redis://127.0.0.1:6379") .setPassword("123456") .setDatabase(0); //获取客户端 RedissonClient redissonClient = Redisson.create(config); //获取所有的key redissonClient.getKeys().getKeys().forEach(key -> System.out.println(key)); //关闭客户端 redissonClient.shutdown(); } }
|
4.1.1 字符串操作
Redisson 支持通过RBucket
对象来操作字符串数据结构,通过RBucket
实例可以设置value
或设置value
和有效期,简单样例如下!
1 2 3 4 5 6
| RBucket<String> rBucket = redissonClient.getBucket("strKey");
rBucket.set("张三", 30, TimeUnit.SECONDS);
System.out.println(redissonClient.getBucket("strKey").get());
|
4.1.2 对象操作
Redisson 支持将对象作为value
存入redis
,被存储的对象事先必须要实现序列化接口Serializable
,否则会报错,简单样例如下!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class Student implements Serializable {
private Long id;
private String name;
private Integer age;
@Override public String toString() { return "Student{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + '}'; } }
|
1 2 3 4 5 6 7 8 9 10 11 12
| Student student = new Student(); student.setId(1L); student.setName("张三"); student.setAge(18);
RBucket<Student> rBucket = redissonClient.getBucket("objKey");
rBucket.set(student, 30, TimeUnit.SECONDS);
System.out.println(redissonClient.getBucket("objKey").get());
|
4.1.3 哈希操作
Redisson 支持通过RMap
对象来操作哈希数据结构,简单样例如下!
1 2 3 4 5 6 7 8 9 10 11
| RMap<String, String> rMap = redissonClient.getMap("mapkey");
rMap.put("id", "123"); rMap.put("name", "赵四"); rMap.put("age", "50");
rMap.expire(30, TimeUnit.SECONDS);
System.out.println(redissonClient.getMap("mapkey").get("name"));
|
4.1.4 列表操作
Redisson 支持通过RList
对象来操作列表数据结构,简单样例如下!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| RList<Student> rList = redissonClient.getList("listkey");
Student student1 = new Student(); student1.setId(1L); student1.setName("张三"); student1.setAge(18); rList.add(student1);
Student student2 = new Student(); student2.setId(2L); student2.setName("李四"); student2.setAge(19); rList.add(student2);
rList.expire(30, TimeUnit.SECONDS);
System.out.println(redissonClient.getList("listkey"));
|
4.1.5 集合操作
Redisson 支持通过RSet
对象来操作集合数据结构,简单样例如下!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| RSet<Student> rSet = redissonClient.getSet("setkey");
Student student1 = new Student(); student1.setId(1L); student1.setName("张三"); student1.setAge(18); rSet.add(student1);
Student student2 = new Student(); student2.setId(2L); student2.setName("李四"); student2.setAge(19); rSet.add(student2);
rSet.expire(30, TimeUnit.SECONDS);
System.out.println(redissonClient.getSet("setkey"));
|
4.1.6 有序集合操作
Redisson 支持通过RSortedSet
对象来操作有序集合数据结构,在使用对象来存储之前,实体对象必须先实现Comparable
接口,并重写比较逻辑,否则会报错,简单样例如下!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public class Student implements Serializable, Comparable<Student> {
private Long id;
private String name;
private Integer age;
@Override public String toString() { return "Student{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + '}'; }
@Override public int compareTo(Student obj) { return this.getId().compareTo(obj.getId()); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| RSortedSet<Student> sortSetkey = redissonClient.getSortedSet("sortSetkey");
Student student1 = new Student(); student1.setId(1L); student1.setName("张三"); student1.setAge(18); sortSetkey.add(student1);
Student student2 = new Student(); student2.setId(2L); student2.setName("李四"); student2.setAge(19); sortSetkey.add(student2);
System.out.println(redissonClient.getSortedSet("sortSetkey"));
|
4.1.7 布隆过滤器
布隆过滤器(Bloom Filter)是 1970 年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。
布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。
Redisson 支持通过RBloomFilter
对象来操作布隆过滤器,简单样例如下!
1 2 3 4 5 6 7 8 9 10 11 12 13
| RBloomFilter rBloomFilter = redissonClient.getBloomFilter("seqId");
rBloomFilter.tryInit(10000, 0.01);
rBloomFilter.add("100"); rBloomFilter.add("200"); rBloomFilter.add("300");
rBloomFilter.expire(30, TimeUnit.SECONDS);
System.out.println(rBloomFilter.contains("300")); System.out.println(rBloomFilter.contains("200")); System.out.println(rBloomFilter.contains("999"));
|
4.1.8 分布式自增ID
ID 是数据的唯一标识,传统的做法是利用 UUID 和数据库的自增 ID。
但由于 UUID 是无序的,不能附带一些其他信息,因此实际作用有限。
随着业务的发展,数据量会越来越大,需要对数据进行分表,甚至分库。分表后每个表的数据会按自己的节奏来自增,这样会造成 ID 冲突,因此这时就需要一个单独的机制来负责生成唯一 ID,redis 原生支持生成全局唯一的 ID。
简单样例如下!
1 2 3 4 5 6 7
| final String lockKey = "aaaa";
RAtomicLong atomicLong = redissonClient.getAtomicLong(lockKey);
atomicLong.expire(30, TimeUnit.SECONDS);
System.out.println(atomicLong.incrementAndGet());
|
4.1.9 分布式锁
Redisson 最大的亮点,也是使用最多的功能,就是提供了强大的分布式锁实现,特点是:使用简单、安全!
简单使用样例如下!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| Config config = new Config(); config.useSingleServer() .setAddress("redis://127.0.0.1:6379") .setPassword("123456") .setDatabase(0); RedissonClient redissonClient = Redisson.create(config);
final String lockKey = "abc"; RLock rLock = redissonClient.getLock(lockKey);
try {
boolean res = rLock.tryLock(5L, 60L, TimeUnit.SECONDS); if (res) {
System.out.println("获取锁成功"); } } catch (Exception e) { System.out.println("获取锁失败,失败原因:" + e.getMessage()); } finally {
rLock.unlock(); }
redissonClient.shutdown();
|
以上是单机环境下的分布式锁实现逻辑,如果是集群环境下,应该如何处理呢?
Redisson 提供RedissonRedLock
操作类,也被称为红锁,实现原理简单的总结有以下几点:
- 1.如果有多个 redis 集群的时候,当且仅当从大多数(N/2+1,比如有3个 redis 节点,那么至少有2个节点)的 Redis 节点都取到锁,并且获取锁使用的总耗时小于锁失效时间时,锁才算获取成功
- 2.如果获取失败,客户端会在所有的 Redis 实例上进行解锁操作
- 3.集群环境下,redis 服务器直接不存在任何复制或者其他隐含的分布式协调机制,否则会存在实效的可能
RedissonRedLock
简单使用样例如下!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| Config config1 = new Config(); config1.useSingleServer().setAddress("redis://192.168.3.111:6379").setPassword("a123456").setDatabase(0); RedissonClient redissonClient1 = Redisson.create(config1);
Config config2 = new Config(); config2.useSingleServer().setAddress("redis://192.168.3.112:6379").setPassword("a123456").setDatabase(0); RedissonClient redissonClient2 = Redisson.create(config2);
Config config3 = new Config(); config3.useSingleServer().setAddress("redis://192.168.3.113:6379").setPassword("a123456").setDatabase(0); RedissonClient redissonClient3 = Redisson.create(config3);
final String lockKey = "abc"; RLock lock1 = redissonClient1.getLock(lockKey); RLock lock2 = redissonClient2.getLock(lockKey); RLock lock3 = redissonClient3.getLock(lockKey);
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
try {
boolean res = redLock.tryLock(5L, 60L, TimeUnit.SECONDS); if (res) {
System.out.println("获取锁成功");
} } catch (Exception e) { System.out.println("获取锁失败,失败原因:" + e.getMessage()); } finally {
redLock.unlock(); }
|
4.2 集群配置
4.2.1 集群模式
如果是集群环境,我们可以采用如下方式进行配置:
1 2 3 4 5 6 7 8
| Config config = new Config(); config.useClusterServers() .setScanInterval(2000) // 集群状态扫描间隔时间,单位是毫秒 //可以用"rediss://"来启用SSL连接 .addNodeAddress("redis://127.0.0.1:7000", "redis://127.0.0.1:7001") .addNodeAddress("redis://127.0.0.1:7002");
RedissonClient redisson = Redisson.create(config);
|
4.2.2 哨兵模式
哨兵模式,参数配置方式如下:
1 2 3 4 5 6 7 8
| Config config = new Config(); config.useSentinelServers() .setMasterName("mymaster") //可以用"rediss://"来启用SSL连接 .addSentinelAddress("127.0.0.1:26389", "127.0.0.1:26379") .addSentinelAddress("127.0.0.1:26319");
RedissonClient redisson = Redisson.create(config);
|
4.2.3 主从模式
主从模式,参数配置方式如下:
1 2 3 4 5 6 7 8
| Config config = new Config(); config.useMasterSlaveServers() //可以用"rediss://"来启用SSL连接 .setMasterAddress("redis://127.0.0.1:6379") .addSlaveAddress("redis://127.0.0.1:6389", "redis://127.0.0.1:6332", "redis://127.0.0.1:6419") .addSlaveAddress("redis://127.0.0.1:6399");
RedissonClient redisson = Redisson.create(config);
|
5. 总结
- Jedis:Redis 官方推出的用于通过 Java 连接 Redis 客户端的一个工具包,它提供了全面的类似于 Redis 原生命令的支持,是目前使用最广的一款 java 客户端。
- Lettuce:一个可扩展的线程安全的 Redis 客户端,通讯框架基于 Netty 开发,支持高级的 Redis 特性,比如哨兵,集群,管道,自动重新连接等特性。从 Spring Boot 2.x 开始, Lettuce 已取代 Jedis 成为首选 Redis 的客户端。
- Redisson:一款架设在 Redis 基础上,通讯基于 Netty 的综合的、新型的中间件,是企业级开发中使用 Redis 的最佳范本。
总结下来,Jedis 把 Redis 命令封装的非常全面,Lettuce 则进一步丰富了 Api,支持 Redis 各种高级特性。
但是两者并没有进一步深化,只给了你操作 Redis 数据库的工具,而 Redisson 则是基于 Redis、Lua 和 Netty 建立起了一套的分布式解决方案,比如分布式锁的实现,分布式对象的操作等等。
在实际使用过程中,Lettuce
+ Redisson
组合使用的比较多,两者相铺相成。
关于分布式锁实现的应用,生产环境推荐尽量采用单点环境来实现,基本上解决绝大部分的分布式锁问题,如果当前服务的环境确实很复杂,可以采用RedissonRedLock
来实现。