Redis(二) - Jedis

Jedis

Java 和 Redis 打交道的 API 客户端。

<dependencies>
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>3.1.0</version>
    </dependency>
    <dependency>
        <groupId>commons-pool</groupId>
        <artifactId>commons-pool</artifactId>
        <version>1.6</version>
    </dependency>
</dependencies>

连接 Redis

public class Test1 {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("192.168.186.128",6379);
        String pong = jedis.ping();
        System.out.println("pong = " + pong);
    }
}

常用 API

package com.zm;
import redis.clients.jedis.Jedis;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

public class testAPI {

    private void testString() {
        Jedis jedis = new Jedis("192.168.186.128", 6379);

        jedis.set("k1", "v1");
        jedis.set("k2", "v2");
        jedis.set("k3", "v3");

        Set<String> set = jedis.keys("*");
        Iterator<String> iterator = set.iterator();
        for (set.iterator(); iterator.hasNext(); ) {
            String k = iterator.next();
            System.out.println(k + " -> " + jedis.get(k));
        }

        // 查看 k2 是否存在
        Boolean k2Exists = jedis.exists("k2");
        System.out.println("k2Exists = " + k2Exists);
        // 查看 k1 的过期时间
        System.out.println(jedis.ttl("k1"));

        jedis.mset("k4", "v4", "k5", "v5");
        System.out.println(jedis.mget("k1", "k2", "k3", "k4", "k5"));
    }

    private void testList() {
        Jedis jedis = new Jedis("192.168.186.128", 6379);

        jedis.lpush("list01", "l1", "l2", "l3", "l4", "l5");
        List<String> list01 = jedis.lrange("list01", 0, -1);
        for (String s : list01) {
            System.out.println(s);
        }
    }

    private void testSet() {
        Jedis jedis = new Jedis("192.168.186.128", 6379);

        jedis.sadd("order", "jd001");
        jedis.sadd("order", "jd002");
        jedis.sadd("order", "jd003");
        Set<String> order = jedis.smembers("order");
        for (String s : order) {
            System.out.println(s);
        }

        jedis.srem("order", "jd002");

        System.out.println(jedis.smembers("order").size());
    }

    private void testHash() {
        Jedis jedis = new Jedis("192.168.186.128", 6379);

        jedis.hset("user1", "username", "renda");
        System.out.println(jedis.hget("user1", "username"));

        HashMap<String, String> map = new HashMap<String, String>();
        map.put("username", "Blair");
        map.put("gender", "female");
        map.put("address", "wuxi");
        map.put("phone", "1523641256");

        jedis.hmset("user2", map);

        List<String> list = jedis.hmget("user2", "username", "phone");
        for (String s : list) {
            System.out.println(s);
        }
    }

    private void testZset() {
        Jedis jedis = new Jedis("192.168.186.128", 6379);

        jedis.zadd("zset01", 60d, "zs1");
        jedis.zadd("zset01", 70d, "zs2");
        jedis.zadd("zset01", 80d, "zs3");
        jedis.zadd("zset01", 90d, "zs4");

        Set<String> zset01 = jedis.zrange("zset01", 0, -1);
        for (String s : zset01) {
            System.out.println(s);
        }
    }

    public static void main(String[] args) {
        testAPI testApi = new testAPI();
        test2Api.testString();
        test2Api.testList();
        test2Api.testSet();
        test2Api.testHash();
        test2Api.testZset();
    }
}

事务

初始化余额和支出

set balance 100
set expense 0
public class TestTransaction {

    public static void main(String[] args) throws InterruptedException {
        Jedis jedis = new Jedis("192.168.186.128",6379);

        int balance = Integer.parseInt(jedis.get("balance"));
        int expense = 10;

        // 监控余额
        jedis.watch("balance");
        // 模拟网络延迟
        Thread.sleep(10000);

        if (balance < expense) {
            // 解除监控
            jedis.unwatch();
            System.out.println("余额不足");
        } else {
            // 开启事务
            Transaction transaction = jedis.multi();
            // 余额减少
            transaction.decrBy("balance", expense);
            // 累计消费增加
            transaction.incrBy("expense", expense);
            // 执行事务
            transaction.exec();
            System.out.println("余额:" + jedis.get("balance"));
            System.out.println("累计支出:" + jedis.get("expense"));
        }
    }

}

模拟网络延迟:10 秒内,使用 linux 窗口修改 balance 为 5 模拟另一个线程的操作,此时因为 balance 被监控到改动,事务将被打断不会提交执行;输出的余额和累计支出将没有变化。

JedisPool

