013-Spring事务管理

说明

本文示例承接上一篇文章,如有需要请移步012-Spring DAO 数据访问对象
上文最后,我们提到了事务管理。对spring事务有了解的都知道,我们需要在Service对应的方法上添加@Transactional。

按照我们的示例,即便你添加了@Transactional注解,事务依然没有生效。因为我们示例中没有配置事务管理器。

事务的ACID特性

  • 原子性(Atomicity):整个事务中的所有操作,要么全部完成,要么全部失败,不可分割。不能部分成功,部分失败。
  • 一致性(Consistency):事务必须始终保持系统处于一致的状态,不管在任何给定的时间并发事务有多少。与原子性密切相关。
  • 隔离性(Isolation):一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
  • 持久性(Durability):在事务完成以后,该事务对数据库所作的更改便持久的保存在数据库之中,并不会被回滚。

Spring的事务管理器TransactionManager

TransactionManager是一个声明式接口,该接口没有任何方法定义。Spring中我们主要使用的是PlatformTransactionManager接口,该接口继承自TransactionManager。

package org.springframework.transaction;

public interface PlatformTransactionManager extends TransactionManager {

    TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;

    void commit(TransactionStatus status) throws TransactionException;

    void rollback(TransactionStatus status) throws TransactionException;
}

PlatformTransactionManager有如下几个常用的实现

说明
org.springframework.jdbc.datasource.DataSourceTransactionManagerJDBC事务管理器
org.springframework.orm.hibernate5.HibernateTransactionManagerHIbernate事务管理器
org.springframework.transaction.jta.JtaTransactionManagerJTA事务管理器

当然实际中不止这3个,目前阶段我们先知道这3个就够了.

Spring 支持事务管理方式

  • 编程式事务管理:使用TransactionTemplate或者直接使用底层的PlatformTransactionManager。对于编程式事务管理,spring推荐使用TransactionTemplate。
  • 声明式事务管理:建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。声明式事务最大的优点就是不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明(或通过基于@Transactional注解的方式),便可以将事务规则应用到业务逻辑中。

声明式事务管理也有两种常用的方式

  • 基于tx和aop名字空间的xml配置文件。
  • 基于@Transactional注解。显然基于注解的方式更简单易用。直接注解于方法或Service类上。

显然声明式事务管理要优于编程式事务管理,这正是spring倡导的非侵入式的开发方式。声明式事务管理使业务代码不受污染,一个普通的POJO对象,只要加上注解就可以获得完全的事务支持。和编程式事务相比,声明式事务唯一不足地方是,后者的最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。但是即便有这样的需求,也存在很多变通的方法,比如,可以将需要进行事务管理的代码块独立为方法等等。

改造示例

package com.yyoo.boot.dao;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.sql.DataSource;
import java.io.IOException;

@Configuration
@ComponentScan("com.yyoo.boot.dao")
@EnableTransactionManagement // 启用@Transactional注解
public class DaoConfig {

    @Value("classpath:dao.properties")
    private Resource configResource;

    @Bean
    public DataSource getDriverManagerDataSource() throws IOException {

        String propertiesPath = configResource.getFile().getAbsolutePath();
        HikariConfig hikariConfig = new HikariConfig(propertiesPath);

        HikariDataSource ds = new HikariDataSource(hikariConfig);

        return ds;
    }


    /**
     *
     * 因为JdbcTemplate一旦创建就是线程安全的,所以我们可以将它定义为一个单例的bean
     *
     */
    @Bean
    public JdbcTemplate getJdbcTemplate(DataSource dataSource){
        return new JdbcTemplate(dataSource);
    }

    
    @Bean("transactionManager")
    public TransactionManager getTransactionManager(DataSource dataSource){
        DataSourceTransactionManager dt = new DataSourceTransactionManager(dataSource);
        return dt;
    }

}

定义TransactionManager ,并使用@EnableTransactionManagement启用@Transactional注解

在Service方法上添加@Transactional注解

 	@Transactional
    @Override
    public void insert(TestBean test, DemoBean demo) {
        test1Dao.insert(test);
        int i = 1/0;

        demoDao.insert(demo);
    }

执行后你会发现,两个表都没有成功添加数据。表示我们的事务控制加上了。

事务的相关概念

要了解事务的特性,我们需要先了解如下几个概念:

脏读

脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。

