开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情 其中,读写命令中比较重要的是 HSET 和 HGET 命令,递增命令可以帮我们实现哈希表中单个 Value 的原子递增操作,批量读取命令一般用于线上的离线任务,批量处理哈希表中的数据,为了防止一次读取大量数据导致性能问题,一般使用 HSCAN 命令实现批量读取,尤其对元素较多的哈希表。最后还简单讲解了哈希表的辅助命令,其中比较常用的是 HLEN 命令。
Hash 由数组和链表两种数据结构组成,数组里面每个元素就是一个槽位,一个槽位下面挂了一个链表。写入 field-value 数据的时候,会计算 field 的 hash 值,然后对数据长度进行取模,找到对应的槽位,把 field-value 插入到这个链表里面。
读写操作
哈希表里面最简单、最常用的操作,就是读写 field-value 数据,对应的就是 HGET、HSET命令。
-
HSET、HGET、HMGET
用
HMGET这个命令去批量拿一下哈希表里面的 field,HMGET 命令后面可以跟多个 field,例如下面示例中的 name、age、height,返回值顺序就是 HMGET 命令取到的 value 值,顺序也和 field 一致。
# HSET Key field-value
hset testHash name kouzhaoxuejie
-> 1
hget testHash name ->kouzhaoxuejie
# HSET命令后可以跟多个field-value
HSET testHash age 25 height 170
HMGET testHash name age height
-
HSETNX(用法和string中setnx一样)
把 name 这个 field 的值改成 kouzhao,但是由于这个 Key 已经存在了,写入失败了。然后,尝试用 HSETNX 写入存 weight 这个 field,由于 weight 这个 field 不存在,所以能写入成功了。
-
HEXISTS命令查询哈希表中是否已经有指定的field
-
HDEL,就是从哈希表里面删除一个 field
递增操作
-
HINCRBY类似于string中的incrby
127.0.0.1:6379> HSET testHash fans 100 (integer) 1 127.0.0.1:6379> HINCRBY testHash fans 2 (integer) 102 -
HINCRBYFLOAT 加减小数
批量读取
有的场景中,我们需要迭代哈希表里面全部的 field-value 值。对于元素比较少、数据小的哈希表,可以考虑用
HGETALL命令,拿到全部的 field-value 值。如果只想获取哈希表里面的 field 值,可以使用HKEYS命令;如果只想获取哈希表里面的 value 值,可以使用HVALS命令。
-
HGETALL、HKEYS、HVALS
重点强调一下,这三个全量获取哈希表数据的命令,只适合查询小数据量的哈希表。对于数据量很大的哈希表,这三个命令就会把整个 Redis 阻塞掉,所以
在生产环境中一般是禁用这三个命令的。HGETALL testHash 1) "name" 2) "kouzhaoxuejie" 3) "age" 4) "26" 5) "height" 6) "170" 7) "fans" 8) "102" 9) "account" 10) "49.5" 127.0.0.1:6379> HKEYS testHash 1) "name" 2) "age" 3) "height" 4) "fans" 5) "account" 127.0.0.1:6379> HVALS testHash 1) "kouzhaoxuejie" 2) "26" 3) "170" 4) "102" 5) "49.5"
那如果在生产环境中要获取整个哈希表的内容,可以使用 HSCAN 命令。
HSCAN 是把一次获取哈希表全量数据的这个操作,拆成了多次迭代,一次迭代只返回一部分数据,这样的话,每次访问的数据量就小了很多,也就不会阻塞 Redis 了。
- HSCAN(需要再详细了解)
HSCAN key cursor [MATCH pattern] [COUNT count]
cursor 是游标的意思,其实就是个数字,你可以把 cursor 理解为一次迭代的结束位置,也是下次迭代的开始位置,它用来告诉 Redis 从哪里开始下一次迭代。MATCH 部分,就像是 SQL 语句里面的 where 条件一样,按照一定的格式来过滤 field。COUNT 部分就像是 SQL语句里面的 limit,但是这个 COUNT 只是提示 Redis 应该返回多少条数据,不是严格控制,具体返回多少条数据,可以先简单认为是看 Redis 心情,后面分析哈希表底层实现的时候,我们会看到为什么 COUNT 无法做到严格限制。
其他命令
- HLEN、HSTRLEN、HRANDFIELD
HLEN 命令在前面使用过了,它的功能是查看哈希表里面有多少条 field-value 数据;HSTRLEN 命令是查看一个 value 的长度。HRANDFIELD 命令的功能是从哈希表中随机返回 field-value 数据。
HMGET testHash name age
1) "kouzhaoxuejie"
2) "26"
127.0.0.1:6379> HSTRLEN testHash name
(integer) 13
127.0.0.1:6379> HSTRLEN testHash age
(integer) 2
#HRANDFIELD随机返回3条数据,WITHVALUES参数会让field和value都返回
127.0.0.1:6379> HRANDFIELD testClass 3 WITHVALUES
1) "student848"
2) "VcBYTGptdmdBOPv..."
3) "student565"
4) "QLgIsHXLGrefPS..."
5) "student559"
6) "GkRWcJOZJtuidJz0HQ..."
#没有加WITHVALUES参数,值返回field
127.0.0.1:6379> HRANDFIELD testClass 3
1) "student47"
2) "student185"
3) "student391"
使用场景
用户资料缓存、购物车缓存。
用户资料缓存
一个 C 端产品的用户量一般都会比较大,每个用户每次登录的时候,都要校验密码,登录成功之后,返回头像、昵称这些基础信息,这个时候就比较适合用 Redis Hash 结构来存用户的这些信息,
活跃用户的信息进 Redis 进行缓存,非活跃用户留在 MySQL 里面。
先往map里面压几个用户。
private static Map<String, User> userDB = new HashMap<>();
@Before
public void before() {
... // 省略初始化RedisClient的逻辑
userDB.put("+8613912345678", new User(1L, "zhangsan", 25, "+8613912345678", "123456", "http://xxxx"));
userDB.put("+8613512345678", new User(2L, "lisi", 25, "+8613512345678", "abcde", "http://xxxx"));
userDB.put("+8618812345678", new User(3L, "wangwu", 25, "+8618812345678", "654321", "http://xxxx"));
userDB.put("+8618912345678", new User(4L, "zhaoliu", 25, "+8618912345678", "98765", "http://xxxx"));
}
private static final String USER_CACHE_PREFIX = "uc_";
private static void mockLogin(String mobile, String password) throws Exception {
// 根据手机号,查询缓存
String key = USER_CACHE_PREFIX + mobile;
Map<String, String> userCache = asyncCommands.hgetall(key).get(1, TimeUnit.SECONDS);
User user = null;
if (MapUtils.isEmpty(userCache)) {
System.out.println("缓存miss,加载DB");
user = userDB.get(mobile);
if (user == null) {
System.out.println("登录失败");
return;
}
// User转成Map
Map<String, String> userMap = BeanUtils.describe(user);
// 写入缓存
Long result = asyncCommands.hset(key, userMap).get(1, TimeUnit.SECONDS);
if (result == 1) {
System.out.println("UserId:" + user.getUserId() + ",已进入缓存");
}
} else {
System.out.println("缓存hit");
user = new User();
BeanUtils.populate(user, userCache);
}
if (password.equals(user.getPassword())) {
System.out.println(user.getName() + ", 登录成功!");
} else {
System.out.println("登录失败");
}
System.out.println("================================");
}
@Test
public void testUserCache() throws Throwable {
mockLogin("+8613912345678", "654321");
mockLogin("+8613912345678", "123456");
}
购物车缓存
public class CartDao {
private static final String CART_PREFIX = "cart_";
public void add(long userId, String productId) throws Exception {
Boolean result = asyncCommands.hset(CART_PREFIX + userId,
productId, "1").get(1, TimeUnit.SECONDS);
if (result) {
System.out.println("添加购物车成功,productId:" + productId);
}
}
public void remove(long userId, String productId) throws Exception {
Long result = asyncCommands.hdel(CART_PREFIX + userId,
productId).get(1, TimeUnit.SECONDS);
if (result == 1) {
System.out.println("商品删除成功,productId:" + productId);
}
}
public void submitOrder(long userId) throws Exception {
Map<String, String> cartInfo = asyncCommands.hgetall(CART_PREFIX + userId).get(1, TimeUnit.SECONDS);
System.out.println("用户:"+userId+", 提交订单:");
for (Map.Entry<String, String> entry : cartInfo.entrySet()) {
System.out.println(entry.getKey() + ":" + entry.getValue());
}
}
}
@Test
public void testCartDao() throws Throwable {
CartDao cartDao = new CartDao();
cartDao.add(1024, "83694");
cartDao.add(1024, "1273979");
cartDao.add(1024, "123323");
cartDao.submitOrder(1024);
cartDao.remove(1024, "123323");
}