赞
踩
Strings数据结构是简单的key-value类型,value其实不仅是String,也可以是数字。
set,get,decr,incr,mget 等。
String是最常用的一种数据类型,普通的key/ value 存储都可以归为此类。即可以完全实现目前 Memcached 的功能,并且效率更高。还可以享受Redis的定时持久化,操作日志及 Replication等功能。除了提供与 Memcached 一样的get、set、incr、decr 等操作外,Redis还提供了下面一些操作:
获取字符串长度
往字符串append内容
设置和获取字符串的某一段内容
设置及获取字符串的某一位(bit)
批量设置一系列字符串的内容
String在redis内部存储默认就是一个字符串,被redisObject所引用,当遇到incr,decr等操作时会转成数值型进行计算,此时redisObject的encoding字段为int。
hget,hset,hgetall 等。
使用 hash 数据结构来存储用户信息,商品信息等对象信息。因为它的底层是以hashMap存储的,所以查找效率很快。
在Memcached中,我们经常将一些结构化的信息打包成HashMap,在客户端序列化后存储为一个字符串的值,比如用户的昵称、年龄、性别、积分等,这时候在需要修改其中某一项时,通常需要将所有值取出反序列化后,修改某一项的值,再序列化存储回去。这样不仅增大了开销,也不适用于一些可能并发操作的场合(比如两个并发的操作都需要修改积分)。而Redis的Hash结构可以使你像在数据库中Update一个属性一样只修改某一项属性值。
我们简单举个实例来描述下Hash的应用场景,比如我们要存储一个用户信息对象数据,包含以下信息:
用户ID为查找的key,存储的value用户对象包含姓名,年龄,生日等信息,如果用普通的key/value结构来存储,主要有 2种存储方式。
第一种方式将用户ID作为查找key,把其他信息封装成一个对象以序列化的方式存储,这种方式的缺点是,增加了序列化/反序列化的开销,并且在需要修改其中一项信息时,需要把整个对象取回,并且修改操作需要对并发进行保护,引入CAS等复杂问题。
第二种方法是这个用户信息对象有多少成员就存成多少个key-value对,用 用户ID+对应属性的名称 作为唯一标识来取得对应属性的值,虽然省去了序列化开销和并发问题,但是用户ID为重复存储,如果存在大量这样的数据,内存浪费还是非常可观的。
那么Redis提供的Hash很好的解决了这个问题,Redis的Hash实际是内部存储的Value为一个HashMap,并提供了直接存取这个Map成员的接口。
也就是说,Key仍然是用户ID, value是一个Map,这个Map的key是成员的属性名,value是属性值,这样对数据的修改和存取都可以直接通过其内部Map的 Key(Redis里称内部Map的key为field), 也就是通过 key(用户ID) + field(属性标签) 就可以操作对应属性数据了,既不需要重复存储数据,也不会带来序列化和并发修改控制的问题。很好的解决了问题。
这里同时需要注意,Redis提供了接口(hgetall)可以直接取到全部的属性数据,但是如果内部Map的成员很多,那么涉及到遍历整个内部 Map的操作,由于Redis单线程模型的缘故,这个遍历操作可能会比较耗时,而另其它客户端的请求完全不响应,这点需要格外注意。
上面已经说到Redis Hash对应Value内部实际就是一个HashMap,实际这里会有2种不同实现,这个Hash的成员比较少时Redis为了节省内存会采用类似一维数组的方式来紧凑存储,而不会采用真正的HashMap结构,对应的value redisObject的encoding为zipmap,当成员数量增大时会自动转成真正的HashMap,此时encoding为ht。
lpush,rpush,lpop,rpop,lrange等。
消息队列:lpop和rpush(或者反过来,lpush和rpop)能实现队列的功能
朋友圈的点赞列表、评论列表、排行榜:lpush命令和lrange命令能实现最新列表的功能,每次通过lpush命令往列表里插入新的元素,然后通过lrange命令读取最新的元素列表。
Redis中list的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销,Redis内部的很多实现,包括发送缓冲队列等也都是用的这个数据结构。
sadd,spop,smembers,sunion 等。
Set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set 是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。
Set 集合的概念就是一堆不重复值的组合。利用Redis提供的Sets数据结构,可以存储一些集合性的数据,比如在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis还为集合提供了求交集、并集、差集等操作,可以非常方便的实现如共同关注、共同喜好、二度好友等功能,对上面的所有集合操作,你还可以使用不同的命令选择将结果返回给客户端还是存集到一个新的集合中。
set 的内部实现是一个value永远为null的HashMap,实际就是通过计算hash的方式来快速排重的,这也是set能提供判断一个成员是否在集合内的原因。
zadd,zrange,zrem,zcard等
Sorted set的使用场景与set类似,区别是set不是自动有序的,而sorted set可以通过用户额外提供一个优先级(score)的参数来为成员排序,并且是插入有序的,即自动排序。当你需要一个有序的并且不重复的集合列表,那么可以选择sorted set数据结构,比如:在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息,适合使用 Redis 中的 Sorted Set 结构进行存储。
另外还可以用Sorted Sets来做带权重的队列,比如普通消息的score为1,重要消息的score为2,然后工作线程可以选择按score的倒序来获取工作任务。让重要的任务优先执行。
Redis sorted set的内部使用HashMap和跳跃表(SkipList)来保证数据的存储和有序,HashMap里放的是成员到score的映射,而跳跃表里存放的是所有的成员,排序依据是HashMap里存的score,使用跳跃表的结构可以获得比较高的查找效率,并且在实现上比较简单。
Pub/Sub 从字面上理解就是发布(Publish)与订阅(Subscribe),在Redis中,你可以设定对某一个key值进行消息发布及消息订阅,当一个 key值上进行了消息发布后,所有订阅它的客户端都会收到相应的消息。这一功能最明显的用法就是用作实时消息系统,比如普通的即时聊天,群聊等功能。
谁说NoSQL都不支持事务,虽然Redis的Transactions提供的并不是严格的ACID的事务(比如一串用EXEC提交执行的命令,在执行中服务器宕机,那么会有一部分命令执行了,剩下的没执行),但是这个Transactions还是提供了基本的命令打包执行的功能(在服务器不出问题的情况下,可以保证一连串的命令是顺序在一起执行的,中间有会有其它客户端命令插进来执行)。Redis还提供了一个Watch功能,你可以对一个key进行 Watch,然后再执行Transactions,在这过程中,如果这个Watched的值进行了修改,那么这个Transactions会发现并拒绝执行。
在更新中保持用户对数据的映射是系统中的一个普遍任务。Redis的pub/sub功能使用了SUBSCRIBE、UNSUBSCRIBE和PUBLISH命令,让这个变得更加容易。
进行各种数据统计的用途是非常广泛的,比如想知道什么时候封锁一个IP地址。INCRBY命令让这些变得很容易,通过原子递增保持计数;GETSET用来重置计数器;过期属性expire用来确认一个关键字什么时候应该删除。
代码示例:
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { //访问之前判断是否次数已到 WShop wShop = UserContext.get(); if (wShop == null || StringUtils.isBlank(wShop.getUserAppId())) { throw new SystemException(ResultEnum.RELOGIN); } if (handler instanceof HandlerMethod) { HandlerMethod hm = (HandlerMethod) handler; //拿到注解 Optional.ofNullable(hm.getMethodAnnotation(AccessLimit.class)).ifPresent(accessLimit -> { // 限流次数 int limit = accessLimit.limit(); // 限流时间长度 int timeScope = accessLimit.timeScope(); // 时间单元,天、小时、秒等 TimeUnit timeUnit = accessLimit.timeUnit(); String redisKey = "spider:limit:" + wShop.getUserAppId() + ":" + request.getRequestURI(); if (redisUtils.hasKey(redisKey)) { Object currentCount = redisUtils.get(redisKey); int count = (int) currentCount; if (count < limit) { redisUtils.increment(redisKey, 1L); } else { //超过次数返回提示 if (LIMIT_SET.contains(request.getRequestURI())) throw new CustomizeException(ResultEnum.SPIDER_API_LIMIT); } } else { // 将所有时间单元转换成秒 long seconds = TimeUnit.SECONDS.convert(timeScope, timeUnit); redisUtils.set(redisKey, 1, seconds); } }); } return true; }
代码示例:
// 指定Redis数据库连接的IP和端口 String host = "192.168.33.130"; int port = 6379; Jedis jedis = new Jedis(host, port); /** * 监控变量a在一段时间内是否被修改,若没有,则执行事务,若被修改,则事务不执行 * * @throws Exception */ @Test public void test4() throws Exception { //监控变量a,在事务执行后watch功能也结束 jedis.watch("a"); //需要数据库中先有a,并且a的值为字符串数字 String value = jedis.get("a"); int parseInt = Integer.parseInt(value); parseInt++; System.out.println("线程开始休息。。。"); Thread.sleep(5000); //开启事务 Transaction transaction = jedis.multi(); transaction.set("a", parseInt + ""); //执行事务 List<Object> exec = transaction.exec(); if (exec == null) { System.out.println("事务没有执行....."); } else { System.out.println("正常执行......"); } }
譬如将用戶的好友/粉丝/关注,可以存在一个sorted set中,score可以是timestamp,这样求两个人的共同好友的操作,可能就只需要用求交集命令即可。
例如:(评论,发布商品,论坛发贴,etc)
作为一个电商网站被各种spam攻击是少不免(垃圾评论、发布垃圾商品、广告、刷自家商品排名等),针对这些spam制定一系列anti-spam规则,其中有些规则可以利用redis做实时分析,譬如:1分钟评论不得超过2次、5分钟评论少于5次等(更多机制/规则需要结合drools )。 采用sorted set将最近一天用户操作记录起来(为什么不全部记录?节省memory,全部操作会记录到log,后续利用hadoop进行更全面分析统计)。
redis> RANGEBYSCORE user:200000:operation:comment 61307510405600 +inf //获得1分钟内的操作记录
redis> ZADD user:200000:operation:comment 61307510402300 "这是一条评论" //score 为timestamp (integer) 1
redis> ZRANGEBYSCORE user:200000:operation:comment 61307510405600 +inf //获得1分钟内的操作记录
在逛有个类似微博的栏目我关注,里面包括关注的人、主题、品牌的动态。redis在这边主要当作cache使用。
其实这业务场景也可以算在计数上,也是采用Hash。如下:
存储用户最近五条搜索记录,最近搜索的记录放在前面。此时,redis用作数据库,因为时间设置为不过期。
代码示例:
@Resource private RedisUtils redisUtils; // 根据key和appId获取value public List<Object> searchKey(String businessKey, String appId) { RedisKey redisKey = new RedisKey(businessKey,appId); List<Object> objectResult = redisUtils.lGet(redisKey.getRedisKeyName(), 0, GOODS_SPIDER_CLASSIFY_LENGTH); if (CollectionUtils.isNotEmpty(objectResult)) { return objectResult; } return new ArrayList<>(); } // 添加key public boolean addKey(String businessKey, String appId, List<Map<String,Object>> value) { RedisKey redisKey = new RedisKey(businessKey,appId); Long size = redisUtils.lGetListSize(redisKey.getRedisKeyName()); if (size >= GOODS_SPIDER_CLASSIFY_LENGTH) { // 大于指定个数移除最后一个,塞入第一个 redisUtils.lRightPop(redisKey.getRedisKeyName()); } return redisUtils.lLeftPush(redisKey.getRedisKeyName(), value, 0); } // 删除key public boolean delKey(String businessKey,String appId, List<Map<String,Object>> value) { RedisKey redisKey = new RedisKey(businessKey,appId); redisUtils.lRemove(redisKey.getRedisKeyName(), 1, value); return true; } // 判断是否存在key public boolean existKey(String businessKey,String appId, List<Map<String,Object>> value){ RedisKey redisKey = new RedisKey(businessKey,appId); List<Object> list = redisUtils.lGet(redisKey.getRedisKeyName(), 0, GOODS_SPIDER_CLASSIFY_LENGTH); for (Object object : list){ if (object.equals(value)) { return true; } } return false; }
// 省略业务代码
boolean isExist = goodsSpiderRedis.existKey(RedisConstant.GOODS_SPIDER_CLASSIFY_NAME, appId, classifyList);
if (isExist) {
goodsSpiderRedis.delKey(RedisConstant.GOODS_SPIDER_CLASSIFY_NAME, appId, classifyList);
}
goodsSpiderRedis.addKey(RedisConstant.GOODS_SPIDER_CLASSIFY_NAME, appId, classifyList);
这里采用Redis的List数据结构或sorted set 结构, 方便实现最新列表or排行榜 等业务场景。
这是特定访问者的问题,可以通过给每次页面浏览使用SADD命令来解决。SADD不会将已经存在的成员添加到一个集合。
使用Redis原语命令,更容易实施垃圾邮件过滤系统或其他实时跟踪系统。
当在集群环境时候,java ConcurrentLinkedQueue 就无法满足我们需求,此时可以采用Redis的List数据结构实现分布式的消息队列。
Redis缓存使用的方式与memcache相同。
网络应用不能无休止地进行模型的战争,看看这些Redis的原语命令,尽管简单但功能强大,把它们加以组合,所能完成的就更无法想象。当然,你可以专门编写代码来完成所有这些操作,但Redis实现起来显然更为轻松。
// 将手机验证码存储到redis中,过期时间5分钟
redisTemplate.opsForValue().set(phone, verificationCode, 5L, TimeUnit.MINUTES);
//获取手机验证码;
String currentCode = (String) redisTemplate.opsForValue().get(phone);
拦截器层代码:
@Component @Slf4j public class SyncGoodsLimitInterceptor implements HandlerInterceptor { @Resource private RedisUtils redisUtils; public SyncGoodsLimitInterceptor() { } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { //访问之前判断是否次数已到 WShop wShop = UserContext.get(); if (wShop == null || StringUtils.isBlank(wShop.getUserAppId())) { throw new SystemException(ResultEnum.RELOGIN); } if (handler instanceof HandlerMethod) { HandlerMethod hm = (HandlerMethod) handler; //拿到注解 Optional.ofNullable(hm.getMethodAnnotation(SyncGoodsAccessLimit.class)).ifPresent(accessLimit -> { // 限流最大时间长度 int timeScopeMax = accessLimit.timeScopeMax(); // 限流最小时间长度 int timeScopeMin = accessLimit.timeScopeMin(); // 时间单元,天、小时、秒等 TimeUnit timeUnit = accessLimit.timeUnit().getUnit(); String redisKey = "sync_goods:limit:" + wShop.getUserAppId() + ":" + request.getRequestURI(); Object object = redisUtils.get(redisKey); if (object != null) { long timeDiff = (int) object - System.currentTimeMillis() / 1000; String message = ""; if (timeDiff > 0) { message = String.format(ResultEnum.SYNC_GOODS_TOO_FREQUENTLY.getMsg(), timeDiff); throw new CustomizeException(ResultEnum.SYNC_GOODS_TOO_FREQUENTLY.getCode(), message); } else { throw new CustomizeException(ResultEnum.SYNC_GOODS_TOO_FREQUENTLY2); } } else { // 将所有时间单元转换成秒 long maxSeconds = TimeUnit.SECONDS.convert(timeScopeMax, timeUnit); long minSeconds = TimeUnit.SECONDS.convert(timeScopeMin, timeUnit); redisUtils.set(redisKey, System.currentTimeMillis() / 1000 + minSeconds, maxSeconds); } }); } return true; } }
Controller层代码:
@PostMapping("sync")
@SyncGoodsAccessLimit(timeScopeMin = 30, timeScopeMax = 90, timeUnit = SyncGoodsAccessLimit.Unit.SECONDS)
public ResultData sync() {
return ResultData.success(shopGoodsDomainService.sync());
}
自定义注解限制访问时间长度,最多访问次数:
@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface SyncGoodsAccessLimit { /** * 时间单元,天、小时、秒等,默认单位是秒 */ Unit timeUnit() default Unit.SECONDS; /** * 时间长度,1天,1小时,1秒等,默认是1 */ int timeScopeMin(); int timeScopeMax(); @Getter @AllArgsConstructor enum Unit { MINUTES(TimeUnit.MINUTES,"分钟"), SECONDS(TimeUnit.SECONDS,"秒"), ; TimeUnit unit; String name; } }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。