Redis 实战学习(上)|8月更文挑战

130 阅读12分钟

这是我参与8月更文挑战的第2天,活动详情查看:8月更文挑战

一、实战

使用 jedis,测试环境:

  1. pom.xml 配置
  2. 测试代码

  1. pom.xml
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.5.2</version>
</dependency>
  1. 测试代码
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)最简单的分布式锁

分布式锁,方式有:

  1. nx 方式
  2. 企业级分布式锁:基于 redis 的分布式锁的企业级实现,在 redisson 框架里的源码
  3. 基于复杂的 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:批量设置多个 keyvalue
  • mget:批量获取多个 keyvalue
  • msetnx:在多个 key 都不存在的情况下,一次性设置多个 keyvalue
  • strlen:统计长度
  • 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 数据结构代替

redishash 数据结构,即 Java 中的 HashMap 数据结构来存储的。

如果一些对象不用 hash 结构来存储的话,比如直接用 json 串把 Java 对象序列化成 json,然后用 key-value 对的字符串形势放在 redis 里,但是操作起来不是太方便。

相关命令:

  • hexists :判断是否存在
  • hmset :一次性设置多个 key-value 对在 hash 数据结构里
  • getall hash:一次性把一个 hash 里全部 key-value 对拿出来
  • hincrhash 增加

实现如下:

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&

实现步骤如下:

  1. 利用 redisincr 自增长
  2. 然后10进制转36进制,接着 hset 存放在 hash 数据结构里
  3. 再提供一个映射转换的 hget 获取方法,hash 数据结构,即 Java 里的 HashMap

最后实现结果如下:

short_url_access_count: {
 http://t.cn/XsGGA9d: 152,
 http://t.cn/I93yUUaF: 269
}

主要命令:

  • hsethset hash field value 设置 keyvalue
  • hsetnx
  • hgethget 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 里的 ArrayListLinkedListQueue

操作过程:

  • 对于抢购请求入队列:lpush list request:左边推入
  • 对于出队列进行抢购:rpop listrpop 就是右边弹出

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个秒杀请求