Redis 的连接池技术详情:https://help.aliyun.com/document_detail/98726.html

<dependency>
    <groupId>commons-pool</groupId>
    <artifactId>commons-pool</artifactId>
    <version>1.6</version>
</dependency>

使用单例模式进行优化:

public class JedisPoolUtil {

    private JedisPoolUtil () {
    }

    private volatile static JedisPool jedisPool = null;
    private volatile static Jedis jedis = null;

    /**
     * 返回一个连接池
     */
    private static JedisPool getInstance() {
        // 双层检测锁(企业中用的非常频繁)
        if (jedisPool == null) {
            synchronized (JedisPoolUtil.class) {
                if (jedisPool == null) {
                    JedisPoolConfig config = new JedisPoolConfig();
                    config.setMaxTotal(1000);
                    config.setMaxIdle(30);
                    config.setMaxWaitMillis(60*1000);
                    config.setTestOnBorrow(true);
                    jedisPool = new JedisPool(config, "192.168.186.128", 6379);
                }
            }
        }
        return jedisPool;
    }

    /**
     * 返回 jedis 对象
     */
    public static Jedis getJedis() {
        if (jedis == null) {
            jedis = getInstance().getResource();
        }
        return jedis;
    }

}

测试类:

public class TestJedisPool {

    public static void main(String[] args) {
        Jedis jedis1 = JedisPoolUtil.getJedis();
        Jedis jedis2 = JedisPoolUtil.getJedis();

        System.out.println(jedis1 == jedis2);
    }

}

高并发下的分布式锁

经典案例:秒杀,抢购优惠券等。

使用 Linux 窗口的 Redis Client 执行 set phone 10 设置测试案例的商品。

搭建工程并测试单线程

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.zm</groupId>
    <artifactId>high-concurrency-redis</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <!-- 指定编码及版本 -->
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.encoding>UTF-8</maven.compiler.encoding>
        <java.version>1.11</java.version>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.2.7.RELEASE</version>
        </dependency>
        <!-- 实现分布式锁的工具类 -->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.6.1</version>
        </dependency>
        <!-- spring 操作 redis 的工具类 -->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-redis</artifactId>
            <version>2.3.2.RELEASE</version>
        </dependency>
        <!-- redis 客户端 -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.1.0</version>
        </dependency>
        <!-- json 解析工具 -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.9.8</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.tomcat.maven</groupId>
                <artifactId>tomcat7-maven-plugin</artifactId>
                <configuration>
                    <port>8001</port>
                    <path>/</path>
                </configuration>
                <executions>
                    <execution>
                        <!-- 打包完成后,运行服务 -->
                        <phase>package</phase>
                        <goals>
                            <goal>run</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

src\main\webapp\WEB-INF\web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         id="WebApp_ID" version="3.1">

    <servlet>
        <servlet-name>springmvc</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring/spring.xml</param-value>
        </init-param>
    </servlet>
    <servlet-mapping>
        <servlet-name>springmvc</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

src\main\resources\spring\spring.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="com.zm.controller"/>

    <bean id="connectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
        <property name="hostName" value="192.168.186.128"/>
        <property name="port" value="6379"/>
    </bean>
    <!-- spring 为连接 redis,提供的一个模版工具类 -->
    <bean id="stringRedisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate">
        <property name="connectionFactory" ref="connectionFactory"/>
    </bean>

</beans>

com.zm.controller.TestConcurrency

@Controller
public class TestConcurrency {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 只能解决一个 tomcat 的并发问题:
     * synchronized 锁只解决了一个进程下的线程并发;
     * 如果分布式环境,多个进程并发,这种方案就失效了。
     */
    @RequestMapping("purchase")
    @ResponseBody
    public synchronized String purchase() {
        // 1.从 redis 中获取手机的库存数量
        int phoneCount = Integer.parseInt(stringRedisTemplate.opsForValue().get("phone"));
        // 2.判断手机的数量是否够秒杀
        if (phoneCount > 0) {
            phoneCount--;
            // 库存减少后,再将库存的值保存回 redis
            stringRedisTemplate.opsForValue().set("phone", phoneCount + "");
            System.out.println("库存减一,剩余:" + phoneCount);
        } else {
            System.out.println("库存不足");
        }
        return "over";
    }
}

高并发测试

  1. 启动两次工程,端口号分别 8001 和 8002。

  2. 使用 nginx 做负载均衡:

# 配置 Redis 多进程测试
upstream zm{
    server 192.168.1.116:8001;
    server 192.168.1.116:8002;
}

server {
    listen       80;
    server_name  www.redistest.com;
    location / {
        proxy_pass http://zm;
        index index.html index.htm;
    }
}