不可重复读

是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。(一个事务内重复读取数据不一致)

幻读

是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。(不同事务内读取数据不一致)

高并发下的更新丢失问题

我们来看如下两个经典示例。发工资了,但是工资和奖金分开发,而且是不同的财务人员发放(即我们的事务1和事务2)。

时间事务一操作事务二操作
T1开始事务
T2开始事务
T3查询账户余额为2000元查询账户余额为2000元
T4发工资8000元
T5发奖金2000元
T6提交事务
T7工资发错了,事务回滚工资修改为4000元
T8工资回滚为2000元
可以看到,最后的余额为2000元。但是我们的奖金发放的事务是正常提交的,最后莫名的少了2000元。这种情况,我们称为第一类丢失更新。

第一类丢失更新的特点是一个事务正常提交,一个事务回滚,回滚的事务撤销掉了正常提交的事务的提交。

时间事务一操作事务二操作
T1开始事务
T2开始事务
T3查询账户余额为2000元查询账户余额为2000元
T4发工资8000元
T5发奖金2000元
T6提交事务
T7提交事务工资修改为4000元
T8工资余额修改为10000元
可以看到,最后的余额为10000元,奖金又没有拿到(这倒挺像老板的承诺哈哈)。这种情况,我们称之为第二类丢失更新。

第二类丢失更新的特点是两个事务都正常提交,后提交的事务覆盖掉了先提交的事务的结果。

两类丢失更新的解决办法

所有的数据库都已经解决了第一类丢失更新的情况。所以我们只需要解决第二类丢失更新的情况。

以 MySQL 为例,MySQL 默认隔离级别为 REPEATABLE_READ (可重复读),说明同一个事务中 MySQL 两次读取信息一样,意味着 A 事务修改数据 a = 10;a 原值为 5;那么 B 事务在 A 事务提交之前读取值为 5,当A事务提交后,B 事务读取值依然是 5(B 事务在开启时有一个数据快照,在事务中每次读取都是这个快照)。这种情况,会导致第二类丢失更新。第二类丢失更新的解决办法一般有如下两个:乐观锁、悲观锁

悲观锁

在数据库表级或行级加锁,同时只能有一个事务对表或行进行修改。相关 sql 语句类似 select …[where a = 1] for update。此方式容易造成事务积压,特别是长事务较多的情况下,锁的时间会加长,进一步导致事务积压(并发效率低)。

另一个跟悲观锁一样的策略就是使用redis,对要执行的事务代码加锁,同样是同一时间内只有一个事务进行操作,并发效率低。

乐观锁

在表中添加版本控制字段(如version),每次修改增加一个版本,修改时将先读取版本号,修改时根据版本号更新,如果版本号被其他事务改变,则不提交更新,进行重试(注意:重试不能在同一个事务中重试,因为 MySQL 默认策略为可重复读,一直读取事务开启时的快照值)。

第二类更新丢失示例

