常用缓存技术

热数据缓存

这是使用缓存最频繁最直接的方式,即我们把需要频繁访问DB的数据加载到内存里面,以提高响应速度。通常我们的做法是使用一个ConcuccrentHashMap来记录一天当中每个请求的次数,每天凌晨取出昨天访问最频繁的K个请求(K取多少个取决你的可用内存有多少),从DB中读取这些请求的返回结果放到一个ConcuccrentHashMap容器中,然后把所有请求计数清0,重新开始计数。

LRU缓存

热数据缓存适用于那些热数据比较明显且稳定的业务场景,而对于那些热数据不稳定的应用场景我们需要发明一种动态的热数据识别方式。我们都知道常用的内存换页算法有2种:LFU和LRU。

LFU(Least Frequently Used)是把那些最近最不经常使用的页面置换出去,这跟上面讲的热数据缓存是一个道理,缺点有2个:

  1. 需要维护一个计数器,记住每个页面的使用次数。
  2. 上一个时间段频繁使用的,在下一个时间段不一定还频繁。

LRU(Least Recently Used)策略是把最近最长时间未使用的页面置换出去。实现起来很简单,只需要一个链表结构,每次访问一个元素时把它移到链表的尾部,当链表已满需要删除元素时就删除头部的元素,因为头部的元素就是最近最长时间未使用的元素。

 1 import java.util.ArrayList;
 2 import java.util.Collection;
 3 import java.util.LinkedHashMap;
 4 import java.util.Map;
 5 import java.util.concurrent.locks.ReadWriteLock;
 6 import java.util.concurrent.locks.ReentrantReadWriteLock;
 7 
 8 /**
 9  * 利用LinkedHashMap实现一个定长容量的,先进先出的队列。当指定按访问顺序排序时,就实际上是一个最近最少使用LRU队列
10 *
11 * 根据链表中元素的顺序可以分为:按插入顺序的链表,和按访问顺序(调用get方法)的链表。
12 * 默认是按插入顺序排序,如果指定按访问顺序排序,那么调用get方法后,会将这次访问的元素移至链表尾部。
13 * 不断访问可以形成按访问顺序排序的链表。
14 * 可以重写removeEldestEntry方法返回true值指定插入元素时移除最老的元素。
15 * 16 * @Author:zhangchaoyang 17 * @Since:2014-9-5 18 * @Version:1.0 19 */ 20 public class LRUCache extends LinkedHashMap { 21 22 private static final long serialVersionUID = -2045058079564141163L; 23 24 private final int maxCapacity; 25 26 // 本类中设置装载因子实际没有意义,因为容量超过maxCapacity时就会把元素移除掉 27 private static final float DEFAULT_LOAD_FACTOR = 1f; 28 29 private final ReadWriteLock lock = new ReentrantReadWriteLock(); 30 31 public LRUCache(int maxCapacity) { 32 super(maxCapacity, DEFAULT_LOAD_FACTOR, true);// 第3个参数false表示维持插入顺序,这样最早插入的将最先被移除。true表示维持访问顺序,调用get方法后,会将这次访问的元素移至链表尾部,删除老元素时会删除表头元素。 33 this.maxCapacity = maxCapacity; 34 } 35 36 @Override 37 protected boolean removeEldestEntry(java.util.Map.Entry eldest) { 38 return size() > maxCapacity;// 到达maxCapacity时就移除老元素,这样实现定长的LinkedHashMap 39 } 40 41 @Override 42 public boolean containsKey(Object key) { 43 try { 44 lock.readLock().lock(); 45 return super.containsKey(key); 46 } finally { 47 lock.readLock().unlock(); 48 } 49 } 50 51 @Override 52 public V get(Object key) { 53 try { 54 lock.readLock().lock(); 55 return super.get(key); 56 } finally { 57 lock.readLock().unlock(); 58 } 59 } 60 61 @Override 62 public V put(K key, V value) { 63 try { 64 lock.writeLock().lock(); 65 return super.put(key, value); 66 } finally { 67 lock.writeLock().unlock(); 68 } 69 } 70 71 public int size() { 72 try { 73 lock.readLock().lock(); 74 return super.size(); 75 } finally { 76 lock.readLock().unlock(); 77 } 78 } 79 80 public void clear() { 81 try { 82 lock.writeLock().lock(); 83 super.clear(); 84 } finally { 85 lock.writeLock().unlock(); 86 } 87 } 88 89 public Collection> getAll() { 90 try { 91 lock.readLock().lock(); 92 return new ArrayList>(super.entrySet()); 93 } finally { 94 lock.readLock().unlock(); 95 } 96 } 97 }
View Code

TimeOut缓存

Timeout缓存常用于那些跟用户关联的请求数据,比如用户在翻页查看一个列表数据时,他第一次看N页的数据时,服务器是从DB中读取的相应数据,当他看第N+1页的数据时应该把第N页的数据放入缓存,因为用户可能呆会儿还会回过头来看第N页的数据,这时候服务器就可以直接从缓存中获取数据。如果用户在5分钟内还没有回过头来看第N页的数据,那么我们认为他再看第N页的概率就非常低了,此时可以把第N页的数据从缓存中移除,实际上相当于我们为缓存设置了一个超时时间。

我想了一种Timeout缓存的实现方法。还是用ConcurrentHashMap来存放key-value,另建一棵小顶堆,每个节点上存放key以及key的到期时间,建堆时依据到期时间来建。开一个后台线程不停地扫描堆顶元素,拿当前的时间戳去跟堆顶的到期时间比较,如果当前时间晚于堆顶的到期时间则删除堆顶,把堆顶里存放的key从ConcurrentHashMap中删除。删除堆顶的时间复杂度为$O(log_2{N})$,具体步骤如下:

  1. 用末元素替换堆顶元素root
    常用缓存技术_第1张图片

  2. 临时保存root节点。从上往下遍历树,用子节点中较小那个替换父节点。最后把root放到叶节点上
    常用缓存技术_第2张图片

下面的代码是直接基于java中的java.util.concurrent.Delayed实现的,Delayed是不是基于上面的小顶堆的思想我也没去深入研究。

TimeoutCache.java

 1 import java.io.IOException;
 2 import java.util.concurrent.ConcurrentHashMap;
 3 import java.util.concurrent.ConcurrentMap;
 4 import java.util.concurrent.DelayQueue;
 5 import java.util.concurrent.TimeUnit;
 6 
 7 import org.apache.commons.logging.Log;
 8 import org.apache.commons.logging.LogFactory;
 9 
10 /**
11  * 可以为每个元素设置存活时间的缓存容器
12  * 
13  * @Author:orisun
14  * @Since:2015-10-9
15  * @Version:1.0
16  */
17 public class TimeoutCache {
18 
19     private static final Log logger = LogFactory.getLog(TimeoutCache.class);
20     private ConcurrentMap cacheObjMap = new ConcurrentHashMap();
21     private DelayQueue>> queue = new DelayQueue>>();
22     private Thread daemonThread;
23 
24     public TimeoutCache() {
25         Runnable daemonTask = new Runnable() {
26             public void run() {
27                 daemonCheck();
28             }
29         };
30         daemonThread = new Thread(daemonTask);
31         daemonThread.setDaemon(true);
32         daemonThread.setName("TimeoutCache Daemon Check");
33         daemonThread.start(); // 启动后台线程,对容器中的元素不停地进行轮循,将过期的元素移除出出去
34     }
35 
36     private void daemonCheck() {
37         logger.info("check timeout element of cache started");
38         for (;;) {
39             try {
40                 DelayItem> delayItem = queue.take();// 如果所有元素都没有超时,该行代码会阻塞
41                 if (delayItem != null) {
42                     Pair pair = delayItem.getItem();
43                     cacheObjMap.remove(pair.first, pair.second); // 超时对象,从容器中移除
44                 }
45             } catch (InterruptedException e) {
46                 logger.error("take timeout element from cache failed", e);
47                 break; // 检测到中断时就退出循环
48             }
49         }
50         logger.info("check timeout element of cache stopped.");
51     }
52 
53     /**
54      * 以覆盖的方式向缓存中添加对象,缓存以的形式存在.
55 * 注意:value如果是List,则它不是由通过List.subList()得来的 56 * 。因为List.subList()返回的是一个RandomAccessSubList实例 57 * ,在反序列化时ObjectOutputStream.writeObject(RandomAccessSubList)会出错 58 * 59 * @param key 60 * @param value 61 * @param time 62 * 对象在缓存中的生存时间 63 * @param unit 64 * 时间单位 65 */ 66 public void put(K key, V value, long time, TimeUnit unit) { 67 V oldValue = cacheObjMap.put(key, value); 68 if (oldValue != null) 69 queue.remove(key); 70 71 long nanoTime = TimeUnit.NANOSECONDS.convert(time, unit); 72 queue.put(new DelayItem>(new Pair(key, value), 73 nanoTime)); 74 } 75 76 /** 77 * 根据key从缓存中取得对应的value,如果key不存在则返回null
78 * 取出的是value的深拷贝 79 * 80 * @param key 81 * @return 82 */ 83 @SuppressWarnings("unchecked") 84 public V get(K key) { 85 try { 86 return (V) JavaSerializer.deepCopy(cacheObjMap.get(key)); 87 } catch (ClassNotFoundException | IOException e) { 88 e.printStackTrace(); 89 return null; 90 } 91 } 92 93 }
View Code

DelayItem.java

 1 import java.util.concurrent.Delayed;
 2 import java.util.concurrent.TimeUnit;
 3 import java.util.concurrent.atomic.AtomicLong;
 4 
 5 /**
 6  * 
 7  * @Author:orisun
 8  * @Since:2015-10-9
 9  * @Version:1.0
10  */
11 public class DelayItem implements Delayed {
12 
13     private static final long ORIGIN = System.nanoTime();// 记录进入队列的时刻
14     private static final AtomicLong sequencer = new AtomicLong(0);
15     private final long sequenceNumber;
16     private final long time;
17     private final T item;
18 
19     final static long now() {
20         return System.nanoTime() - ORIGIN;
21     }
22 
23     /**
24      * 
25      * @param submit
26      *            队列中的元素类型
27      * @param timeout
28      *            元素在队列中存活的时间,单位:毫秒
29      */
30     public DelayItem(T submit, long timeout) {
31         this.time = now() + timeout;// 出队时刻
32         this.item = submit;// 入队元素
33         this.sequenceNumber = sequencer.getAndIncrement();// 在队列中的编号
34     }
35 
36     public T getItem() {
37         return this.item;
38     }
39 
40     @Override
41     public long getDelay(TimeUnit unit) {
42         long d = unit.convert(time - now(), TimeUnit.NANOSECONDS);
43         return d;
44     }
45 
46     @Override
47     public int compareTo(Delayed other) {
48         if (other == this)
49             return 0;
50         if (other instanceof DelayItem) {
51             DelayItem x = (DelayItem) other;
52             long diff = time - x.time;
53             if (diff < 0)
54                 return -1;
55             else if (diff > 0)
56                 return 1;
57             else if (sequenceNumber < x.sequenceNumber) // 如果是同时进入队列的,则先进者先出
58                 return -1;
59             else
60                 return 1;
61         }
62         long d = (getDelay(TimeUnit.NANOSECONDS) - other
63                 .getDelay(TimeUnit.NANOSECONDS));
64         return (d == 0) ? 0 : ((d < 0) ? -1 : 1);
65     }
66 }
View Code

JavaSerializer.java

 1 import java.io.ByteArrayInputStream;
 2 import java.io.ByteArrayOutputStream;
 3 import java.io.IOException;
 4 import java.io.ObjectInputStream;
 5 import java.io.ObjectOutputStream;
 6 
 7 public class JavaSerializer {
 8 
 9     public static Object deepCopy(Object obj) throws IOException,
10             ClassNotFoundException {
11         // 将该对象序列化成流,因为写在流里的是对象的一个拷贝,而原对象仍然存在于JVM里面。所以利用这个特性可以实现对象的深拷贝
12         ByteArrayOutputStream bos = new ByteArrayOutputStream();
13         ObjectOutputStream oos = new ObjectOutputStream(bos);
14         oos.writeObject(obj);// 要写入ObjectOutputStream的话必须实现Serializable接口
15         // 将流序列化成对象
16         ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
17         ObjectInputStream ois = new ObjectInputStream(bis);
18         return ois.readObject();
19     }
20 }
View Code

Redis省内存的技巧

 redis自带持久化功能,当它决定要把哪些数据换出内存写入磁盘时,使用的也是LRU算法。同时redis也有timeout机制,但它不像上面的TimeoutCache.java类一样开个无限循环的线程去扫描到期的元素,而是每次get元素时判断一个该元素有没有到期,所以redis中一个元素的存活时间远远超出了设置的时间是很正常的。

本节想讲的重点其实是redis省内存的技巧,这也是实践中经常遇到的问题,因为内存总是很昂贵的,运维大哥总是很节约的。在我们的推荐系数中使用Redis来存储信息的索引,没有使用Lucene是因为Lucene不支持分布式,但是省内存的技巧都是从Lucene那儿学来的。

首先,如果你想为redis节省内存那你就不能再用类型的key-value结构,必须全部将它们序列化成二进制的形式。我写了一个工具类,实现各种数据类型和byte[]的互相置换。

DataTransform.java

  1 import java.nio.ByteBuffer;
  2 import java.util.ArrayList;
  3 import java.util.List;
  4 
  5 /**
  6  * 各种数据类型的相互转换
7 *
    8 *
  • {@code <<} 左移,符号位不动 9 *
  • {@code >>} 右移,符号位不动 10 *
  • {@code >>>} 循环右移,符号位要跟着移,高位用0填充 11 *
12 * 位移运算只对32位和64位值有意义。位移运算返回一个新值,但是不改变原值。 13 * 14 * @Author:zhangchaoyang 15 * @Since:2014-7-9 16 * @Version: 17 */ 18 public class DataTransform { 19 20 private static final char[] Digit = { '0', '1', '2', '3', '4', '5', '6', 21 '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; 22 23 /** 24 * byte数组转换成int 25 * 26 * @param bRefArr 27 * byte数组 28 * @param LowEndian 29 * byte数组是否按小端字节序存储 30 * @return int值 31 * @throws ArgumentException 32 * byte数组长度超过4时抛出该异常 33 */ 34 public static int bytesToInt(byte[] bRefArr, boolean LowEndian) 35 throws ArgumentException { 36 int len = bRefArr.length; 37 if (len > 4) { 38 throw new ArgumentException("字节数组长度不能超过4"); 39 } 40 41 int iOutcome = 0; 42 byte bLoop; 43 for (int i = 0; i < len; i++) { 44 bLoop = bRefArr[i]; 45 int shift; 46 if (LowEndian) { 47 shift = i; 48 } else { 49 shift = len - 1 - i; 50 } 51 iOutcome += (bLoop & 0xFF) << (8 * shift);// 之所以要跟0xFF进行与运行是为了把bLoop转换成int,去除符号位的影响 52 } 53 return iOutcome; 54 } 55 56 /** 57 * byte数组转换成long 58 * 59 * @param bRefArr 60 * byte数组 61 * @param LowEndian 62 * byte数组是否按小端字节序存储 63 * @return long值 64 * @throws ArgumentException 65 * byte数组长度超过8时抛出该异常 66 */ 67 public static long bytesToLong(byte[] bRefArr, boolean LowEndian) 68 throws ArgumentException { 69 int len = bRefArr.length; 70 if (len > 8) { 71 throw new ArgumentException("字节数组长度不能超过8"); 72 } 73 74 long iOutcome = 0; 75 byte bLoop; 76 for (int i = 0; i < len; i++) { 77 bLoop = bRefArr[i]; 78 int shift; 79 if (LowEndian) { 80 shift = i; 81 } else { 82 shift = len - 1 - i; 83 } 84 iOutcome += (bLoop & 0xFFL) << (8 * shift);// 之所以要跟0xFFL进行与运行是为了把bLoop转换成long,去除符号位的影响 85 } 86 return iOutcome; 87 } 88 89 /** 90 * byte数组转换成double 91 * 92 * @param bRefArr 93 * byte数组 94 * @param LowEndian 95 * byte数组是否按小端字节序存储 96 * @return double值 97 * @throws ArgumentException 98 * byte数组长度超过8时抛出该异常 99 */ 100 public static double bytesToDouble(byte[] bRefArr, boolean LowEndian) 101 throws ArgumentException { 102 long l = bytesToLong(bRefArr, LowEndian); 103 return Double.longBitsToDouble(l); 104 } 105 106 /** 107 * int转换为byte数组,采用大端字节序会更快一些 108 * 109 * @param number 110 * int数 111 * @param LowEndian 112 * byte数组是否按小端字节序存储 113 * @return byte数组 114 */ 115 public static byte[] intToBytes(int number, boolean LowEndian) { 116 int len = 4; 117 byte[] rect = new byte[len]; 118 for (int i = 0; i < len; i++) { 119 rect[i] = (byte) (number >>> (len - 1 - i) * 8); 120 } 121 if (LowEndian) { 122 for (int i = 0; i < len / 2; i++) { 123 byte swap = rect[i]; 124 rect[i] = rect[len - i - 1]; 125 rect[len - i - 1] = swap; 126 } 127 } 128 return rect; 129 } 130 131 /** 132 * 仿照Lucene的可变长度整型:最高位表示是否还有字节要读取,低七位就是就是具体的有效位,添加到结果数据中.
133 * 比如00000001 最高位表示0,那么说明这个数就是一个字节表示,有效位是后面的七位0000001,值为1。10000010 00000001 134 * 第一个字节最高位为1 135 * ,表示后面还有字节,第二位最高位0表示到此为止了,即就是两个字节,那么具体的值注意,是从最后一个字节的七位有效数放在最前面,依次放置 136 * ,最后是第一个自己的七位有效位,所以这个数表示 0000001 0000010,换算成整数就是130。
137 * 用VInt来表示Integer.MAX_VALUE时需要5个字节. 138 * 139 * @param num 140 * @return 141 */ 142 public static byte[] vintToByte(int num) { 143 ByteBuffer buffer = ByteBuffer.allocate(32); 144 while ((num & ~0x7F) != 0) { 145 buffer.put((byte) ((num & 0x7F) | 0x80)); 146 num >>>= 7;// 等价于num=num>>>7; 147 } 148 buffer.put((byte) num); 149 byte[] rect = new byte[buffer.position()]; 150 buffer.flip(); 151 buffer.get(rect); 152 return rect; 153 } 154 155 public static byte[] vintArrToByteArr(int[] arr) { 156 ByteBuffer buffer = ByteBuffer.allocate(32 * arr.length); 157 for (int ele : arr) { 158 byte[] brr = vintToByte(ele); 159 buffer.put(brr); 160 } 161 byte[] rect = new byte[buffer.position()]; 162 buffer.flip(); 163 buffer.get(rect); 164 return rect; 165 } 166 167 /** 168 * 仿照Lucene的可变长度整型 169 * 170 * @see #vintToByte 171 * @param bytes 172 * @return 173 */ 174 public static int byteToVInt(byte[] bytes) { 175 int i = 0; 176 byte b = bytes[i++]; 177 int num = b & 0x7F; 178 for (int shift = 7; (b & 0x80) != 0; shift += 7) { 179 b = bytes[i++]; 180 num |= (b & 0x7F) << shift; 181 } 182 return num; 183 } 184 185 public static int[] byteArrToVIntArr(byte[] bytes) { 186 List list = new ArrayList(); 187 int i = 0; 188 while (i < bytes.length) { 189 byte b = bytes[i++]; 190 int num = b & 0x7F; 191 for (int shift = 7; (b & 0x80) != 0; shift += 7) { 192 b = bytes[i++]; 193 num |= (b & 0x7F) << shift; 194 } 195 list.add(num); 196 } 197 int[] rect = new int[list.size()]; 198 for (int j = 0; j < rect.length; j++) { 199 rect[j] = list.get(j); 200 } 201 return rect; 202 } 203 204 /** 205 * 仿照Lucene的可变长度整型 206 * 207 * @see #vintToByte 208 * @param num 209 * @return 210 */ 211 public static byte[] vlongToByte(long num) { 212 ByteBuffer buffer = ByteBuffer.allocate(64); 213 while ((num & ~0x7F) != 0) { 214 buffer.put((byte) ((num & 0x7F) | 0x80)); 215 num >>>= 7; 216 } 217 buffer.put((byte) num); 218 byte[] rect = new byte[buffer.position()]; 219 buffer.flip(); 220 buffer.get(rect); 221 return rect; 222 } 223 224 /** 225 * 仿照Lucene的可变长度整型 226 * 227 * @see #vintToByte 228 * @param bytes 229 * @return 230 */ 231 public static long byteToVLong(byte[] bytes) { 232 int i = 0; 233 byte b = bytes[i++]; 234 long num = b & 0x7FL; 235 for (int shift = 7; (b & 0x80) != 0; shift += 7) { 236 b = bytes[i++]; 237 num |= (b & 0x7FL) << shift; 238 } 239 return num; 240 } 241 242 /** 243 * long转换为byte数组 244 * 245 * @param number 246 * long数 247 * @param LowEndian 248 * byte数组是否按小端字节序存储 249 * @return byte数组,长度为8 250 */ 251 public static byte[] longToBytes(long number, boolean LowEndian) { 252 int len = 8; 253 byte[] rect = new byte[len]; 254 for (int i = 0; i < len; i++) { 255 rect[i] = (byte) (number >>> (len - 1 - i) * 8); 256 } 257 if (LowEndian) { 258 for (int i = 0; i < len / 2; i++) { 259 byte swap = rect[i]; 260 rect[i] = rect[len - i - 1]; 261 rect[len - i - 1] = swap; 262 } 263 } 264 return rect; 265 } 266 267 /** 268 * double转换为byte数组 269 * 270 * @param number 271 * double数值 272 * @param LowEndian 273 * byte数组是否按小端字节序存储 274 * @return byte数组,长度为8 275 */ 276 public static byte[] doubleToBytes(double number, boolean LowEndian) { 277 long l = Double.doubleToLongBits(number); 278 return longToBytes(l, LowEndian); 279 } 280 281 /** 282 * IP转换成int值,int在全域上和IP是一一对应的 283 * 284 * @param ip 285 * @return 286 * @throws ArgumentException 287 * IP范围超界时抛出该异常 288 */ 289 public static int ip2int(String ip) throws ArgumentException { 290 String[] arr = ip.trim().split("\\."); 291 int part1 = Integer.parseInt(arr[0]); 292 int part2 = Integer.parseInt(arr[1]); 293 int part3 = Integer.parseInt(arr[2]); 294 int part4 = Integer.parseInt(arr[3]); 295 if (part1 >= 0 && part1 < 256 && part2 >= 0 && part2 < 256 296 && part3 >= 0 && part3 < 256 && part4 >= 0 && part4 < 256) { 297 // 左移,正数左移之后有可能把最高位变为1,从而成为负数 298 int rect = part1 << 24; 299 rect += part2 << 16; 300 rect += part3 << 8; 301 rect += part4; 302 return rect; 303 } else { 304 throw new ArgumentException("IP范围超界"); 305 } 306 } 307 308 /** 309 * int值转换成IP,int在全域上和IP是一一对应的 310 * 311 * @param number 312 * @return 313 */ 314 public static String int2ip(int number) { 315 StringBuilder sb = new StringBuilder(); 316 int part1 = number >>> 24;// 右移,如果是负数最高位的1会向右移,且最高位变为0 317 int part2 = (0x00ff0000 & number) >>> 16;// 位移的优先级高于与运算的优先级 318 int part3 = (0x0000ff00 & number) >>> 8; 319 int part4 = 0x000000ff & number; 320 sb.append(String.valueOf(part1)); 321 sb.append("."); 322 sb.append(String.valueOf(part2)); 323 sb.append("."); 324 sb.append(String.valueOf(part3)); 325 sb.append("."); 326 sb.append(String.valueOf(part4)); 327 return sb.toString(); 328 } 329 330 /** 331 * 一个将字节转化为十六进制ASSIC码的函数 332 * 333 * @param ib 334 * @return 335 */ 336 public static String byteHEX(byte ib) { 337 char[] ob = new char[2]; 338 ob[0] = Digit[(ib >>> 4) & 0X0F]; 339 ob[1] = Digit[ib & 0X0F]; 340 String s = new String(ob); 341 return s; 342 } 343 344 public static String byteHEX(byte[] bytes) { 345 StringBuilder sb = new StringBuilder(); 346 for (byte ib : bytes) { 347 char[] ob = new char[2]; 348 ob[0] = Digit[(ib >>> 4) & 0X0F]; 349 ob[1] = Digit[ib & 0X0F]; 350 String s = new String(ob); 351 sb.append(s); 352 } 353 return sb.toString(); 354 } 355 356 /** 357 * 把一个byte表示成二进制的字符串字面值 358 * 359 * @param ib 360 * @return 361 */ 362 public static String byteLiteral(byte ib) { 363 StringBuilder sb = new StringBuilder(); 364 for (int i = 7; i >= 0; i--) { 365 int v = (ib >>> i) & 0x01; 366 if (v == 0) { 367 sb.append("0"); 368 } else { 369 sb.append("1"); 370 } 371 } 372 return sb.toString(); 373 } 374 375 public static String byteLiteral(byte[] ib) { 376 StringBuilder sb = new StringBuilder(); 377 for (int i = 0; i < ib.length; i++) { 378 sb.append(byteLiteral(ib[i])); 379 } 380 return sb.toString(); 381 } 382 }
View Code

请留意一下上述代码中出现了VInt和VLong两种类型,具体看注释。

倒排索引常见的形式为:term -->  [infoid1,infoid2,infoid3...],针对这种形式的索引我们看下如何节省内存。首先value要采用redis中的list结构,而且是list而非list(想省内存就要杜绝使用String,上面已经说过了)。假如infoid是个int,置换成byte[]就要占4个字节,而绝大部分情况下infoid都1000万以内的数字,因此使用VInt只需要3个字节。内存还可以进一步压缩。链表的第1个infoid我们存储它的VInt形式,后面的infoid与infoid1相减,差值也是个1000万以内的数字而且有可能非常小,我们采用VInt存储这个差值最多需要3个字节,有可能只需要1个字节。访问链表中的任意一个元素时都需要先把首元素取出来。

另一种常见的索引形式为:infoid --> infoDetail,infoDetail中包含很多字段,譬如city、valid、name等,通常情况下人们会使用Redis的hash结构来存储实体,而我们现在要做的就是把infoDetail这个实体序列化成尽可能短的字节流。首先city代表城市,本来是个String类型,而city这个东西是可以穷举的,我们事先对所有city进行编号,在redis中只存储city编号即可。valid表示信息是否过期是个bool类型,在java中存储一个bool也需要1个字节,这显然很浪费,本来一个bit就够了嘛,同时city又用不满一个int,所以可以让valid跟city挤一挤,把city左移一位,把valid塞到city的末位上去。

 1 import java.nio.ByteBuffer;
 2 
 3 /**
 4  * 
 5  *@Author:orisun 
 6  *@Since:2016-5-14  
 7  *@Version:1.0
 8  */
 9 public class Info {
10 
11     private int city;
12     private boolean valid;
13     private String name;
14 
15     public byte[] serialize() {
16         ByteBuffer buffer = ByteBuffer.allocate(10);
17         int cv = (city << 1) + (valid ? 1 : 0);
18         byte[] cv_b = DataTransform.intToBytes(cv, false);
19         buffer.put(cv_b);
20         buffer.put(name.getBytes());
21         byte[] rect = new byte[buffer.position()];
22         buffer.flip();
23         buffer.get(rect);
24         return rect;
25     }
26 
27     public static Info deserialize(byte[] value) {
28         if (value == null || value.length <= 4) {
29             return null;
30         }
31         Info inst = new Info();
32         try {
33             int cv = DataTransform.bytesToInt(new byte[] { (byte) value[0],
34                     value[1], value[2], value[3] }, false);
35             inst.setValid(cv % 2 != 0);
36             inst.setCity(cv >> 1);
37             inst.setName(new String(value, 4, value.length - 4));
38         } catch (ArgumentException e) {
39             e.printStackTrace();
40         }
41         return inst;
42     }
43 
44     public int getCity() {
45         return city;
46     }
47 
48     public void setCity(int city) {
49         this.city = city;
50     }
51 
52     public boolean isValid() {
53         return valid;
54     }
55 
56     public void setValid(boolean valid) {
57         this.valid = valid;
58     }
59 
60     public String getName() {
61         return name;
62     }
63 
64     public void setName(String name) {
65         this.name = name;
66     }
67 
68     public static void main(String[] args) {
69         Info inst1 = new Info();
70         inst1.setCity(100);
71         inst1.setValid(true);
72         inst1.setName("pc");
73         Info inst2 = Info.deserialize(inst1.serialize());
74         assert inst1.getCity() == inst2.getCity();
75         assert inst1.getName().equals(inst2.getName());
76         assert inst1.isValid() ^ inst2.isValid();
77     }
78 }
View Code

 

你可能感兴趣的:(常用缓存技术)