这是我参与8月更文挑战的第2天,活动详情查看:8月更文挑战
一、实战
使用 jedis,测试环境:
pom.xml配置- 测试代码
pom.xml
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.5.2</version>
</dependency>
- 测试代码
public class JedisTest {
private Jedis jedis;
@Before
public void setUp() {
jedis = new Jedis("127.0.0.1", 6379);
}
@After
public void after() {
jedis.close();
}
@Test
public void testCache() {
// 最简单的设置缓存
jedis.set("key1", "value1");
System.out.println(jedis.get("key1"));
}
}
(1)最简单的分布式锁
分布式锁,方式有:
nx方式- 企业级分布式锁:基于
redis的分布式锁的企业级实现,在redisson框架里的源码 - 基于复杂的
lua脚本去实现的
最简单的命令:set key value nx
- 这个
key此时是不存在的,才能设置成功 - 如果说
key要是存在了,此时设置是失败的
public class JedisTest {
@Test
public void testNx() {
// 最简单的基于 nx 选项实现的分布式锁
String result = jedis.set("lock_test", "value_test",
SetParams.setParams().nx());
System.out.println("第一次加锁的结果:" + result);
result = jedis.set("lock_test", "value_test",
SetParams.setParams().nx());
System.out.println("第二次加锁的结果:" + result);
}
}
输出结果:
第一次加锁的结果:OK
第二次加锁的结果:null
(2)博客网站
1. 文章发布、查看、字数统计、文章预览
所需命令如下:
mset:批量设置多个key的valuemget:批量获取多个key的valuemsetnx:在多个key都不存在的情况下,一次性设置多个key的valuestrlen:统计长度getrange:获取一定范围数据
模拟需求:将一篇博客,落数据库,并将数据双写入缓存中
public class JedisTest {
@Test
public void testBlogPush() {
// 1. 博客的发布、修改与查看
/*jedis.mset("article:1:title", "redis实战",
"article:1:content", "jedis使用",
"article:1:author", "donald",
"article:1:time", "2021-07-31");*/
Long publishBlogResult = jedis.msetnx("article:1:title", "redis实战",
"article:1:content", "jedis使用",
"article:1:author", "donald",
"article:1:time", "2021-07-31");
System.out.println("发布的博客的结果: " + publishBlogResult);
// 批量获取
List<String> blog = jedis.mget("article:1:title", "article:1:content",
"article:1:author", "article:1:time");
System.out.println("查看博客:" + blog);
// 批量修改
String updateBlogResult = jedis.mset("article:1:title", "修改了标题",
"article:1:content", "修改后的文章内容");
System.out.println("修改博客的结果:" + updateBlogResult);
// 2. 统计字数, 注意中英文长度不同
Long blogLen = jedis.strlen("article:1:content");
System.out.println("博客的长度统计:" + blog);
// 3. 文章预览
String blogContentPreview = jedis.getrange("article:1:content", 0, 5);
System.out.println("博客内容预览: " + blogContentPreview);
}
}
输出结果:
发布的博客的结果: 1
查看博客:[修改了标题, 修改后的文章内容, donald, 2021-07-31]
修改博客的结果:OK
博客的长度统计:[修改了标题, 修改后的文章内容, donald, 2021-07-31]
博客内容预览: 修改
2. 点赞次数计数器
主要命令:
incr:增加decr:减少
public class JedisTest {
@Test
public void testBlogLike() {
// 博客的点赞计数器
for (int i = 0; i < 10; ++i) {
jedis.incr("article:1:like");
}
Long likeCounter = Long.valueOf(jedis.get("article:1:like"));
System.out.println("博客的点赞次数为:" + likeCounter);
jedis.decr("article:1:like");
likeCounter = Long.valueOf(jedis.get("article:1:like"));
System.out.println("再次查看博客的点赞次数为:" + likeCounter);
}
}
输出结果:
博客的点赞次数为:10
再次查看博客的点赞次数为:9
3. 用 hash 数据结构代替
redis 的 hash 数据结构,即 Java 中的 HashMap 数据结构来存储的。
如果一些对象不用
hash结构来存储的话,比如直接用json串把Java对象序列化成json,然后用key-value对的字符串形势放在redis里,但是操作起来不是太方便。
相关命令:
hexists:判断是否存在hmset:一次性设置多个key-value对在hash数据结构里getall hash:一次性把一个hash里全部key-value对拿出来hincr:hash增加
实现如下:
public class JedisTest {
/**
* 发表一篇博客
* @param id 文章Id
* @param blog 文章信息
* @return 是否发布
*/
public boolean publishBlog(long id, Map<String, String> blog) {
if(jedis.hexists("article::" + id, "title")) {
return false;
}
blog.put("content_length", String.valueOf(blog.get("content").length()));
jedis.hmset("article::" + id, blog);
return true;
}
/**
* 查看一篇博客
* @param id 文章Id
* @return 文章信息
*/
public Map<String, String> findBlogById(long id) {
Map<String, String> blog = jedis.hgetAll("article::" + id);
incrementBlogViewCount(id);
return blog;
}
/**
* 更新一篇博客
* @param id 文章Id
* @param updatedBlog 文章
*/
public void updateBlog(long id, Map<String, String> updatedBlog) {
String updatedContent = updatedBlog.get("content");
if(updatedContent != null && !"".equals(updatedContent)) {
updatedBlog.put("content_length", String.valueOf(updatedContent.length()));
}
jedis.hmset("article::" + id, updatedBlog);
}
/**
* 对博客进行点赞
* @param id 文章Id
*/
public void incrementBlogLikeCount(long id) {
jedis.hincrBy("article::" + id, "like_count", 1);
}
/**
* 增加博客浏览次数
* @param id 文章Id
*/
private void incrementBlogViewCount(long id) {
jedis.hincrBy("article::" + id, "view_count", 1);
}
@Test
public void testBlogHash() {
Map<String, String> blog = new HashMap<>();
blog.put("id", String.valueOf(1000));
blog.put("title", "我喜欢学习Redis");
blog.put("content", "学习Redis是一件特别快乐的事情");
blog.put("author", "donald");
blog.put("time", "2020-01-01 10:00:00");
publishBlog(1000, blog);
// 更新一篇博客
Map<String, String> updatedBlog = new HashMap<>();
updatedBlog.put("title", "我特别的喜欢学习Redis");
updatedBlog.put("content", "我平时喜欢到官方网站上去学习Redis");
updateBlog(1000, updatedBlog);
// 有别人点击进去查看你的博客的详细内容,并且进行点赞
Map<String, String> blogResult = findBlogById(1000);
System.out.println("查看博客的详细内容:" + blogResult);
incrementBlogLikeCount(1000);
// 你自己去查看自己的博客,看看浏览次数和点赞次数
blogResult = findBlogById(1000);
System.out.println("自己查看博客的详细内容:" + blogResult);
}
}
输出结果:
查看博客的详细内容:{id=1000, time=2020-01-01 10:00:00, title=我特别的喜欢学习Redis, author=donald, content_length=19, content=我平时喜欢到官方网站上去学习Redis}
自己查看博客的详细内容:{id=1000, time=2020-01-01 10:00:00, like_count=1, title=我特别的喜欢学习Redis, author=donald, content_length=19, content=我平时喜欢到官方网站上去学习Redis, view_count=1}
4. 实现博客网站的分页浏览
实现如下:
public class JedisTest {
/**
* 发表一篇博客
* @param id 文章Id
* @param blog 文章信息
* @return 是否发布
*/
private boolean publishBlog(long id, Map<String, String> blog) {
if(jedis.hexists("article::" + id, "title")) {
return false;
}
blog.put("content_length", String.valueOf(blog.get("content").length()));
jedis.hmset("article::" + id, blog);
jedis.lpush("blog_list", String.valueOf(id));
return true;
}
/**
* 分页查询博客
* @param pageNo 当前页
* @param pageSize 页大小
* @return 博客列表
*/
private List<String> findBlogByPage(int pageNo, int pageSize) {
int startIndex = (pageNo - 1) * pageSize;
int endIndex = pageNo * pageSize - 1;
return jedis.lrange("blog_list", startIndex, endIndex);
}
@Test
public void testBlogList() {
int id = 1001;
// 构造20篇博客数据
for(int i = 0; i < 20; i++) {
id += i;
Map<String, String> map = new HashMap<>();
map.put("id", String.valueOf(id));
map.put("title", "第" + (i + 1) + "篇博客");
map.put("content", "学习第" + (i + 1) + "篇博客,是一件很有意思的事情");
map.put("author", "donald");
map.put("time", "2020-01-01 10:00:00");
publishBlog(id, map);
}
// 有人分页浏览所有的博客,先浏览第一页
int pageNo = 1;
int pageSize = 10;
List<String> blogPage = findBlogByPage(pageNo, pageSize);
System.out.println("展示第一页的博客......");
for(String blogId : blogPage) {
Map<String, String> map = findBlogById(Long.valueOf(blogId));
System.out.println(map.toString());
}
pageNo = 2;
blogPage = findBlogByPage(pageNo, pageSize);
System.out.println("展示第二页的博客......");
for(String blogId : blogPage) {
Map<String, String> map = findBlogById(Long.valueOf(blogId));
System.out.println(map.toString());
}
}
}
输出结果:
展示第一页的博客......
{time=2020-01-01 10:00:00, id=1191, title=第20篇博客, content=学习第20篇博客,是一件很有意思的事情, content_length=19, author=donald}
{time=2020-01-01 10:00:00, id=1172, title=第19篇博客, content_length=19, author=donald, content=学习第19篇博客,是一件很有意思的事情}
{id=1154, time=2020-01-01 10:00:00, title=第18篇博客, content_length=19, content=学习第18篇博客,是一件很有意思的事情, author=donald}
{id=1137, time=2020-01-01 10:00:00, title=第17篇博客, content=学习第17篇博客,是一件很有意思的事情, content_length=19, author=donald}
{time=2020-01-01 10:00:00, id=1121, title=第16篇博客, content_length=19, content=学习第16篇博客,是一件很有意思的事情, author=donald}
{id=1106, time=2020-01-01 10:00:00, title=第15篇博客, author=donald, content=学习第15篇博客,是一件很有意思的事情, content_length=19}
{id=1092, time=2020-01-01 10:00:00, title=第14篇博客, content=学习第14篇博客,是一件很有意思的事情, author=donald, content_length=19}
{id=1079, time=2020-01-01 10:00:00, title=第13篇博客, author=donald, content_length=19, content=学习第13篇博客,是一件很有意思的事情}
{time=2020-01-01 10:00:00, id=1067, title=第12篇博客, author=donald, content=学习第12篇博客,是一件很有意思的事情, content_length=19}
{id=1056, time=2020-01-01 10:00:00, title=第11篇博客, author=donald, content=学习第11篇博客,是一件很有意思的事情, content_length=19}
展示第二页的博客......
{id=1046, time=2020-01-01 10:00:00, title=第10篇博客, content_length=19, author=donald, content=学习第10篇博客,是一件很有意思的事情}
{id=1037, time=2020-01-01 10:00:00, title=第9篇博客, author=donald, content=学习第9篇博客,是一件很有意思的事情, content_length=18}
{id=1029, time=2020-01-01 10:00:00, title=第8篇博客, content=学习第8篇博客,是一件很有意思的事情, author=donald, content_length=18}
{time=2020-01-01 10:00:00, id=1022, title=第7篇博客, author=donald, content=学习第7篇博客,是一件很有意思的事情, content_length=18}
{id=1016, time=2020-01-01 10:00:00, title=第6篇博客, content_length=18, author=donald, content=学习第6篇博客,是一件很有意思的事情}
{id=1011, time=2020-01-01 10:00:00, title=第5篇博客, content_length=18, author=donald, content=学习第5篇博客,是一件很有意思的事情}
{time=2020-01-01 10:00:00, id=1007, title=第4篇博客, content=学习第4篇博客,是一件很有意思的事情, author=donald, content_length=18}
{time=2020-01-01 10:00:00, id=1004, title=第3篇博客, author=donald, content=学习第3篇博客,是一件很有意思的事情, content_length=18}
{id=1002, time=2020-01-01 10:00:00, title=第2篇博客, author=donald, content=学习第2篇博客,是一件很有意思的事情, content_length=18}
{id=1001, time=2020-01-01 10:00:00, title=第1篇博客, author=donald, content_length=18, content=学习第1篇博客,是一件很有意思的事情}
5. 博客网站的文章标签管理
使用 set 数据结构:
sadd:往set中添加
实现如下:
public class JedisTest {
/**
* 发表一篇博客
* @param id 文章Id
* @param blog 文章信息
* @param tags 标签
* @return 是否发布
*/
public boolean publishBlog(long id, Map<String, String> blog, String[] tags) {
if(jedis.hexists("article::" + id, "title")) {
return false;
}
blog.put("content_length", String.valueOf(blog.get("content").length()));
jedis.hmset("article::" + id, blog);
jedis.lpush("blog_list", String.valueOf(id));
jedis.sadd("article::" + id + "::tags", tags);
return true;
}
}
(3)用户操作日志审计功能
需求:对系统上的一些用户的一些核心操作,比如更新一些数据的操作,都是会有一个审计日志的功能,其实就是把你用户在系统里的一些操作记录下来,每一次操作都记录成一条日志,审计日志。
主要命令:append
public class JedisTest {
@Test
public void testUserLog() {
// 操作日志的审计功能
jedis.setnx("operation_log_2021_07_31", "");
for (int i = 0; i < 10; ++i) {
jedis.append("operation_log_2021_07_31", "今天的第" + (i + 1) + "操作日志\n");
}
String operationLog = jedis.get("operation_log_2021_07_31");
System.out.println("今天所有的操作日志: \n" + operationLog);
}
}
输出结果:
今天所有的操作日志:
今天的第1操作日志
今天的第2操作日志
今天的第3操作日志
今天的第4操作日志
今天的第5操作日志
今天的第6操作日志
今天的第7操作日志
今天的第8操作日志
今天的第9操作日志
今天的第10操作日志
(4)实现一个简单的唯一 ID 生成器
需求:ID,都是通过数据库的自增主键来实现的,有一些场景下。分库分表的时候,同一个表里的数据都会分散在多个库的多个表里,这个时候就不能光是靠数据库来生成自增长的主键了,此时就需要生成唯一 ID
主要命令:incr1
public class JedisTest {
@Test
public void testGenerateId() {
// 唯一ID生成器
for (int i = 0; i < 10; ++i) {
Long orderId = jedis.incr("order_id_counter");
System.out.println("生成的第" + (i + 1) + "个唯一ID" + orderId);
}
}
}
输出结果:
生成的第1个唯一ID1
生成的第2个唯一ID2
生成的第3个唯一ID3
生成的第4个唯一ID4
生成的第5个唯一ID5
生成的第6个唯一ID6
生成的第7个唯一ID7
生成的第8个唯一ID8
生成的第9个唯一ID9
生成的第10个唯一ID10
(5)社交网站,点击追踪机制
社交网站(微博)一般会把你发表的一些微博里的长连接转换为短连接,这样可以利用短连接进行点击数量追踪,然后再让你进入短连接对应的长连接地址里去,所以可以利用 hash 数据结构去实现网址点击追踪机制。
# 例如:
http://t.cn/XsGGA9d -> http://redis.com/index.html?dfd=sf& dfd=sf& dfd=sf& dfd=sf& dfd=sf& dfd=sf&
实现步骤如下:
- 利用
redis的incr自增长 - 然后10进制转36进制,接着
hset存放在hash数据结构里 - 再提供一个映射转换的
hget获取方法,hash数据结构,即Java里的HashMap
最后实现结果如下:
short_url_access_count: {
http://t.cn/XsGGA9d: 152,
http://t.cn/I93yUUaF: 269
}
主要命令:
hset:hset hash field value设置key和valuehsetnx:hget:hget hash field
短链接访问次数和生成如下:
public class JedisTest {
private static final String X36 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static final String[] X36_ARRAY = "0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z".split(",");
/**
* 获取短链接地址
* @param url 原地址
* @return 短链接
*/
public String getShortUrl(String url) {
long shortUrlSeed = jedis.incr("short_url_seed");
StringBuffer buffer = new StringBuffer();
while (shortUrlSeed > 0) {
buffer.append(X36_ARRAY[(int)(shortUrlSeed % 36)]);
shortUrlSeed = shortUrlSeed / 36;
}
String shortUrl = buffer.reverse().toString();
jedis.hset("short_url_access_count", shortUrl, "0");
jedis.hset("short_url_mapping", shortUrl, url);
return shortUrl;
}
/**
* 短链接访问次数增长
* @param shortUrl 短链接
*/
public void incrShortUrlAccessCount(String shortUrl) {
jedis.hincrBy("short_url_access_count", shortUrl, 1);
}
public long getShortUrlAccessCount(String shortUrl) {
return Long.valueOf(jedis.hget("short_url_access_count", shortUrl));
}
@Test
public void shotUrl() {
String shortUrl = getShortUrl("http://redis.com/index.html");
System.out.println("页面上展示的短链接地址为:" + shortUrl);
for (int i = 1; i < 155; ++i) {
incrShortUrlAccessCount(shortUrl);
}
long accessCng = getShortUrlAccessCount(shortUrl);
System.out.println("短连接被访问的次数为:" + accessCng);
}
}
输出结果:
页面上展示的短链接地址为:1
短连接被访问的次数为:154
(6)秒杀活动下的公平队列抢购机制
秒杀系统有很多实现方案,其中有一种技术方案,就是对所有涌入系统的秒杀抢购请求,都放入 redis 的一个 list 数据结构里去,进行公平队列排队,然后入队之后就等待秒杀结果,专门搞一个消费者从 list 里按顺序获取抢购请求,按顺序进行库存扣减,扣减成功了就让你抢购成功。
如果说你要是不用公平队列的话,可能就会导致你很多抢购请求进来,大家都在尝试扣减库存,此时可能先涌入进来的请求并没有先对 redis 执行抢购请求,此时可能后涌入进来的请求先执行了抢购请求,此时就是不公平的。
公平队列,基于 redis 里的 list 数据结构:先入先出,先出来的人先抢购,此时就是公平的。
list数据结构:可以理解为是Java里的ArrayList、LinkedList、Queue
操作过程:
- 对于抢购请求入队列:
lpush list request:左边推入 - 对于出队列进行抢购:
rpop list:rpop就是右边弹出
lpush+rpop,就是做了一个左边推入和右边弹出的先入先出的公平队列
# 例如:左边入,右边出
[第3个请求,第2个请求,第1个请求]
实现如下:
public class JedisTest {
@Test
public void testSecKill() {
for (int i = 0; i < 10; ++i) {
enqueueSecKill("第" + (i + 1) + "个秒杀请求");
}
while (true) {
String secKillRequest = dequeueSecKill();
if (secKillRequest == null || "null".equals(secKillRequest)) {
return;
}
System.out.println(secKillRequest);
}
}
private void enqueueSecKill(String request) {
// 秒杀抢购请求入队
jedis.lpush("sec_kill_request_queue", request);
}
private String dequeueSecKill() {
// 秒杀抢购出队列
return jedis.rpop("sec_kill_request_queue");
}
}
输出结果:
第1个秒杀请求
第2个秒杀请求
第3个秒杀请求
第4个秒杀请求
第5个秒杀请求
第6个秒杀请求
第7个秒杀请求
第8个秒杀请求
第9个秒杀请求
第10个秒杀请求