先查询出来原值,计算后更新为新值

    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean testTx(ProjectBean bean) {
        ProjectBean old = getById(bean.getId());
        int type;

        try {
            int t = RandomUtils.nextInt(10, 1000);
            type = old.getType();
            System.out.println("线程:" + Thread.currentThread().getName() + ";sleep :" + t + ";typeOld:" + type);
            Thread.sleep(t);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        bean.setType(old.getType()+1);// 获取旧值执行 +1 操作
        return updateById(bean);
    }
    @Resource
    private ProjectService projectService;

    @Test
    public void testTx() throws InterruptedException {

        ProjectBean bean = new ProjectBean();
        bean.setId(1L);

        Runnable r = () -> {
              update = projectService.testTx(bean);
        };

        for(int i = 0; i < 100; i++) {
            Thread t = new Thread(r,"thread-" + i);
            t.start();
        }
        Thread.sleep(10000000);

    }

以上是我们最常犯的错误,执行结果并不是 type 字段被增加了 100,最终结果可能 type 的值才增加了很少,说明很多线程都更新丢失了。我们增加乐观锁判断代码如下

乐观锁判断+CAS自旋的方式

    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean testTx(ProjectBean bean) {
            ProjectBean old = getById(bean.getId());
            int type;

            try {
                int t = RandomUtils.nextInt(10, 1000);
                type = old.getType();
                System.out.println("线程:" + Thread.currentThread().getName() + ";sleep :" + t + ";typeOld:" + type);
                Thread.sleep(t);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            UpdateWrapper<ProjectBean> updateWrapper = Wrappers.update();
            // 以旧值作为乐观锁进行判断,如果值已改变则更新结果为false,可以进行重试
            updateWrapper.set("type", type + 1).ge("id", old.getId()).eq("type", type);

            return update(updateWrapper);
    }
    @Test
    public void testTx() throws InterruptedException {

        ProjectBean bean = new ProjectBean();
        bean.setId(1L);

        Runnable r = () -> {
            boolean update = false;
            while (!update) {// 如果更新失败则重试(这里也可以设置重试次数等,具体情况可自行设计)
                update = projectService.testTx(bean);
            }
        };

        for(int i = 0; i < 100; i++) {
            Thread t = new Thread(r,"thread-" + i);
            t.start();
        }
        Thread.sleep(10000000);

    }

这样做最终结果是没有问题的,但针对此需求(每个线程对 type 值执行 +1 操作并记录到数据库)我们还可以通过 sql 语句,update set field = field + 1 where … 的方式来做到

通过sql 语句的 update set field = field + 1 where … 的方式

    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean testTxSql(ProjectBean bean) {
        ProjectBean old = getById(bean.getId());
        int type;

        try {
            int t = RandomUtils.nextInt(10, 1000);
            type = old.getType();
            System.out.println("线程:" + Thread.currentThread().getName() + ";sleep :" + t + ";typeOld:" + type);
            Thread.sleep(t);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        UpdateWrapper<ProjectBean> updateWrapper = Wrappers.update();
        // 以旧值作为乐观锁进行判断,如果值已改变则更新结果为false,可以进行重试
        updateWrapper.setSql("type = type + " + 1).ge("id", old.getId());

        return update(updateWrapper);
    }
    @Test
    public void testTxSql() throws InterruptedException {

        Runnable r = () -> {
            ProjectBean bean = new ProjectBean();
            bean.setId(1L);
            projectService.testTxSql(bean);
        };

        for(int i = 0; i < 100; i++) {
            Thread t = new Thread(r,"thread-" + i);
            t.start();
        }
        Thread.sleep(10000000);

    }

我们这里是同时启动100个线程,启动 1000 个线程也可以正确执行(可能需要你提高线程池的配置,比如等待链接的时间,或连接池的大小)

事务的特性

在spring中由TransactionDefinition接口定义spring的特性。

  • 事务的隔离级别
  • 事务的传播行为
  • 事务超时时间
  • 事务是否只读
  • 事务回滚规则

事务隔离级别

隔离级别是指若干个并发的事务之间的隔离程度。例如,这个事务可以看到来自其他事务的未提交的写入吗?Isolation 枚举类中定义了五个表示隔离级别的常量:

常量名称名称常量说明常量值
DEFAULT默认这是默认值,表示使用底层数据库的默认隔离级别。对大部分数据库而言,通常这值就是Isolation.READ_COMMITTED。-1
READ_UNCOMMITTED未读提交该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据。该级别不能防止脏读,不可重复读和幻读,因此很少使用该隔离级别。比如PostgreSQL实际上并没有此级别。1
READ_COMMITTED已读提交该隔离级别表示一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读,这也是大多数情况下的推荐值。2
REPEATABLE_READ可重复读该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。该级别可以防止脏读和不可重复读。4
SERIALIZABLE可串行化所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。8

事务的传播行为

所谓事务的传播行为是指,如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为。在Propagation定义中包括了如下几个表示传播行为的常量

常量名称常量说明常量值
REQUIRED如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是默认值。0
SUPPORTS如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。1
MANDATORY如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。2
REQUIRES_NEW建一个新的事务,如果当前存在事务,则把当前事务挂起3
NOT_SUPPORTED以非事务方式运行,如果当前存在事务,则把当前事务挂起。4
NEVER以非事务方式运行,如果当前存在事务,则抛出异常。5
NESTED如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于Propagation.REQUIRED。6

事务超时

此事务在超时和被底层事务基础设施自动回滚之前运行的时间。即事务执行时间超过该时间,事务会被自动回滚。
我们可以在@Transactional或者DataSourceTransactionManager的setDefaultTimeout中设置事务超时时间。

事务超时时间默认是-1,不超时。一般情况下我们也是如此设置。

设置超时时间

DataSourceTransactionManager dt = new DataSourceTransactionManager(dataSource);
dt.setDefaultTimeout(1);
@Transactional(timeout = 1)

注意:以上两种设置时间的单位都是秒

只读状态

当您的代码读取但不修改数据时,您可以使用只读事务。在某些情况下,只读事务可能是一种有用的优化,例如使用 Hibernate 时。

事务回滚规则

默认配置下,spring只有在抛出的异常为运行时unchecked异常时才回滚该事务,也就是抛出的异常为RuntimeException的子类(Errors也会导致事务回滚),而抛出checked异常则不会导致事务回滚。可以明确的配置在抛出那些异常时回滚事务,包括checked异常。也可以明确定义那些异常抛出时不回滚事务。还可以编程性的通过setRollbackOnly()方法来指示一个事务必须回滚,在调用完setRollbackOnly()后你所能执行的唯一操作就是回滚。

也就是说在如下情况下事务不会回滚

    @Transactional
    @Override
    public void insert(TestBean test, DemoBean demo)throws Exception {
        test1Dao.insert(test);

        if(true){
            throw new Exception("");
        }

        demoDao.insert(demo);
    }

执行之后你会发现t_test数据插入成功,而t_demo数据插入失败

意味着我们在使用@Transactional应该尽量的规定其rollbackFor属性为Exception.class,像如下这样

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void insert(TestBean test, DemoBean demo)throws Exception {
        test1Dao.insert(test);

        if(true){
            throw new Exception("");
        }

        demoDao.insert(demo);
    }

@Transactional的属性

名称类型说明
valueString可选的限定描述符,指定使用的事务管理器的bean名称是属性transactionManager的别名。如果有多个事务管理器,就需要使用名称来限定。
propagationenum: Propagation可选的事务传播行为设置
isolationenum: Isolation可选的事务隔离级别设置
readOnlyboolean读写或只读事务,默认读写
timeoutint (单位:秒)事务超时时间设置
rollbackForClass对象数组,必须继承自Throwable导致事务回滚的异常类数组
rollbackForClassName类名数组,必须继承自Throwable导致事务回滚的异常类名字数组
noRollbackForClass对象数组,必须继承自Throwable不会导致事务回滚的异常类数组
noRollbackForClassName类名数组,必须继承自Throwable不会导致事务回滚的异常类名字数组
labelString用于向交易添加表达性描述的标签数组标签可以由事务管理器评估以将特定于实现的行为与实际事务相关联

自定义组合注释

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional(transactionManager = "order", label = "causal-consistency",rollbackFor = Exception.class)
public @interface OrderTx {
}

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional(transactionManager = "account", label = "retryable",rollbackFor = Exception.class)
public @interface AccountTx {
}

这样我们可以使用@OrderTx 或@AccountTx 替代@Transactional,而且@OrderTx 或@AccountTx都有固定的属性值。

Spring中@Transactional注解失效场景

AOP动态代理的原理

  • JDK实现动态代理需要实现类通过接口定义业务方法,对于没有接口的类,使用cglib动态代理。
  • CGLib采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。(这意味着如果被代理的类是final的、方法是private的、static的,cglib将不能代理)

第一种(cglib动态代理导致)

Transactional注解标注方法修饰符为private时或者是final修饰、static修饰,@Transactional注解将会不起作用。

第二种(代理的方法中调用的是原方法)

在类内部调用调用类内部@Transactional标注的方法。这种情况下也会导致事务不开启。示例代码如下。

第三种(异常回滚规则导致)

事务方法内部捕捉了异常,没有抛出新的异常,导致事务操作不会进行回滚。示例代码如下。

示例

第一种和第三种情况我们很少遇到,关键第二种情况。我们改造我们的示例程序如下

    @Transactional(rollbackFor = Exception.class)
    public void insert(TestBean test,DemoBean demo) throws Exception {
        test1Dao.insert(test);
        insert(demo);
        if(true){
            throw new Exception("");
        }
    }

    @Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRES_NEW)
    public void insert(DemoBean demoBean)  {
        demoDao.insert(demoBean);
    }

当我们执行insert(TestBean test,DemoBean demo) 方法(方法a)时,就会出现insert(DemoBean demoBean)方法(方法b)的事务注解无效。

按照我们一般的理解,我们被调用的方法b,事务的传播特性为REQUIRES_NEW,即为建一个新的事务,如果当前存在事务,则把当前事务挂起。这样的话,即便我们最后抛出了异常,那是a方法的异常,并不能影响我们b方法的事务提交。但实际上b方法的数据依然没有插入。(我们实际中会有实际需要,比如记录日志,不管我们的业务代码是否正常完成,我们的日志都要记录。)

因为spring中声明式事务使用aop织入相关事务逻辑。调用insert方法的对象实际上是myService的代理类MyServiceProxy对象调用其insert方法。伪代码如下:


pubic void insert(TestBean test,DemoBean demo){
	try{	
		// 开启事务的代码
		myService.insert(TestBean test,DemoBean demo); 
		// 代理方法中使用了原对象调用对应方法
		// 提交事务的代码
	}catch(Exception e){
	 .....
	}finally{
		// 事务回滚的方法
	}
}

那么我们要b方法的代理方法有效怎么办?

其实很简单,将b方法定义在其他的类中,并将新定义的类注入到a方法所在类。

通过Spring容器注入的引用,其实已经是被代理的类的引用了。在其他类中定义b方法在a方法中调用实际上是使用的b方法对于类的代理类。所以有效。

常见问题

UnexpectedRollbackException 异常

org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
	at org.springframework.transaction.support.AbstractPlatformTransactionManager.processRollback(AbstractPlatformTransactionManager.java:873)
	at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:710)
	at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:533)
	at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:304)
	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:98)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:688)