重新启动 Nginx:

/usr/local/nginx/sbin/nginx -s stop
/usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf

使用 SwitchHosts 编辑本地 host 地址:

# Redis
192.168.186.128 www.redistest.com

使用 Linux 窗口的 Redis Client 执行 set phone 20 设置测试案例的商品为 20 个。

  1. 使用 JMeter 模拟 1 秒内发出 100 个 http 请求,会发现同一个商品会被两台服务器同时抢购。

实现 Redis 的分布式锁的思路

  1. 因为 redis 是单线程的,所以命令也就具备原子性,使用 setnx (判断如果不存在才执行 set)命令实现锁,保存 key / value。如果 key 不存在,则执行 set key value 给当前线程加锁,执行完成后,删除 key 表示释放锁;如果 key 已存在,阻塞线程执行,表示有锁。

  2. 如果加锁成功,在执行业务代码的过程中出现异常,导致没有删除 key(释放锁失败),那么就会造成死锁(后面的所有线程都无法执行)。为了解决这个问题,可以设置过期时间,例如 10 秒后,Redis 自动删除。

  3. 高并发下,由于时间段等因素导致服务器压力过大或过小,每个线程执行的时间不同:第一个线程,执行需要 13 秒,执行到第 10 秒时,redis 的 key 自动过期了(释放锁);第二个线程,执行需要 7 秒,加锁,执行第 3 秒(锁被释放了,为什么,是因为被第一个线程的 finally 主动 deleteKey 释放掉了)。。。。连锁反应,当前线程刚加的锁,就被其他线程释放掉了,周而复始,导致锁会永久失效。

  4. 给每个线程加上唯一的标识 UUID 随机生成,释放的时候判断是否是当前的标识即可。

  5. 另外,还需要考虑过期时间如果设定。如果 10 秒太短不够用怎么办?设置 60 秒,太长又浪费时间。可以开启一个定时器线程,当过期时间小于总过期时间的 1/3 时,增长总过期时间。

Redisson

Redis 是最流行的 NoSQL 数据库解决方案之一,而 Java 是最流行的编程语言之一。

虽然两者看起来很自然地在一起,但是 Redis 其实并没有对 Java 提供原生支持。

相反,作为 Java 开发人员,想在程序中集成 Redis,必须使用 Redis 的第三方库。

而 Redisson 就是用于在 Java 程序中操作 Redis 的库,可以在程序中轻松地使用 Redis。

Redisson 在 java.util 中常用接口的基础上,提供了一系列具有分布式特性的工具类。

@Controller
public class TestConcurrency {

    @Autowired
    private Redisson redisson;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Bean
    public Redisson redisson() {
        Config config = new Config();
        // 使用单个 redis 服务器
        config.useSingleServer().setAddress("redis://192.168.186.128:6379").setDatabase(0);
        // 如果使用集群 redis:
        // config.useClusterServers().setScanInterval(2000).addNodeAddress("redis://192.168.186.128:6379","redis://192.168.186.129:6379","redis://192.168.186.130:6379");
        return (Redisson) Redisson.create(config);
    }

    @RequestMapping("purchase")
    @ResponseBody
    public synchronized String purchase() {
        // 定义商品 id,写死
        String productKey = "HUAWEI-P40";
        // 通过 redisson 获取锁(底层源码就是集成了 setnx,过期时间等操作)
        RLock rLock = redisson.getLock(productKey);
        // 上锁(过期时间为 30 秒)
        rLock.lock(30, TimeUnit.SECONDS);

        try {
            // 1.从 redis 中获取手机的库存数量
            int phoneCount = Integer.parseInt(stringRedisTemplate.opsForValue().get("phone"));
            // 2.判断手机的数量是否够秒杀
            if (phoneCount > 0) {
                phoneCount--;
                // 库存减少后,再将库存的值保存回 redis
                stringRedisTemplate.opsForValue().set("phone", phoneCount + "");
                System.out.println("库存减一,剩余:" + phoneCount);
            } else {
                System.out.println("库存不足");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 释放锁
            rLock.unlock();
        }
        return "over";
    }

}

实现分布式锁的方案有很多,比如 ZooKeeper 的分布式锁特点就是高可靠性,Redis 的分布式锁的特点就是高性能。

目前分布式锁应用最多的仍然是 Redis。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,496评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,407评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,632评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,180评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,198评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,165评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,052评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,910评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,324评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,542评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,711评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,424评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,017评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,668评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,823评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,722评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,611评论 2 353

推荐阅读更多精彩内容