Redis(二) - Jedis

目录

1. Jedis

1.1 连接 Redis

1.2 常用 API

1.3 事务

1.4 JedisPool

2. 高并发下的分布式锁

2.1 搭建工程并测试单线程

2.2 高并发测试

2.3 实现 Redis 的分布式锁的思路

2.4 Redisson实现分布式锁


1. 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>

1.1 连接 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);
    }
}
/** 运行前: 
        1.关闭防火墙 systemctl stop firewalld.service 
        2.修改redis.conf [ bind 0.0.0.0 ] 允许任何ip访问,以这个redis.conf启动redis服务 
         (重启redis) 
          redis-server /opt/redis5.0.4/redis.conf
*/

1.2 常用 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", "zm");
        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();
    }
}

1.3 事务

初始化余额和支出

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 被监控到改动,事务将被打断不会提交执行;输出的余额和累计支出将没有变化。

1.4 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);
    }

}

2. 高并发下的分布式锁

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

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

2.1 搭建工程并测试单线程

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";
    }
}

注意: :使用synchronized 做同步锁只能解决一个 tomcat 的并发问题,也就是只解决了一个进程下的线程并发。如果分布式环境,多个进程并发,这种方案就失效了。

2.2 高并发测试

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

2. 使用 nginx 做负载均衡:

# 配置 Redis 多进程测试
upstream zm{
    server 192.168.186.128:8001;
    server 192.168.186.128: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 个。

3. 使用 JMeter 模拟 1 秒内发出 100 个 http 请求,会发现同一个商品会被两台服务器同时抢购。由此可见,在分布式环境下用synchronized 是行不通的。

2.3 实现 Redis 的分布式锁的思路

1.  因为 redis 是单线程的,所以命令也就具备原子性,使用 setnx (判断如果不存在才执行 set)命令实现锁,保存 key / value。

        \circ  如果 key 不存在,则执行 set key value 给当前线程加锁,执行完成后,删除 key 表示释放锁;

        \circ  如果 key 已存在,阻塞线程执行,表示有锁。 

2.  如果加锁成功,在执行业务代码的过程中出现异常,导致没有删除 key(释放锁失败),那么就会造成死锁(后面的所有线程都无法执行)。

         \circ  为了解决这个问题,可以设置过期时间,例如 10 秒后,Redis 自动删除。 

3.  高并发下,由于时间段等因素导致服务器压力过大或过小,每个线程执行的时间不同:

          \circ  第一个线程,执行需要 13 秒,执行到第 10 秒时,redis 的 key 自动过期了(释放锁);

          \circ  第二个线程,执行需要 7 秒,加锁,执行第 3 秒(锁被释放了,为什么,是因为被第一个线程的 finally 主动 deleteKey 释放掉了)

           \circ   。。。。连锁反应,当前线程刚加的锁,就被其他线程释放掉了,周而复始,导致锁会永久失效。

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

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

2.4 Redisson实现分布式锁

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

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

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

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

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

@Controller
public class TestConcurrency {

    // 引入redisson对象
    @Autowired
    private Redisson redisson;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /*
     创建redisson工具类
    */
    @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";
    }

}

测试:使用 JMeter 模拟 1 秒内发出 100 个 http 请求,结果无误,测试通过。

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值