10. Redis的Java客户端
10.1 启用远程连接
注释掉
bind 127.0.0.1
可以使所有的ip访问redis修改:
protected-mode no
-
修改
daemonize
为yes,以守护进程的方式运行daemonize yes # 根据版本不同可能默认值不同我的默认是yes不需要修改
-
启用redis密码登陆:
config set requirepass 30807 #由于在redis在虚拟机密码没有设置很长
-
设置防火墙规则
iptables -I INPUT -p tcp --dport 6379 -j ACCEPT
10.2 Jedis测试连接
创建maven项目名为redis
-
pom.xml中引入依赖
<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>3.0.1</version> <type>jar</type> <scope>compile</scope> </dependency>
-
创建测试类
TestPing
@Test public void testPing(){ Jedis jedis = new Jedis("192.168.93.131", 6379); jedis.auth("30807");//验证密码 System.out.println(jedis.ping()); jedis.close(); }
10.3 Jedis的基本操作
public class TestAPI {
private static String url = "192.168.93.131";
private static int port = 6379;
public Jedis getJedis(){
Jedis jedis = new Jedis(url, port);
jedis.auth("30807");//验证密码
return jedis;
}
//测试set操作
@Test
public void testSet(){
Jedis jedis = getJedis();
jedis.set("k1", "v1");
jedis.set("k2", "v2");
jedis.close();
}
//测试get操作
@Test
public void testGet(){
Jedis jedis = getJedis();
System.out.println(jedis.get("k1"));
jedis.close();
}
//测试keys
@Test
public void testKeys(){
Jedis jedis = getJedis();
Set<String> set = jedis.keys("*");
System.out.println(set);
jedis.close();
}
}
jedis的事务:
public class TestTransaction {
public Jedis getJedis(){
Jedis jedis = new Jedis("192.168.93.131", 6379);
jedis.auth("30807");//验证密码
return jedis;
}
@Test
public void testTx(){
Jedis jedis = getJedis();
//开启事务
Transaction transaction = jedis.multi();
transaction.set("k3", "v3");
transaction.set("k4", "v4");
transaction.get("k4");
//提交,并返回执行结果
List<Object> list = transaction.exec();
System.out.println(list);
jedis.close();
}
}
通过上面的代码会发现方法名基本上和redis命令是一样的,但是每次都创建连接使用完关闭连接就很烦。
所以下面演示redis线程池
10.4 JedisPool线程池
Jedis实例不是线程安全的,所以不可以多个线程共用一个Jedis实例,但是创建太多的实现也不好因为这意味着会建立很多sokcet连接。
JedisPool是一个线程安全的网络连接池。可以用JedisPool创建一些可靠Jedis实例,可以从池中获取Jedis实例,使用完后再把Jedis实例还回JedisPool。这种方式可以避免创建大量socket连接并且会实现高效的性能。
JedisPoll有切片非切片之分:
- JedisPool类是非切片连接池适用于单机版,即单节点非集群
- ShardedJedisPool是切片连接池,适用于Redis集群
这里创建JedisPool单机版连接池工具类,一般集群会使用spring-data所以不写ShardedJedisPool工具类了
public class RedisManager {
private JedisPool jedisPool;//非切片连接池
private String connectHost;
private int connectPort;
private String requirePass;
private int maxTotal = 20;//最大连接数
private int maxIdle = 5;//最小空闲数
private long maxWaitMillis = 10000;
//是否在从池中取出连接前进行检验,如果检验失败,则从池中去除连接并尝试取出另一个
private boolean testOnBorrow = true;
//在空闲时检查有效性,默认false
private boolean testWhileIdle = false;
private Properties prop = new Properties();
private volatile static RedisManager redisManager = null;
//创建redisClient时初始化连接池创建jedis并返回
private RedisManager() {
try {
prop.load(RedisManager.class.getClassLoader().getResourceAsStream("redis.properties"));
} catch (IOException e) {
new RuntimeException("File not find:redis.properties is not find in classpth");
}
//读取配置文件
this.connectHost = prop.getProperty("redis.connect.host");
String port = prop.getProperty("redis.connect.port");
if(isNotBlank(port)){
this.connectPort = Integer.parseInt(port);
}
String maxIdle = prop.getProperty("redis.connect.maxIdle");
if(isNotBlank(maxIdle)){
this.maxIdle = Integer.parseInt(maxIdle);
}
String maxTotal = prop.getProperty("redis.connect.maxTotal");
if(isNotBlank(maxTotal)){
this.maxTotal = Integer.parseInt(maxTotal);
}
String maxWaitMillis = prop.getProperty("redis.connect.maxWaitMillis");
if(isNotBlank(maxWaitMillis)){
this.maxWaitMillis = Long.parseLong(maxWaitMillis);
}
String testOnBorrow = prop.getProperty("redis.connect.testOnBorrow");
if(isNotBlank(testOnBorrow)){
this.testOnBorrow = Boolean.parseBoolean(testOnBorrow);
}
String testWhileIdle = prop.getProperty("redis.connect.testWhileIdle");
if(isNotBlank(testWhileIdle)){
this.testOnBorrow = Boolean.parseBoolean(testWhileIdle);
}
this.requirePass = prop.getProperty("redis.connect.requirePass");
initialPool(connectHost, connectPort,requirePass);
}
/**
* @Description: 单类模式获取instance的方法
* @param: @return
* @return: RedisManager
* @throws
*/
public static RedisManager getRedisManagerInstance(){
while(redisManager==null){
synchronized(RedisManager.class){
redisManager = new RedisManager();
}
}
return redisManager;
}
/**
* @Description: 判断字符串是否为空
* @param: @param str
* @param: @return
* @return: boolean
* @throws
*/
private boolean isNotBlank(String str){
if(str != null && str.length() > 0 && str.trim().length() > 0){
return true;
}
return false;
}
/**
* 初始化非切片池
*/
private void initialPool(String connectionUrl, int port, String requirePass) {
// 池基本配置
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(maxTotal);//总连接数
config.setMaxIdle(maxIdle);
config.setMaxWaitMillis(maxWaitMillis);//10s
config.setTestOnBorrow(testOnBorrow);
config.setTestWhileIdle(testWhileIdle);
this.jedisPool = new JedisPool(config, connectionUrl, port);
}
/**
* @Description: 获取Jedis实例
* @param: @return
* @return: Jedis
* @throws
*/
public Jedis getJedis() {
Jedis jedis = null;
try {
if (jedisPool != null) {
jedis = jedisPool.getResource();
//设置连接密码
jedis.auth(this.requirePass);
}
} catch (Exception e) {
throw new RuntimeException("get jedis failed");
}
return jedis;
}
}
测试:
@Test
public void testRedisManager(){
RedisManager redisManager1 = RedisManager.getRedisManagerInstance();
Jedis jedis = redisManager1.getJedis();
System.out.println(jedis.ping());
//使用连接池后底层会自动归还连接
jedis.close();
}
用完后直接使用jedis.close();
方法即可向池归还连接,贴出close实现源码:
@Override
public void close() {
if (dataSource != null) {
JedisPoolAbstract pool = this.dataSource;
this.dataSource = null;
if (client.isBroken()) {
pool.returnBrokenResource(this);
} else {
pool.returnResource(this);
}
} else {
super.close();
}
}
11.关于锁补充
表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般
悲观锁:每次获取数据的时候,都会担心数据被修改,所以每次获取数据的时候都会进行加锁,确保在自己使用的过程中数据不会被别人修改,直接将整张表上锁,使用完成后进行数据解锁。由于数据进行加锁,期间对该数据进行读写的其他线程都会进行等待。
乐观锁(OptimisticLock),顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量乐观锁策略:
提交版本必须大于记录当前版本才能执行更新。
InnoDB的行锁模式及加锁方法
InnoDB实现了以下两种类型的行锁。
- 共享锁(s):又称读锁。允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。
- 排他锁(X):又称写锁。允许获取排他锁的事务更新数据,阻止其他事务取得相同的数据集共享读锁和排他写锁。若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。
- 对于共享锁大家可能很好理解,就是多个事务只能读数据不能改数据。
对于排他锁大家的理解可能就有些差别,我当初就犯了一个错误,以为排他锁锁住一行数据后,其他事务就不能读取和修改该行数据,其实不是这样的。排他锁指的是一个事务在一行数据加上排他锁后,其他事务不能再在其上加其他的锁。mysql InnoDB引擎默认的修改数据语句:update,delete,insert都会自动给涉及到的数据加上排他锁,select语句默认不会加任何锁类型,如果加排他锁可以使用select …for update语句,加共享锁可以使用select … lock in share mode语句。所以加过排他锁的数据行在其他事务种是不能修改数据的,也不能通过for update和lock in share mode锁的方式查询数据,但可以直接通过select …from…查询数据,因为普通查询没有任何锁机制。
另外,为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁。
- 意向共享锁(IS):事务打算给数据行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁。
- 意向排他锁(IX):事务打算给数据行加排他锁,事务在给一个数据行加排他锁前必须先取得该表的IX锁。