1.redis介绍
2.redis常用数据类型及应用场景
3.Java中操作redis
4.redis集群
5.redis事务
1.redis介绍
redis是一个key-value存储系统,它支持存储的value类型相对更多,包括string(字符串)、hash(哈希类型)、list(链表)、set(集合)、SortedSet(有序集合)。
这些数据类型都支持push/pop、add/remove及取交集并集和差集的操作,而且这些操作都是原子性的。
在此基础上,redis支持各种不同方式的排序。为了保证效率,数据都是缓存在内存中。
Redis支持主从同步。数据可以从主服务器向任意数量的从服务器上同步,从服务器可以是关联其他从服务器的主服务器
2.redis常用数据类型及应用场景
(1)Key
1)keys 查找所有符合条件的key
2)del 删除
3)exists 是否存在
4)expire 设置生存时间(以秒为单位)
5)persist 将生存时间移除
5)ttl 查看剩余生存时间(以秒为单位)
6)pexpire 设置生存时间(以毫秒为单位)
7)pttl 查看剩余生存时间(以毫秒为单位)
8)randomkey 从数据库随机返回一个key
9)sort 返回键值从小到大排序 desc 从大到小排序
10)type 所存储的值的类型
(2)String
1)get 返回一个
2)mget 返回一个或多个
3)set 设置一个
4)mset 设置一个或多个
5)setex key seconds value 将值value关联到key,并将key的生存时间设为seconds(以秒为单位)
6)setnx 设置一个key值,当且仅当key不存在
7)append 追加值
8)decr 减1
9)decrby key decrement 值减去减量decrement
10)incr 加1
11)incrby key incrment 值加上增量increment
12)getrange key start end 字符串截取0开始
13)strlen 字符串长度
应用场景:
String是最常用的一种数据类型,普通的key/value存储都可以归为此类
(3)Hash
1)hset 将哈希表key中的域field的值设为value (旧值将被覆盖)
2)hmset 同时将多个field-value(域-值)对设置到哈希表key中 (旧值将被覆盖)
3)hsetnx 将哈希表key中的域field的值设置为value,当且仅当域field不存在
4)hget 返回给定域field的值
5)hmget 返回一个或多个给定域的值
6)hgetall 返回所有的域和值
7)hdel 删除一个或多个指定域
8)hexists 查看给定域field是否存在
9)hincrby key field increment 值加上增量increment(增量也可以为负数,相当于对给定域进行减法操作)
10)hkeys 返回所有域
11)hvals 返回所有域的值
12)hlen 返回域的数量
应用场景:
比如我们要存储一个用户信息对象数据,包含以下信息:
用户ID为查找的key,存储的value用户对象包含姓名,年龄,生日等信息,如果用普通的key/value结构来存储,主要有以下2种存储方式:
第一种方式将用户ID作为查找key,把其他信息封装成一个对象以序列化的方式存储,这种方式的缺点是,增加了序列化/反序列化的开销,并且在需要修改其中一项信息时,需要把整个对象取回。
第二种方法是这个用户信息对象有多少成员就存成多少个key-value对儿,用用户ID+对应属性的名称作为唯一标识来取得对应属性的值,虽然省去了序列化开销和并发问题,但是用户ID为重复存储,如果存在大量这样的数据,会造成内存浪费。
Redis提供的Hash很好的解决了这个问题,Redis的Hash实际是内部存储的Value为一个HashMap,并提供了直接存取这个Map成员的接口:
Key仍然是用户ID, value是一个Map,这个Map的key是成员的属性名,value是属性值,这样对数据的修改和存取都可以直接通过其内部Map的Key, 也就是通过 key(用户ID) + field(属性标签) 就可以操作对应属性数据了,既不需要重复存储数据,也不会带来序列化的问题。
(4)List
1)lset 将下标为index的元素的值设置为value
2)lpush 将一个或多个值插入到列表的表头
3)lpushx 将值插入到列表的表头,当且仅当key存在并且是一个列表
4)lpop 移除并返回列表的头元素
5)rpush 将一个或多个值插入到列表的表尾(最右边)
6)rpushx 将值插入到列表的表尾,当且仅当key存在并且是一个列表
7)rpop 移除并返回列表的尾元素
8)linsert key BEFORE|AFTER pivot value 将值value插入到列表key当中,位于值pivot之前或之后
9)lindex 返回列表中,下标为index的元素
10)lrange 返回列表中指定区间内的元素,区间以偏移量start和stop指定 -1表示最后一个元素
11)llen 返回列表的长度
应用场景:
list的应用场景非常多,也是Redis最重要的数据结构之一,比较适用于列表式存储且顺序相对比较固定,例如:省份、城市列表。
(5)Set
1)sadd 将一个或多个元素加入到集合中
2)scard 集合中元素的数量
3)sismember 元素是否集合的成员
4)smembers 返回集合中的所有成员
5)spop 将随机元素从集合中移除并返回
6)srandmember 返回随机元素
7)srem 移除集合中的一个或多个元素
应用场景:
set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,可以使用set,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。
(6)SortedSet
1)zadd 将一个或多个元素加入到有序集中
2)zcard 集合中元素的数量
3)zincrby key increment member 为有序集的成员值加上增量increment
4)zrem 移除有序集中的一个或多个成员
5)zscore 返回有序集中,成员的值
6)zrevrange 返回有序集中,指定区间内的成员
应用场景:
sorted set的使用场景与set类似,区别是set不是自动有序的,而sorted set可以通过用户额外提供一个优先级(score)的参数来为成员排序,并且是插入有序的,即自动排序。当你需要一个有序的并且不重复的集合列表,那么可以选择sorted set数据结构。
3.Java中操作redis
Java中使用Jedis操作Redis,具体实现代码部分讲解。
1 package redis; 2 3 import org.junit.Before; 4 import org.junit.Test; 5 import redis.clients.jedis.BinaryClient; 6 import redis.clients.jedis.Jedis; 7 import redis.clients.jedis.Transaction; 8 9 import java.util.HashMap; 10 import java.util.Map; 11 12 13 public class TestRedis { 14 private Jedis jedis; 15 16 @Before 17 public void setup() { 18 jedis = new Jedis("127.0.0.1", 6379); 19 } 20 21 /** 22 * Key 23 * 1)keys 查找所有符合条件的key 24 * 2)del 删除 25 * 3)exists 是否存在 26 * 4)expire 设置生存时间(以秒为单位) 27 * 5)persist 将生存时间移除 28 * 5)ttl 查看剩余生存时间(以秒为单位) 29 * 6)pexpire 设置生存时间(以毫秒为单位) 30 * 7)pttl 查看剩余生存时间(以毫秒为单位) 31 * 8)randomkey 从数据库随机返回一个key 32 * 9)sort 返回键值从小到大排序 desc 从大到小排序 33 * 10)type 所存储的值的类型 34 */ 35 @Test 36 public void keyTest() { 37 jedis.mset("one", "1", "two", "2", "three", "3"); 38 System.out.println("所有值:" + jedis.keys("*")); 39 System.out.println("删除:" + jedis.del("one")); 40 System.out.println("存在:" + jedis.exists("one")); 41 System.out.println("存在:" + jedis.exists("two")); 42 System.out.println("有效时间:" + jedis.expire("two", 3600));//生存时间 秒 43 System.out.println("剩余有效时间:" + jedis.ttl("two"));//剩余生存时间 44 System.out.println("随机返回:" + jedis.randomKey()); 45 System.out.println("值类型:" + jedis.type("three")); 46 System.out.println("key的数量:" + jedis.dbSize()); 47 System.out.println("删除全部key:" + jedis.flushDB()); 48 } 49 50 51 /** 52 * redis存储字符串 53 * 1)get 返回一个 54 * 2)mget 返回一个或多个 55 * 3)set 设置一个 56 * 4)mset 设置一个或多个 57 * 5)setex key seconds value 将值value关联到key,并将key的生存时间设为seconds(以秒为单位) 58 * 6)setnx 设置一个key值,当且仅当key不存在 59 * 7)append 追加值 60 * 8)decr 减1 61 * 9)decrby key decrement 值减去减量decrement 62 * 10)incr 加1 63 * 11)incrby key incrment 值加上增量increment 64 * 12)getrange key start end 字符串截取0开始 65 * 13)strlen 字符串长度 66 */ 67 @Test 68 public void testString() { 69 jedis.set("one", "1"); 70 System.out.println("单个值:" + jedis.get("one")); 71 72 jedis.mset("two", "2", "three", "3", "four", "4", "five", "5"); 73 System.out.println("多个值:" + jedis.mget("two", "three", "four", "five")); 74 75 jedis.setex("six", 3600, "6"); 76 System.out.println(jedis.get("six") + ",有效时间:" + jedis.ttl("six")); 77 78 System.out.println(jedis.setnx("six", "6")); 79 System.out.println(jedis.setnx("seven", "7")); 80 81 System.out.println("six长度:" + jedis.append("six", "66") + ",six新值:" + jedis.get("six")); 82 83 System.out.println("six:" + jedis.decr("six")); 84 System.out.println("six:" + jedis.decrBy("six", 659)); 85 86 System.out.println("six:" + jedis.incr("six")); 87 System.out.println("six:" + jedis.incrBy("six", 659)); 88 89 jedis.set("java", "hello word"); 90 System.out.println(jedis.getrange("java", 0, 4));//字符串截取0开始 91 System.out.println(jedis.getrange("java", -4, -1)); 92 System.out.println(jedis.strlen("java")); 93 94 System.out.println("key的数量:" + jedis.dbSize()); 95 System.out.println("删除全部key:" + jedis.flushDB()); 96 } 97 98 /** 99 * redis操作Map 100 * 1)hset 将哈希表key中的域field的值设为value (旧值将被覆盖) 101 * 2)hmset 同时将多个field-value(域-值)对设置到哈希表key中 (旧值将被覆盖) 102 * 3)hsetnx 将哈希表key中的域field的值设置为value,当且仅当域field不存在 103 * 4)hget 返回给定域field的值 104 * 5)hmget 返回一个或多个给定域的值 105 * 6)hgetall 返回所有的域和值 106 * 7)hdel 删除一个或多个指定域 107 * 8)hexists 查看给定域field是否存在 108 * 9)hincrby key field increment 值加上增量increment(增量也可以为负数,相当于对给定域进行减法操作) 109 * 10)hkeys 返回所有域 110 * 11)hvals 返回所有域的值 111 * 12)hlen 返回域的数量 112 */ 113 @Test 114 public void testMap() { 115 jedis.hset("user", "name", "cheng"); 116 117 Map<String, String> map = new HashMap<String, String>(); 118 map.put("age", "10"); 119 map.put("qq", "123456"); 120 121 jedis.hmset("user", map); 122 123 jedis.hsetnx("user", "name", "chengwei"); 124 125 System.out.println("name值:" + jedis.hget("user", "name")); 126 127 System.out.println(jedis.hmget("user", "name", "age", "qqq")); 128 129 System.out.println("所有的域:" + jedis.hkeys("user")); 130 System.out.println("所有的域的值:" + jedis.hvals("user")); 131 System.out.println("所有的域和值:" + jedis.hgetAll("user")); 132 System.out.println("域的数量:" + jedis.hlen("user")); 133 134 jedis.hdel("user", "name"); 135 System.out.println("name值:" + jedis.hmget("user", "name")); 136 137 System.out.println(jedis.exists("user")); 138 139 jedis.hincrBy("user", "age", 10L); 140 System.out.println("age值加后:" + jedis.hget("user", "age")); 141 jedis.hincrBy("user", "age", -10L); 142 System.out.println("age值减后:" + jedis.hget("user", "age")); 143 144 System.out.println("删除全部key:" + jedis.flushDB()); 145 } 146 147 /** 148 * jedis操作List 149 * 1)lset 将下标为index的元素的值设置为value 150 * 2)lpush 将一个或多个值插入到列表的表头 151 * 3)lpushx 将值插入到列表的表头,当且仅当key存在并且是一个列表 152 * 4)lpop 移除并返回列表的头元素 153 * 5)rpush 将一个或多个值插入到列表的表尾(最右边) 154 * 6)rpushx 将值插入到列表的表尾,当且仅当key存在并且是一个列表 155 * 7)rpop 移除并返回列表的尾元素 156 * 8)linsert key BEFORE|AFTER pivot value 将值value插入到列表key当中,位于值pivot之前或之后 157 * 9)lindex 返回列表中,下标为index的元素 158 * 10)lrange 返回列表中指定区间内的元素,区间以偏移量start和stop指定 -1表示最后一个元素 159 * 11)llen 返回列表的长度 160 */ 161 @Test 162 public void testList() { 163 jedis.lpush("java", "struts"); 164 jedis.lpush("java", "mybatis"); 165 166 jedis.lset("java", 0L, "spring"); 167 168 System.out.println("1." + jedis.lrange("java", 0, -1)); 169 170 System.out.println("2." + jedis.lpop("java")); 171 System.out.println("2.1." + jedis.lrange("java", 0, -1)); 172 173 jedis.del("java"); 174 jedis.rpush("java", "spring"); 175 jedis.rpush("java", "struts"); 176 jedis.rpush("java", "mybatis"); 177 178 System.out.println("3." + jedis.lrange("java", 0, -1)); 179 180 System.out.println("4." + jedis.rpop("java")); 181 System.out.println("4.1." + jedis.lrange("java", 0, -1)); 182 183 jedis.linsert("java", BinaryClient.LIST_POSITION.AFTER, "spring", "springMVC"); 184 System.out.println("5." + jedis.lrange("java", 0, -1)); 185 186 System.out.println("6." + jedis.lindex("java", 0L)); 187 188 System.out.println("7." + jedis.llen("java")); 189 190 System.out.println("删除全部key:" + jedis.flushDB()); 191 } 192 193 /** 194 * jedis操作Set 195 * 1)sadd 将一个或多个元素加入到集合中 196 * 2)scard 集合中元素的数量 197 * 3)sismember 元素是否集合的成员 198 * 4)smembers 返回集合中的所有成员 199 * 5)spop 将随机元素从集合中移除并返回 200 * 6)srandmember 返回随机元素 201 * 7)srem 移除集合中的一个或多个元素 202 */ 203 @Test 204 public void testSet() { 205 //添加 206 jedis.sadd("java", "spring", "struts", "mybatis", "struts2"); 207 208 System.out.println("集合中元素的数量:" + jedis.scard("java")); 209 210 System.out.println("所有成员:" + jedis.smembers("java")); 211 212 jedis.srem("java", "spring"); 213 System.out.println("移除spring后所有成员:" + jedis.smembers("java")); 214 215 System.out.println("元素是否集合的成员:" + jedis.sismember("java", "struts2")); 216 217 System.out.println("将随机元素从集合中移除并返回:" + jedis.spop("java")); 218 219 System.out.println("所有成员:" + jedis.smembers("java")); 220 221 System.out.println("返回随机元素:" + jedis.srandmember("java")); 222 223 System.out.println("所有成员:" + jedis.smembers("java")); 224 225 System.out.println("删除全部key:" + jedis.flushDB()); 226 } 227 228 /** 229 * jedis操作SortedSet 230 * 1)zadd 将一个或多个元素加入到有序集中 231 * 2)zcard 集合中元素的数量 232 * 3)zincrby key increment member 为有序集的成员值加上增量increment 233 * 4)zrem 移除有序集中的一个或多个成员 234 * 5)zscore 返回有序集中,成员的值 235 * 6)zrevrange 返回有序集中,指定区间内的成员 236 */ 237 @Test 238 public void testSortedSet() { 239 jedis.zadd("java", 3.1, "spring"); 240 System.out.println("1." + jedis.zcard("java")); 241 242 Map map = new HashMap(); 243 map.put(2.0, "struts"); 244 map.put(3.0, "mybatis"); 245 jedis.zadd("java", map); 246 System.out.println("2." + jedis.zcard("java"));//集合中元素的数量 247 248 System.out.println("3." + jedis.zrevrange("java", 0L, -1));//返回有序集中,指定区间内的成员 249 250 jedis.zincrby("java", 1, "mybatis"); 251 252 System.out.println("4." + jedis.zrevrange("java", 0L, -1)); 253 254 jedis.zrem("java", "spring");//移除有序集中的一个或多个成员 255 256 System.out.println("5." + jedis.zrevrange("java", 0L, -1)); 257 258 System.out.println("6." + jedis.zscore("java", "mybatis"));//返回有序集中,成员的值 259 260 System.out.println("删除全部key:" + jedis.flushDB()); 261 } 262 263 264 }
4.redis集群
1)Redis集群的分配数据是采用另外一种叫做哈希槽 (hash slot)的方式来分配的。redis集群默认分配了16384个哈希槽,当我们set一个key时,会用CRC16算法来取模得到所属的槽,然后将这个key分到哈希槽区间的节点上,具体算法就是:CRC16(key)%16384。
节点:一个端口的redis服务便是一个节点
槽(集群将整个系统分为16384个hash槽):这16384个槽位要全部分布在集群中的主节点上。
重新分片:若某个主节点故障了,将该主节点的槽位分配到其他可以用的主节点上。
上线/下线状态:是否全部的槽位都分布在节点上。
2)集群中的每个节点负责处理一部分哈希槽。举个例子,一个集群可以有三个哈希槽,其中:
节点A负责处理0号至5500号哈希槽。
节点B负责处理5501号至11000号哈希槽。
节点C负责处理11001号至16384号哈希槽。
这种将哈希槽分布到不同节点的做法使得用户可以很容易地向集群中添加或者删除节点。 比如说:
如果用户将新节点D添加到集群中, 那么集群只需要将节点A 、B 、 C中的某些槽移动到节点D就可以了。
与此类似,如果用户要从集群中移除节点A ,那么集群只需要将节点A中的所有哈希槽移动到节点B和节点C ,然后再移除空白(不包含任何哈希槽)的节点A就可以了。
5.事务
事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
MULTI 命令用于开启一个事务
EXEC 执行所有事务块内的命令
DISCARD 取消事务
WATCH 监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断
1 package redis; 2 3 import org.junit.Test; 4 import redis.clients.jedis.Jedis; 5 import redis.clients.jedis.Transaction; 6 7 import java.util.List; 8 9 /** 10 * 模拟刷一次信用卡的交易 11 */ 12 public class TestTransaction { 13 @Test 14 public void test(){ 15 boolean retValue = false; 16 boolean Interrupted = false; 17 18 try { 19 retValue = transMethod(100); 20 } catch (Exception e) { 21 Interrupted = true; 22 System.out.println("事务被打断,请重新执行!"); 23 } finally { 24 if (retValue) { 25 System.out.println("使用信用卡消费成功!"); 26 } else { 27 if (!Interrupted) { 28 System.out.println("使用信用卡消费失败!余额不足!"); 29 } 30 } 31 } 32 } 33 34 35 /** 36 * 通俗点讲,watch命令就是标记一个键,如果标记了一个键, 37 * 在提交事务前如果该键被别人修改过,那事务就会失败,这种情况通常可以在程序中 38 * 重新再尝试一次。 39 * <p> 40 * 首先标记了balance,然后检查余额是否足够,不足就取消标记,并不做扣减; 41 * 足够的话,就启动事务进行更新操作。 42 * 如果在此期间键balance被其他人修改,拿在提交事务(执行exec)时就会报错, 43 * 程序中通常可以捕获这类错误再重新执行一次,直到成功。 44 */ 45 private boolean transMethod(int amount) throws Exception { 46 47 System.out.println("您使用信用卡预付款" + amount + "元"); 48 49 Jedis jedis = new Jedis("127.0.0.1", 6379); 50 51 int balance = 1000;//可用余额 52 int debt;//欠额 53 int amtToSubtract = amount;//实刷额度 54 55 jedis.set("balance", String.valueOf(balance)); 56 jedis.watch("balance"); 57 // jedis.set("balance", "1100");//为了模拟其他程序已经修改了该条目 58 balance = Integer.parseInt(jedis.get("balance")); 59 if (balance < amtToSubtract) {//可用余额小于实刷金额,拒绝交易 60 jedis.unwatch(); 61 System.out.println("可用余额不足!"); 62 return false; 63 } else {//可用余额够用的时候再去执行扣费操作 64 System.out.println("扣费transaction事务开始执行..."); 65 Transaction transaction = jedis.multi(); 66 transaction.decrBy("balance", amtToSubtract);//余额减去amtToSubtract的钱数 67 transaction.incrBy("debt", amtToSubtract);//信用卡欠款增加amtToSubtract的钱数 68 List<Object> result = transaction.exec();//执行事务 69 70 if (result == null) {//事务提交失败,说明在执行期间数据被修改过 71 72 System.out.println("扣费transaction事务执行中断..."); 73 throw new Exception(); 74 75 } else {//事务提交成功 76 balance = Integer.parseInt(jedis.get("balance")); 77 debt = Integer.parseInt(jedis.get("debt")); 78 System.out.println("扣费transaction事务执行结束..."); 79 80 System.out.println("您的可用余额:" + balance); 81 System.out.println("您目前欠款:" + debt); 82 83 return true; 84 } 85 } 86 } 87 }
redis的事务存在的问题。redis只能保证事务的每个命令连续执行,但是如果事务中的一个命令失败了,并不回滚其他命令
以下是这种做法的优点:
1.Redis 命令只会因为错误的语法而失败(并且这些问题不能在入队时发现),或是命令用在了错误类型的键上面,从实用性的角度来说,失败的命令是由编程错误造成的,而这些错误应该在开发的过程中被发现,而不应该出现在生产环境中。
2.因为不需要对回滚进行支持,所以 Redis 的内部可以保持简单且快速。
参考资料:http://doc.redisfans.com/index.html