一、本文参考了Redis开发实战指南GitBook,还有《Redis实战》本身以前的笔记。主体框架来自这里。java
二、感谢大佬们的付出,在这里本身只是记录,加深本身的印象。git
三、本文会同步放到我本身的Guo_GitHub,方便本身复习,喜欢的能够点个star。github
四、原谅我如今还没能力本身总结,因此只能临摹别人,加本身的理解,结合本身以前的笔记redis
五、若有拼写错误,还请谅解。有不一样观点,能够评论出来,一块儿努力。数据库
gogogo 咱们正式进入主题吧,浏览器
Redis is an open source (BSD licensed), in-memory data structure store, used as a database, cache and message broker. It supports data structures such as strings, hashes, lists, sets, sorted sets with range queries, bitmaps, hyperloglogs and geospatial indexes with radius queries. Redis has built-in replication, Lua scripting, LRU eviction, transactions and different levels of on-disk persistence, and provides high availability via Redis Sentinel and automatic partitioning with Redis Cluster。缓存
推介看《Redis In Action》很是很是很是不错,尤为前三章。使用Redis的关键点不是设计数据库,而是如何选择合适场景的数据结构。就像咱们以前操做容器时,选择不一样容易装不一样对象。每一个都有各自的各点,具体的内容过两天看源码的时候分享给你们。今天的主题是如下五种数据结构。cookie
主要5种数据结构:session
String数据结构是简单的Key-Value类型,Value不只能够是String,也能够是数字。使用String类型,能够彻底实现目前Memcached
(只有一种String)的功能,而且效率更高,还能够享受Redis的定时持久化(RDB模式或AOF模式),提供日志及Replication等功能。除了提供与Memcached的get、set、incr、decr等操做外,Redis还提供了下面一些操做:数据结构
//一、计数器的使用
String articleId = String.valueOf(conn.incr("article:")); //String.valueOf(int i) : 将 int 变量 i 转换成字符串
复制代码
放一些经常使用命令,方便本身复习
set key value [ex 秒数] / [px 毫秒数] [nx] /[xx] //返回1表示成功,0失败
incr key //对key作加加操做,
decr key //对key作减减操做
setnx key value //仅当key不存在时才set,nx表示not exist。(重点)
mset key1 value1 key2 value2 .。。 //一次设置多个key,
--------------------------------------------------------------------------------
get key //若是key不存在返回null
mget key1 key2 ... keyN //一次获取多个key的值,若是对于的key不存在,则返回null
getset key value //设置新值返回旧值。
复制代码
分布式锁的思路:
伪代码:
# get lock
lock = 0
while lock != 1:
timestamp = current Unix time + lock timeout + 1 //时间戳
lock = SETNX lock.foo timestamp //尝试获取锁,返回0,则下面检查是否超时,GET。
if lock == 1 or (now() > (GET lock.foo) and now() > (GETSET lock.foo timestamp)):
break; //关键点上面这条代码
else:
sleep(10ms)
# do your job
do_job()
# release
if now() < GET lock.foo:
DEL lock.foo
复制代码
gogogo
incr key //对key作加加操做,
decr key //对key作减减操做
incrby key interge //对key加指定的数
incrbyfloat key floatnumber //针对浮点数
append key value //返回新字符的长度
substr key start end //并不会修改字符串,返回截取的字符串
getrange key start end //返回子串
strlen key //取指定key的Value
复制代码
在Memcached中,常常就爱那个一些结构化的信息打包成hashMap,在客户端序列化后存储为一个字符串的值,(一般为JSON格式),好比用户的昵称、年龄、性别、积分。这时候在须要修改其中某一项时,通用须要将字符串(JSON)取出来,而后进行反序列化,修改某一项的值,在序列化成字符串(JSON)存储回去。而Redis和Hash结构可使你像在数据库中Update一个属性同样只修改某一项属性值。
底层实现是hash table,通常操做复杂度是O(1),要同时操做多个field时就是O(N),N是field的数量。应用场景:土法建索引。好比User对象,除了id有时还要按name来查询。
经常使用命令:
hset key field value //设置hash field为指定值,若是key不存在,则先建立
hsetnx //同时,若是存在返回0,nx是not exist的意思
hmset key filed1 value1 ... filedN valueN //设置多个值
hget key field //获取指定的hash field
hmget key field1 field2 //获取所有指定的field
-------------------------------------------------------------------------------
hincrby key field integer //将指定的hash field加上给定值
hexists key field //测试指定field是否存在
hdel key field //删除指定的field
hlen key //返回会指定hash的field数量
hgetall //返回hash全部field和value
复制代码
场景:
/** * 使用Redis从新实现登陆cookie,取代目前由关系型数据库实现的登陆cookie功能 * 一、将使用一个散列来存储登陆cookie令牌与与登陆用户之间的映射。 * 二、须要根据给定的令牌来查找与之对应的用户,并在已经登陆的状况下,返回该用户id。 */
//一、尝试获取并返回令牌对应的用户 注意“login:” 通常使用冒号作分割符,这是不成文的规矩
conn.hget("login:", token);
//二、维持令牌与已登陆用户之间的映射。
conn.hset("login:", token, user);
//六、移除被删除令牌对应的用户信息
conn.hdel("login:", tokens);
---------------------------注意中间还有其余步骤----------------------------------
/** * 使用cookie实现购物车——就是将整个购物车都存储到cookie里面, * 优势:无需对数据库进行写入就能够实现购物车功能, * 缺点:怎是程序须要从新解析和验证cookie,确保cookie的格式正确。而且包含商品能够正常购买 * 还有一缺点:由于浏览器每次发送请求都会连cookie一块儿发送,因此若是购物车的体积较大, * 那么请求发送和处理的速度可能下降。 * ----------------------------------------------------------------- * 一、每一个用户的购物车都是一个散列,存储了商品ID与商品订单数量之间的映射。 * 二、若是用户订购某件商品的数量大于0,那么程序会将这件商品的ID以及用户订购该商品的数量添加到散列里。 * 三、若是用户购买的商品已经存在于散列里面,那么新的订单数量会覆盖已有的。 * 四、相反,若是某用户订购某件商品数量不大于0,那么程序将从散列里移除该条目 * 五、须要对以前的会话清理函数进行更新,让它在清理会话的同时,将旧会话对应的用户购物车也一并删除。 */
//一、从购物车里面移除指定的商品 注意"cart:" 能够在数据迁移、转换 、和删除时轻松识别
conn.hdel("cart:" + session, item);
//二、将指定的商品添加到购物车
conn.hset("cart:" + session, item, String.valueOf(count));
//六、移除被删除令牌对应的用户信息
conn.hdel("login:", tokens);
--------------------------------------------------------------------------------
//五、将文章信息存储到一个散列里面。
//HMSET key field value [field value ...]
//同时将多个 field-value (域-值)对设置到哈希表 key 中。
//此命令会覆盖哈希表中已存在的域。
conn.hmset(article, articleData);
//为哈希表 key 中的域 field 的值加上增量 increment 。
//增量也能够为负数,至关于对给定域进行减法操做。
//HINCRBY counter page_view 200
conn.hincrBy(article, "votes", 1L);
//若是返回1表示尚未这个数据,注意-1后面的L
conn.hincrBy(article, "votes", -1L);
//三、根据文章ID获取文章的详细信息
Map<String,String> articleData = conn.hgetAll(id);
--------------------------测试-------------------------------------------------
//二、测试文章的投票过程
articleVote(conn, "other_user", "article:" + articleId);
String votes = conn.hget("article:" + articleId, "votes");
System.out.println("咱们为该文章投票,目前该文章的票数 " + votes);
assert Integer.parseInt(votes) > 1;
复制代码
其实应用场景还有不少。这里只是摘出程序片断中的一部分,具体能够点这里。须要注意的是组合使用,取其精华、去其糟粕。
List说白了就是链表(Redis使用双端链表实现的List),使用List结构,咱们能够轻松的实现最新消息的排行等功能(好比Twitter的TimeLine),List的另外一个应用就是消息队列,能够利用List的PUSH操做,将任务存在List中,而后工做线POP操做取出任务执行。Redis还提供了操做List中某一段元素的API,能够直接查询,删除List中某一段元素。
命令:
lpush key string //在key对应的list的头部添加元素
rpush key string //在list的尾部添加元素
lpushx key value //若是key不存在,什么都不作
rpushx key value //同上
linsert key BEFORE|AFTER pivot value //在list对应的位置以前或以后
---------------------------------------------------------------------------
llen key //查看列表对应的长度
lindex key index //指定索引的位置,0第一个
lrange key start end //查看一段列表
lrange key 0 -1 // -1表示返回全部数据
ltrim key start end //保留指定区间的元素
lset key index value //idnex表示指定索引的位置
ldel //删除元素
blpop key [key ...] timeout //阻塞队列
brpop key [key ...] timeout
复制代码
基于redis构建消息队列点这里,写的很是不错。为了本身复习,就拿来主义了。
通常来讲,消息队列有两种场景:一种是发布者订阅者模式;一种是生产者消费者模式。利用redis这两种场景的消息队列都可以实现。
一、redis做为消息中间件:
brpop
(阻塞方法)去不断监听该key。// producer code
String key = "demo:mq:test";
String msg = "hello world";
redisDao.lpush(key, msg);
// consumer code
String key = "demo:mq:test";
while (true) {
// block invoke
List<String> msgs = redisDao.brpop(BLOCK_TIMEOUT, listKey); //注意brpop
if (msgs == null) continue;
String jobMsg = msgs.get(1);
processMsg(jobMsg);
}
复制代码
一、使用redis怎么作消息队列
二、订阅-发布系统
Pub/Sub 从字面上理解就是发布(Publish)与订阅(Subscribe),在 Redis 中,你能够设定对某一个 key 值进行消息发布及消息订阅,当一个 key 值上进行了消息发布后,全部订阅它的客户端都会收到相应的消息。这一功能最明显的用法就是用做实时消息系统,好比普通的即时聊天,群聊等功能。
代码实现点这里和上面同一我的,已征求:
要使用Jedis的Publish/Subscribe功能,必须编写对JedisPubSub的本身的实现。
public class MyListener extends JedisPubSub {
// 取得订阅的消息后的处理
@Override
public void onMessage(String channel, String message) {
// TODO Auto-generated method stub
System.out.println(channel + "=" + message);
}
// 取得按表达式的方式订阅的消息后的处理
@Override
public void onPMessage(String pattern, String channel, String message) {
// TODO Auto-generated method stub
System.out.println(pattern + ":" + channel + "=" + message);
}
// 初始化订阅时候的处理
@Override
public void onSubscribe(String channel, int subscribedChannels) {
// TODO Auto-generated method stub
System.out.println("初始化 【频道订阅】 时候的处理 ");
}
// 取消订阅时候的处理
@Override
public void onUnsubscribe(String channel, int subscribedChannels) {
// TODO Auto-generated method stub
System.out.println("// 取消 【频道订阅】 时候的处理 ");
}
// 初始化按表达式的方式订阅时候的处理
@Override
public void onPSubscribe(String pattern, int subscribedChannels) {
// TODO Auto-generated method stub
System.out.println("初始化 【模式订阅】 时候的处理 ");
}
// 取消按表达式的方式订阅时候的处理
@Override
public void onPUnsubscribe(String pattern, int subscribedChannels) {
// TODO Auto-generated method stub
System.out.println("取消 【模式订阅】 时候的处理 ");
}
}
复制代码
二、Sub
public class Sub {
public static void main(String[] args) {
try {
Jedis jedis = getJedis();
MyListener ml = new MyListener();
//能够订阅多个频道
//jedis.subscribe(ml, "liuxiao","hello","hello_liuxiao","hello_redis");
//jedis.subscribe(ml, new String[]{"hello_foo","hello_test"});
//这里启动了订阅监听,线程将在这里被阻塞
//订阅获得信息在lister的onPMessage(...)方法中进行处理
//使用模式匹配的方式设置频道
jedis.psubscribe(ml, new String[]{"hello_*"});
} catch (Exception e) {
e.printStackTrace();
}
}
}
复制代码
须要注意的是:
调用subscribe()或psubscribe() 时,当前线程都会阻塞。
三、Pub
public class Pub {
public static void main(String[] args) {
try {
Jedis jedis = getJedis();
jedis.publish("hello_redis","hello_redis");
} catch (Exception e) {
e.printStackTrace();
}
}
}
复制代码
Set 就是一个集合,集合的概念就是一堆不重复值的组合。利用 Redis 提供的 Set 数据结构,能够存储一些集合性的数据。好比在Twitter应用中,能够将一个用户全部的关注人存在一个集合中,将其全部粉丝存在一个集合。由于 Redis 很是人性化的为集合提供了求交集、并集、差集等操做,那么就能够很是方便的实现如共同关注、共同喜爱、二度好友等功能,对上面的全部集合操做,你还可使用不一样的命令选择将结果返回给客户端仍是存集到一个新的集合中。
命令:
sadd key member //添加元素,成功返回1,
srem key member //移除元素,成功返回1
spop key //删除并返回,若是set是空或者不存在则返回null
srandmember key //同spop,随机取set中一个元素,可是不删除
smove srckey dstkey member //集合间移动元素
scard key //查看集合的大小,若是set是空或者key不存在则返回0
sismember key member //判断member是否在Set中,存在返回1,0表示不存在或key不存在
smembers key //获取全部元素,返回key对应的全部元素,结果是无序的哦
--------------------------------------------------------------------------------
//集合交集
sinter key1 key2 //返回全部给定key的交集
sinterstore dstkey key1 key2 //同sinter,并同时保存并集到dstkey下
//集合并集
sunion key1 key2 //返回全部给定key的并集
sunionstore dstkey key1 key2 //同sunion,并同时保存并集到dstkey下
//集合差集
sdiff key1 key2 //返回给定key的差集
sdiffstore dstkey key1 key2 //同sdiff,并同时保存并集到dstkey下
复制代码
为了防止用户对同一篇文章进行屡次投票,须要为每篇文章记录一个已投票用户名单。使用集合来存储已投票的用户ID。因为集合是不能存储多个相同的元素的,因此不会出现同个用户对同一篇文章屡次投票的状况。 代码实现:
2、程序须要使用SADD将文章发布者的ID添加到记录文章已投票用户名单的集合中
并使用EXPIRE命令为这个集合设置一个过时时间,让Redis在文章发布期满一周以后自动删除这个集合。
//二、添加到记录文章已投用户名单中,
conn.sadd(voted, user);
//三、设置一周为过时时间
conn.expire(voted, ONE_WEEK_IN_SECONDS);
--------------------------------------------------------------------------------
4、检查用户是否第一次为这篇文章投票,若是是第一次,则在增长这篇文章的投票数量和评分。
将一个或多个 member 元素加入到集合 key 当中,已经存在于集合的 member 元素将被忽略。
if (conn.sadd("voted:" + articleId, user) == 1) {
//ZINCRBY salary 2000 tom # tom 加薪啦!
conn.zincrby("score:", VOTE_SCORE, article);
//HINCRBY counter page_view 200
conn.hincrBy(article, "votes", 1L);
}
---------------------------------------------------------------------------------
/** * 记录文章属于哪一个群组 * 将所属一个群组的文章ID记录到那个集合中 * Redis不只能够对多个集合执行操做,甚至在一些状况下,还能够在集合和有序集合之间执行操做 */
//一、构建存储文章信息的键名
String article = "article:" + articleId;
for (String group : toAdd) {
//二、将文章添加到它所属的群组里面
conn.sadd("group:" + group, article);
}
复制代码
Sorted Set的实现是hash table(element->score, 用于实现ZScore及判断element是否在集合内),和skip list(score->element,按score排序)的混合体。 skip list有点像平衡二叉树那样,不一样范围的score被分红一层一层,每层是一个按score排序的链表。 ZAdd/ZRem是O(log(N)),ZRangeByScore/ZRemRangeByScore是O(log(N)+M),N是Set大小,M是结果/操做元素的个数。可见,本来可能很大的N被很关键的Log了一下,1000万大小的Set,复杂度也只是几十不到。固然,若是一次命中不少元素M很大那谁也没办法了。
经常使用命令 :
zadd key score member //添加元素到集合,元素在集合中存在则更新对应的score
zrem key member //1表示成功,若是元素不存在则返回0
zremrangebyrank min max //删除集合中排名在给定的区间
zincrvy key member //增长对于member的score的值。
zcard key //返回集合中元素的个数
//获取排名
zrank key member //返回指定元素在集合中的排名,
zrebrank key member //同时,可是集合中的元素是按score从大到小排序的
//获取排行榜
zrange key start end //相似lrange操做从集合中去指定区间的元素,返回时有序的。
//给给定分数区间的元素
zrangebyscore key min max
//评分的聚合
ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS weight] [AGGREGATE SUM|MIN|MAX]
复制代码
代码实现:
/** * 一、每次用户浏览页面的时候,程序需都会对用户存储在登陆散列里面的信息进行更新, * 二、并将用户的令牌和当前时间戳添加到记录最近登陆用户的集合里。 * 三、若是用户正在浏览的是一个商品,程序还会将商品添加到记录这个用户最近浏览过的商品有序集合里面, * 四、若是记录商品的数量超过25个时,对这个有序集合进行修剪。 */
//一、获取当前时间戳
long timestamp = System.currentTimeMillis() / 1000;
//二、维持令牌与已登陆用户之间的映射。
conn.hset("login:", token, user);
//三、记录令牌最后一次出现的时间
conn.zadd("recent:", timestamp, token);
if (item != null) {
//四、记录用户浏览过的商品
conn.zadd("viewed:" + token, timestamp, item);
//五、移除旧记录,只保留用户最近浏览过的25个商品
conn.zremrangeByRank("viewed:" + token, 0, -26);
//六、为有序集key的成员member的score值加上增量increment。经过传递一个负数值increment 让 score 减去相应的值,
conn.zincrby("viewed:", -1, item);
}
---------------------------------------------------------------------------------
/** *存储会话数据所需的内存会随着时间的推移而不断增长,全部咱们须要按期清理旧的会话数据。 * 一、清理会话的程序由一个循环构成,这个循环每次执行的时候,都会检查存储在最近登陆令牌的有序集合的大小。 * 二、若是有序集合的大小超过了限制,那么程序会从有序集合中移除最多100个最旧的令牌, * 三、并从记录用户登陆信息的散列里移除被删除令牌对应的用户信息, * 四、并对存储了这些用户最近浏览商品记录的有序集合中进行清理。 * 五、于此相反,若是令牌的数量没有超过限制,那么程序会先休眠一秒,以后在从新进行检查。 */
public void run() {
while (!quit) {
//一、找出目前已有令牌的数量。
long size = conn.zcard("recent:");
//二、令牌数量未超过限制,休眠1秒,并在以后从新检查
if (size <= limit) {
try {
sleep(1000);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
continue;
}
long endIndex = Math.min(size - limit, 100);
//三、获取须要移除的令牌ID
Set<String> tokenSet = conn.zrange("recent:", 0, endIndex - 1);
String[] tokens = tokenSet.toArray(new String[tokenSet.size()]);
ArrayList<String> sessionKeys = new ArrayList<String>();
for (String token : tokens) {
//四、为那些将要被删除的令牌构建键名
sessionKeys.add("viewed:" + token);
}
//五、移除最旧的令牌
conn.del(sessionKeys.toArray(new String[sessionKeys.size()]));
//六、移除被删除令牌对应的用户信息
conn.hdel("login:", tokens);
//七、移除用户最近浏览商品记录。
conn.zrem("recent:", tokens);
}
}
}
//七、移除用户最近浏览商品记录。
conn.zrem("recent:", tokens);
复制代码
另一个案例:
/** * 为了应对促销活动带来的大量负载,须要对数据行进行缓存,具体作法是: * 一、编写一个持续运行的守护进程,让这个函数指定的数据行缓存到redis里面,并不按期的更新。 * 二、缓存函数会将数据行编码为JSON字典并存储在Redis字典里。其中数据列的名字会被映射为JSON的字典, * 而数据行的值则被映射为JSON字典的值。 * ----------------------------------------------------------------------------------------- * 程序使用两个有序集合来记录应该在什么时候对缓存进行更新: * 一、第一个为调用有序集合,他的成员为数据行的ID,而分支则是一个时间戳, * 这个时间戳记录了应该在什么时候将指定的数据行缓存到Redis里面 * 二、第二个有序集合为延时有序集合,他的成员也是数据行的ID, * 而分值则记录了指定数据行的缓存须要每隔多少秒更新一次。 * ---------------------------------------------------------------------------------------- * 为了让缓存函数按期的缓存数据行,程序首先须要将hangID和给定的延迟值添加到延迟有序集合里面, * 而后再将行ID和当前指定的时间戳添加到调度有序集合里面。 */
public void scheduleRowCache(Jedis conn, String rowId, int delay) {
//一、先设置数据行的延迟值
conn.zadd("delay:", delay, rowId);
//二、当即对须要行村的数据进行调度
conn.zadd("schedule:", System.currentTimeMillis() / 1000, rowId);
}
--------------------------------------------------------------------------------------
/** * 一、经过组合使用调度函数和持续运行缓存函数,实现类一种重读进行调度的自动缓存机制, * 而且能够为所欲为的控制数据行缓存的更新频率: * 二、若是数据行记录的是特价促销商品的剩余数量,而且参与促销活动的用户特别多的话,那么最好每隔几秒更新一次数据行缓存: * 另外一方面,若是数据并不常常改变,或者商品缺货是能够接受的,那么能够每隔几分钟更新一次缓存。 */
public class CacheRowsThread extends Thread {
private Jedis conn;
private boolean quit;
public CacheRowsThread() {
this.conn = new Jedis("localhost");
this.conn.select(14);
}
public void quit() {
quit = true;
}
public void run() {
Gson gson = new Gson();
while (!quit) {
//一、尝试获取下一个须要被缓存的数据行以及该行的调度时间戳,返回一个包含0个或一个元组列表
Set<Tuple> range = conn.zrangeWithScores("schedule:", 0, 0);
Tuple next = range.size() > 0 ? range.iterator().next() : null;
long now = System.currentTimeMillis() / 1000;
//二、暂时没有行须要被缓存,休眠50毫秒。
if (next == null || next.getScore() > now) {
try {
sleep(50);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
continue;
}
//三、提早获取下一次调度的延迟时间,
String rowId = next.getElement();
double delay = conn.zscore("delay:", rowId);
if (delay <= 0) {
//四、没必要在缓存这个行,将它从缓存中移除
conn.zrem("delay:", rowId);
conn.zrem("schedule:", rowId);
conn.del("inv:" + rowId);
continue;
}
//五、继续读取数据行
Inventory row = Inventory.get(rowId);
//六、更新调度时间,并设置缓存值。
conn.zadd("schedule:", now + delay, rowId);
conn.set("inv:" + rowId, gson.toJson(row));
}
}
}
复制代码
具体的内容看我以前写的笔记购物网站的redis相关实现(Java)和文章投票网站的redis相关实现(Java).
这个Redis主题就算记录完了,gogogo。