Java 和 Redis 打交道的 API 客户端。
redis.clients
jedis
3.1.0
commons-pool
commons-pool
1.6
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);
}
}
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 被监控到改动,事务将被打断不会提交执行;输出的余额和累计支出将没有变化。
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 请求,会发现同一个商品会被两台服务器同时抢购。
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 时,增长总过期时间。
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。