Redis从基础命令到实战之散列类型(Hash)
从上一篇的实例中可以看出,用字符串类型存储对象有一些不足,在存储/读取时需要进行序列化/反序列化,即时只想修改一项内容,如价格,也必须修改整个键值。不仅增大开发的复杂度,也增加了不必要的性能开销。
一个更好的选择是使用散列类型,或称为Hash表。散列类型与Java中的HashMap相似,是一组键值对的集合,且支持单独对其中一个键进行增删改查操作。使用散列类型存储前面示例中的商品对象,结构如下图所示:
下面先通过示例代码来看散列类型常用的操作命令
一、常用命令
HashExample.java
import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import redis.clients.jedis.Jedis; public class HashExample { public static void main(String[] args) { Jedis jedis = JedisProvider.getJedis(); jedis.flushDB(); // 为了避免混淆,下文中对Hash表中的键统称为field String key = "goods"; // hset 仅当操作在hash中创建新field时返回1 Long hset = jedis.hset(key, "id", "1"); print("hset id 1=" + hset + "; value=" + jedis.hget(key, "id")); // 如果field已存在则执行修改,并返回0 hset = jedis.hset(key, "id", "2"); print("hset id 2=" + hset + "; value=" + jedis.hget(key, "id")); // hexists 判断field是否存在 boolean hexists = jedis.hexists(key, "id"); print("hexists id=" + hexists); hexists = jedis.hexists(key, "title"); print("hexists title=" + hexists); // hsetex 如果field不存在则添加, 已存在则不会修改值, 可用来添加要求不重复的field Long hsetnx = jedis.hsetnx(key, "id", "3"); print("hsetnx id 3=" + hsetnx + "; value=" + jedis.hget(key, "id")); hsetnx = jedis.hsetnx(key, "title", "商品001"); print("hsetnx title 商品001=" + hsetnx + "; value=" + jedis.hget(key, "title")); // hmset 设置多个field Map<String, String> msets = new HashMap<>(); msets.put("color", "red"); msets.put("width", "100"); msets.put("height", "80"); String hmset = jedis.hmset(key, msets); print("hmset color,width,height=" + hmset); // hincr 新增整数类型的键值对或增加值 long hincr = jedis.hincrBy(key, "price", 4l); print("hincrBy price 4=" + hincr + "; value=" + jedis.hget(key, "price")); // hlen 读取field数量 print("hlen=" + jedis.hlen(key)); // hkeys 读取所有field Set<String> sets = jedis.hkeys(key); print("hkeys=" + Arrays.toString(sets.toArray())); // hvals 读取所有值 List<String> list = jedis.hvals(key); print("hvals=" + Arrays.toString(list.toArray())); // hgetAll 读取所有键值对 System.out.println("hgetAll 读取所有键值对"); Map<String, String> maps = jedis.hgetAll(key); for (String field : maps.keySet()) { System.out.println("hget " + field + "=" + maps.get(field)); } System.out.println("------------------------------------------------------"); System.out.println(); // hdel 删除field Long hdel = jedis.hdel(key, "id"); print("hdel id=" + hdel); // 删除多个field hdel = jedis.hdel(key, "color", "width", "height"); print("hdel color,width,height=" + hdel); // hincrBy 在整数类型值上增加, 返回修改后的值 Long hincrBy = jedis.hincrBy(key, "price", 100l); print("hincrBy price 100=" + hincrBy); // hget 读取单个field的值 String hget = jedis.hget(key, "title"); print("hget title=" + hget); // hmget 批量读取field的值 jedis.hmget(key, "title", "price"); list = jedis.hvals(key); print("hmget title,price=" + Arrays.toString(list.toArray())); jedis.close(); } private static void print(String info) { System.out.println(info); System.out.println("------------------------------------------------------"); System.out.println(); } }二、实践练习
对前一篇基于字符串类型的商品管理示例改造,以散列类型存储商品,并增加单独修改标题和修改价格的接口。
首先是添加商品代码
/** * 添加一个商品 * @param goods * @return */ public boolean addGoods(Goods goods) { long id = getIncrementId(); Map<String, String> map = new HashMap<>(); map.put("id", String.valueOf(id)); map.put("title", goods.getTitle()); map.put("price", String.valueOf(goods.getPrice())); String key = "goods:" + id; return jedis.hmset(key, map).equals("OK"); }然后增加两个单独修改属性的方法
/** * 修改商品标题 * @param goods * @return */ public boolean updateTitle(long id, String title) { String key = "goods:" + id; return jedis.hset(key, "title", title) == 0; } /** * 修改商品价格 * @param id * @param price * @return */ public boolean updatePrice(long id, float price) { String key = "goods:" + id; return jedis.hset(key, "price", String.valueOf(price)) == 0; }最后还需要修改读取商品列表的方法
/** * 读取用于分页的商品列表 * @param pageIndex 页数 * @param pageSize 每页显示行数 * @return */ public List<Goods> getGoodsList(int pageIndex, int pageSize) { int totals = (int)getTotalCount(); int from = (pageIndex - 1) * pageSize; if(from < 0) { from = 0; } else if(from > totals) { from = (totals / pageSize) * pageSize; } int to = from + pageSize; if(to > totals) { to = totals; } List<Goods> goodsList = new ArrayList<>(); for(int i = from; i < to; i++) { String key = "goods:" + (i + 1); Map<String, String> maps = jedis.hgetAll(key); Goods goods = new Goods(); goods.setId(NumberUtils.toLong(maps.get("id"))); goods.setTitle(maps.get("title")); goods.setPrice(NumberUtils.toFloat(maps.get("price"))); goodsList.add(goods); } return goodsList; }测试代码
public static void main(String[] args) { HashLession hl = new HashLession(); hl.clear(); //添加一批商品 for(int i = 0; i< 41; i++) { Goods goods = new Goods(0, "goods" + String.format("%05d", i), i); hl.addGoods(goods); } //读取商品总数 System.out.println("商品总数: " + hl.getTotalCount()); //修改商品价格 for(int i = 1; i <= hl.getTotalCount(); i++) { hl.updatePrice(i, new Random().nextFloat()); } //分页显示 List<Goods> list = hl.getGoodsList(2, 20); System.out.println("第二页商品:"); for(Goods goods : list) { System.out.println(goods); } }到目前为止,此示例仍然不能支持删除商品的功能,这是因为商品总数是以一个自增数字记录的,且关联了新商品key的生成,删除商品后不能直接减小总数,进而影响到分页的计算。一个比较低效的办法遍历数据库并累加符合规则的key总数,但是更好的做法是以链表保存所有存活的id,这将在下一篇介绍。