Redis(二) - Jedis

Jedis

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


    
        redis.clients
        jedis
        3.1.0
    
    
        commons-pool
        commons-pool
        1.6
    

连接 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 set = jedis.keys("*");
        Iterator 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 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 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 map = new HashMap();
        map.put("username", "Blair");
        map.put("gender", "female");
        map.put("address", "wuxi");
        map.put("phone", "1523641256");

        jedis.hmset("user2", map);

        List 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 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


    commons-pool
    commons-pool
    1.6

使用单例模式进行优化:

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



    4.0.0

    com.zm
    high-concurrency-redis
    1.0-SNAPSHOT
    war

    
    
        UTF-8
        UTF-8
        1.11
        11
        11
    

    
        
            org.springframework
            spring-webmvc
            5.2.7.RELEASE
        
        
        
            org.redisson
            redisson
            3.6.1
        
        
        
            org.springframework.data
            spring-data-redis
            2.3.2.RELEASE
        
        
        
            redis.clients
            jedis
            3.1.0
        
        
        
            com.fasterxml.jackson.core
            jackson-databind
            2.9.8
        
    

    
        
            
                org.apache.tomcat.maven
                tomcat7-maven-plugin
                
                    8001
                    /
                
                
                    
                        
                        package
                        
                            run
                        
                    
                
            
        
    


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




    
        springmvc
        org.springframework.web.servlet.DispatcherServlet
        
            contextConfigLocation
            classpath:spring/spring.xml
        
    
    
        springmvc
        /
    

src\main\resources\spring\spring.xml




    

    
        
        
    
    
    
        
    


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 个。

3. 使用 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。

你可能感兴趣的:(分布式技术,redis,java,数据库)