Redis Hash命令操作与应用场景详解

1,102 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情 其中,读写命令中比较重要的是 HSET 和 HGET 命令,递增命令可以帮我们实现哈希表中单个 Value 的原子递增操作,批量读取命令一般用于线上的离线任务,批量处理哈希表中的数据,为了防止一次读取大量数据导致性能问题,一般使用 HSCAN 命令实现批量读取,尤其对元素较多的哈希表。最后还简单讲解了哈希表的辅助命令,其中比较常用的是 HLEN 命令。

Hash 由数组链表两种数据结构组成,数组里面每个元素就是一个槽位,一个槽位下面挂了一个链表。写入 field-value 数据的时候,会计算 field 的 hash 值,然后对数据长度进行取模,找到对应的槽位,把 field-value 插入到这个链表里面。

读写操作

哈希表里面最简单、最常用的操作,就是读写 field-value 数据,对应的就是 HGETHSET命令。

  • 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");
}