如何使用redis缓存来实现用户最近浏览的商品列表
首先,我们要弄明白两个要点:最近浏览的商品肯定是一个存和取的两个操作.好了目前摆在我们面前的有以下几个问题:
1,最近浏览的记录肯定是需要失效时间的
确定使用缓存,缓存可以设置失效时间(最大设置为一个月,但是这已经足够了),如果使用mysql等数据库,还需要定时任务清除,很明显是不切合实际的,
2,最近浏览的记录肯定是有个数限制的,不可能记录所有的浏览记录
目前使用的主流缓存有 memached和redis两种(原谅无知的我并不清楚其他的),redis有LTRM来修剪,保证存储的浏览的条数;
3,我们需要在哪里添加保存浏览商品的方法
用户最近浏览的商品,肯定是再用户最近打开商品详情页的时候才算浏览,这点毋庸置疑;
4,怎么保证每次添加的浏览的商品列表按着浏览的先后顺序排序?
每次用户的浏览商品的ID,可以以用户的ID作为key,以List作为value,储存在redis中,而List是有序的,而且,在使用LRANGE的时候能保证先进后出,后进先出的原则,已达到排列在最前面的商品始终是里当前最近浏览的那个商品;
5,怎么保证用户在连续浏览同一个商品的时候,不会重复保存商品?
可以使用redis中LREM来移除列表中与参数 value(该商品ID) 相等的元素。同时在使用Lpush重新再List插入最新的浏览商品;
6,读取缓存的时候,又该如何保证分页?
redis中的LRANGE可以指定获取指定长度的元素,能够满足需要;
下面是简单的实现思路:
1,储存用户浏览的商品:
用户在打开详情页的时候,以用户ID作key,商品的ID做值,以List存入redi缓存中;
在加入添加缓存之前,为了保证浏览商品的 唯一性,每次添加前,使用lrem将缓存的list中该商品ID去掉,在加入,以保证其浏览的最新的商品在最前面;
在lpush到redis的List中之后,根据产品需求还需要将该list的前60个数据之外的缓存修剪掉;
最后添加缓存失效时间30天;
2.获取用户最近浏览的商品列表:
根据用户的ID及当前的页数和每页的个数,来获取商品缓存;
下面是最后实现的代码:
1,根据用户ID和商品Id存入到缓存中:
public void addMemberResentGoods(Long memberId, Long templateId) {
String key = RedisKeyUtil.generteKeyWithPlaceholder(RedisKeys.MEMBER_RECENT_GOODS, memberId);
//为了保证浏览商品的 唯一性,每次添加前,将list 中该 商品ID去掉,在加入,以保证其浏览的最新的商品在最前面
redisService.lrem(key, 1, templateId.toString());
//将value push 到该key下的list中
redisService.lpush(key,templateId.toString());
//使用ltrim将60个数据之后的数据剪切掉
redisService.lTrim(key,0,59);
//设置缓存时间为一个月
redisService.expire(key,60*60*24*30);
}
2,根据用户的ID,分页获取最近浏览的商品:
public Map<String,Object> queryMemberResentGoods(Long memberId, int page, int pageSize) {
String key = RedisKeyUtil.generteKeyWithPlaceholder(RedisKeys.MEMBER_RECENT_GOODS, memberId);
//获取用户的浏览的商品的总页数;
long pageCount = redisService.llen(key);
//根据用户的ID分頁获取该用户最近浏览的50个商品信息
List<String> result = redisService.lrange(key,(page-1)*pageSize,page*pageSize-1);
//拼装返回
Map<String,Object> map = new HashMap<>();
map.put("result",result);
map.put("pageCount",(pageCount%pageSize == 0 ? pageCount/pageSize : pageCount/pageSize+1));
return map;
}
补充: 下面是一些根据业务需要用 jedis 封装的 上面的 redisService
public interface RedisService{
/**
* 根据参数 count 的值,移除列表中与参数 value 相等的元素。
* <p>
* count 的值可以是以下几种:
* <p>
* count > 0 : 从表头开始向表尾搜索,移除与 value 相等的元素,数量为 count 。
* count < 0 : 从表尾开始向表头搜索,移除与 value 相等的元素,数量为 count 的绝对值。
* count = 0 : 移除表中所有与 value 相等的值。
* 返回值:
* 被移除元素的数量。
* 因为不存在的 key 被视作空表(empty list),所以当 key 不存在时, LREM 命令总是返回 0 。
*/
public Long lrem(String key, long count, String value);
/**
* Add the string value to the head (LPUSH) or tail (RPUSH) of the list
* stored at key. If the key does not exist an empty list is created just
* before the append operation. If the key exists but is not a List an error
* is returned.
* <p>
* Time complexity: O(1)
*
* @param key
* @param string
* @return Integer reply, specifically, the number of elements inside the
* list after the push operation.
*/
public Long lpush(final String key, final String string);
/**
* LTRIM操作
* <p>
* 对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。
* <p>
* 举个例子,执行命令 LTRIM list 0 2 ,表示只保留列表 list 的前三个元素,其余元素全部删除。
* <p>
* 下标(index)参数 start 和 stop 都以 0 为底,也就是说,以 0 表示列表的第一个元素,以 1 表示列表的第二个元素,以此类推。
* <p>
* 你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。
* <p>
* 当 key 不是列表类型时,返回一个错误。
*/
String lTrim(String key, final long start, final long end);
/**
* 设置一个键的相对过期时间,O(1)
*
* @param key 键
* @param seconds 过期秒数
* @return true 设置成功,false 设置失败
* @throws JedisConnectionException 若和redis服务器的连接不成功
*/
boolean expire(String key, int seconds);
/**
* Return the specified elements of the list stored at the specified key.
* Start and end are zero-based indexes. 0 is the first element of the list
* (the list head), 1 the next element and so on.
* <p>
* For example LRANGE foobar 0 2 will return the first three elements of the
* list.
* <p>
* start and end can also be negative numbers indicating offsets from the
* end of the list. For example -1 is the last element of the list, -2 the
* penultimate element and so on.
* <p>
* <b>Consistency with range functions in various programming languages</b>
* <p>
* Note that if you have a list of numbers from 0 to 100, LRANGE 0 10 will
* return 11 elements, that is, rightmost item is included. This may or may
* not be consistent with behavior of range-related functions in your
* programming language of choice (think Ruby's Range.new, Array#slice or
* Python's range() function).
* <p>
* LRANGE behavior is consistent with one of Tcl.
* <p>
* <b>Out-of-range indexes</b>
* <p>
* Indexes out of range will not produce an error: if start is over the end
* of the list, or start > end, an empty list is returned. If end is over
* the end of the list Redis will threat it just like the last element of
* the list.
* <p>
* Time complexity: O(start+n) (with n being the length of the range and
* start being the start offset)
*
* @param key
* @param start
* @param end
* @return Multi bulk reply, specifically a list of elements in the
* specified range.
*/
public List<String> lrange(String key, final long start, final long end);
/**
* Return the length of the list stored at the specified key. If the key
* does not exist zero is returned (the same behaviour as for empty lists).
* If the value stored at key is not a list an error is returned.
* <p>
* Time complexity: O(1)
*
* @param key
* @return The length of the list.
*/
public long llen(String key);
}
redisServiceImpl 实现类,这里没有必要完全按照这个来,自己可以直接可以直接 看看 Jedis 里的 方法,自己封装一些自己适合的方法
public class RedisServiceImpl implements RedisService {
private final Logger logger = LoggerFactory.getLogger(com.mamahao.eb.framework.redis.RedisServiceImpl.class);
// redis lua scripts(supportted since redis 2.6)
private static final String CAS_CMD = "local v=redis.call('get',KEYS[1]);local r=v;local n=#ARGV-1;local tb=ARGV[#ARGV];local succ='F';for i=1, n do if(v==ARGV[i]) then redis.call('set',KEYS[1],tb); r=tb; succ='T' break;end end;return {succ,r}";
private static final String SUBS_CMD = "local v=redis.call('incrby',KEYS[1],0);local r=ARGV[1]-v;redis.call('set',KEYS[1],ARGV[1]);return r;";
private volatile String CAS_KEY;
private volatile String SUBS_KEY;
private String redisAddr;
private String[] addrInfo;
private Pool<Jedis> jedisPool;
private String sentinels;
private long redisSubMaintainInterval = 10000;
private int maxActive = 10;
private int maxIdle = 5;
private int timeOut = 2000;
private String password;
public void setPassword(String password) {
this.password = password;
}
public void setRedisAddr(String redisAddr) {
this.redisAddr = redisAddr;
}
public void setMaxActive(int maxActive) {
this.maxActive = maxActive;
}
public void setMaxIdle(int maxIdle) {
this.maxIdle = maxIdle;
}
public void setSentinels(String sentinels) {
this.sentinels = sentinels;
}
public void init() {
if (redisAddr == null || "".equals(redisAddr.trim()) || "NULL".equals(redisAddr.trim())) {
logger.warn("no redis addr was configured,this redis service will be unavaliable");
return;
}
JedisPoolConfig cfg = new JedisPoolConfig();
cfg.setMaxIdle(maxIdle);// 设置最大连接数
cfg.setMaxTotal(maxActive); //add by chenxinchao 20151016
if (!StringUtil.isBlank(sentinels)) {
// addr info is a name rather than ip:port
String[] sentinelsInfo = sentinels.split(",");
Set<String> sentinelSet = new HashSet<>();
for (String s : sentinelsInfo) {
sentinelSet.add(s.trim());
}
if(StringUtils.isNotBlank(password)){
jedisPool = new JedisSentinelPool(redisAddr, sentinelSet, cfg,timeOut,password);
}else {
jedisPool = new JedisSentinelPool(redisAddr, sentinelSet, cfg,timeOut);
}
} else {
addrInfo = redisAddr.split(":");
jedisPool = new JedisPool(cfg, addrInfo[0], Integer.valueOf(addrInfo[1]), timeOut, password);
}
// load cas scripts
Jedis jedis = jedisPool.getResource();
try {
CAS_KEY = jedis.scriptLoad(CAS_CMD);
SUBS_KEY = jedis.scriptLoad(SUBS_CMD);
} finally {
jedis.close();
}
}
public void stop() {
if (jedisPool != null) {
jedisPool.destroy ();
}
}
@Override
public long llen(String key) {
Jedis jedis = jedisPool.getResource();
try {
return jedis.llen(key);
} catch (JedisConnectionException e) {
jedis.close();
throw e;
} finally {
if (jedis != null) {
jedis.close();
}
}
}
@Override
public List<String> lrange(String key, final long start, final long end) {
Jedis jedis = jedisPool.getResource();
try {
return jedis.lrange(key, start, end);
} catch (JedisConnectionException e) {
jedis.close();
throw e;
} finally {
if (jedis != null) {
jedis.close();
}
}
}
@Override
public boolean expire(String key, int seconds) {
Jedis jedis = jedisPool.getResource();
try {
long result = jedis.expire(key, seconds);
return result > 0;
} catch (JedisConnectionException e) {
jedis.close();
throw e;
} finally {
if (jedis != null) {
jedis.close();
}
}
}
@Override
public String lTrim(String key, long start, long end) {
Jedis jedis = jedisPool.getResource();
try {
return jedis.ltrim(key, start, end);
} catch (JedisConnectionException e) {
jedis.close();
throw e;
} finally {
if (jedis != null) {
jedis.close();
}
}
}
@Override
public Long lpush(String key, String string) {
Jedis jedis = jedisPool.getResource();
try {
return jedis.lpush(key, string);
} catch (JedisConnectionException e) {
jedis.close();
throw e;
} finally {
if (jedis != null) {
jedis.close();
}
}
}
@Override
public Long lrem(String key, long count, String value) {
Jedis jedis = jedisPool.getResource();
try {
return jedis.lrem(key, count, value);
} catch (JedisConnectionException e) {
jedis.close();
throw e;
} finally {
if (jedis != null) {
jedis.close();
}
}
}
}