多层嵌套事务中,如果使用了默认的事务传播方式,当内层事务抛出异常(按理说它应该执行失败),外层事务将异常捕捉处理并不抛出异常时,外层事务代码就没有异常(按理说它应该执行成功),这样一个事务执行成功,一个事务执行失败,而又使用默认的事务传播方式,就会报出rollback-only异常

处理方式

  • 如果内外层事务需要同时成功或同时失败,则外层事务可以不用 try … catch 内层事务或者需要将 内层事务的异常进行抛出,以便进行事务回滚
  • 如果希望内层事务回滚,但不影响外层事务提交,需要将内层事务的传播方式指定为 REQUIRES_NEW。

上一篇:012-Spring DAO 数据访问对象
下一篇:待续

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
基于SSM+MySQL的企业人力资源管理系统是一个用于企业内部人力资源管理的综合平台。该系统采用了SSM(Spring+SpringMVC+MyBatis)框架作为后端开发技术,并结合MySQL数据库进行数据存储和管理。 企业人力资源管理系统旨在提供一个集中管理和优化企业人力资源的解决方案,以提高人力资源管理的效率和准确性。系统提供了一系列功能,包括员工信息管理、招聘流程管理、培训管理、绩效考核、薪酬管理等,以满足企业对人力资源的全面管理需求。 系统的主要功能包括: 员工信息管理:管理员可以在系统中录入和管理员工的基本信息,包括姓名、部门、职位、联系方式等。同时,系统还提供了员工档案的管理功能,可记录员工的学历、工作经验、个人技能等详细信息。 招聘流程管理:系统提供了招聘需求发布、简历筛选、面试安排等功能,帮助企业管理招聘流程并快速筛选合适的人才。管理员可以在系统中发布招聘岗位,设置筛选条件,并根据候选人的简历和面试表现进行评估和选择。 培训管理:系统记录和管理企业内部的培训活动,包括培训计划、培训内容、参与人员等。管理员可以根据不同岗位的需求制定培训计划,并跟踪员工的培训进度和效果。 绩效考核:系统提供绩效考核指标的设定和绩效评估功能。管理员可以设定不同岗位的考核指标,并根据员工的绩效表现进行评估和打分。系统自动生成绩效报表,帮助企业评估员工的工作表现和激励优秀员工。 薪酬管理:系统管理员工的薪资信息,包括基本工资、奖金、津贴等。管理员可以根据企业政策和员工绩效情况进行薪酬调整,并生成工资单和薪资报表。 基于SSM框架和MySQL数据库的应用,企业人力资源管理系统实现了数据的高效存储和管理。通过使用MyBatis进行数据库操作,提高了数据访问的性能和效率。同时,Spring框架提供了依赖注入、事务管理等功能,简化了系统的开发和维护工作,增强了系统的可扩展性和稳定性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值